Skip to content

Commit

Permalink
refactor: Add more git utils (#645)
Browse files Browse the repository at this point in the history
Adds the functions `NormalizeGitURI`, `ParseGitURIAndRef`, and
`ValidateGitRef`. `ParseGitRef` was updated to be permissive of the ref
type whereas `ValidateGitRef` validates that the type is of a given
type.

Code extracted from #641

Signed-off-by: Ian Lewis <ianlewis@google.com>
  • Loading branch information
ianlewis committed Jul 1, 2023
1 parent e2b1828 commit 965f578
Show file tree
Hide file tree
Showing 4 changed files with 349 additions and 69 deletions.
87 changes: 28 additions & 59 deletions verifiers/internal/gha/provenance.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,100 +76,69 @@ func verifyBuilderIDLooseMatch(prov iface.Provenance, expectedBuilderID string)
return nil
}

func asURI(s string) string {
source := s
if !strings.HasPrefix(source, "https://") &&
!strings.HasPrefix(source, "git+") {
source = "git+https://" + source
}
if !strings.HasPrefix(source, "git+") {
source = "git+" + source
}

return source
}

// Verify source URI in provenance statement.
func verifySourceURI(prov iface.Provenance, expectedSourceURI string, allowNoMaterialRef bool) error {
source := asURI(expectedSourceURI)
source := utils.NormalizeGitURI(expectedSourceURI)

// We expect github.com URIs only.
if !strings.HasPrefix(source, "git+https://github.com/") {
return fmt.Errorf("%w: expected source github.com repository '%s'", serrors.ErrorMalformedURI,
return fmt.Errorf("%w: expected source github.com repository %q", serrors.ErrorMalformedURI,
source)
}

// Verify source in the trigger
fullConfigURI, err := prov.TriggerURI()
fullTriggerURI, err := prov.TriggerURI()
if err != nil {
return err
}

configURI, err := sourceFromURI(fullConfigURI, false)
triggerURI, triggerRef, err := utils.ParseGitURIAndRef(fullTriggerURI)
if err != nil {
return err
}
if configURI != source {
return fmt.Errorf("%w: expected source '%s' in configSource.uri, got '%s'", serrors.ErrorMismatchSource,
source, fullConfigURI)
if triggerURI != source {
return fmt.Errorf("%w: expected source '%s' in configSource.uri, got %q", serrors.ErrorMismatchSource,
source, fullTriggerURI)
}
// We expect the trigger URI to always have a ref.
if triggerRef == "" {
return fmt.Errorf("%w: missing ref: %q", serrors.ErrorMalformedURI, fullTriggerURI)
}

// Verify source from material section.
materialSourceURI, err := prov.SourceURI()
fullSourceURI, err := prov.SourceURI()
if err != nil {
return err
}

materialURI, err := sourceFromURI(materialSourceURI, allowNoMaterialRef)
sourceURI, sourceRef, err := utils.ParseGitURIAndRef(fullSourceURI)
if err != nil {
return err
}
if materialURI != source {
return fmt.Errorf("%w: expected source '%s' in material section, got '%s'", serrors.ErrorMismatchSource,
source, materialSourceURI)
if sourceURI != source {
return fmt.Errorf("%w: expected source '%s' in material section, got %q", serrors.ErrorMismatchSource,
source, fullSourceURI)
}

// Last, verify that both fields match.
// We use the full URI to match on the tag as well.
if allowNoMaterialRef && len(strings.Split(materialSourceURI, "@")) == 1 {
// NOTE: this is an exception for npm packages built before GA,
// see https://github.com/slsa-framework/slsa-verifier/issues/492.
// We don't need to compare the ref since materialSourceURI does not contain it.
return nil
if sourceRef == "" {
if allowNoMaterialRef {
// NOTE: this is an exception for npm packages built before GA,
// see https://github.com/slsa-framework/slsa-verifier/issues/492.
// We don't need to compare the ref since materialSourceURI does not contain it.
return nil
}
return fmt.Errorf("%w: missing ref: %q", serrors.ErrorMalformedURI, fullSourceURI)
}
if fullConfigURI != materialSourceURI {
return fmt.Errorf("%w: material and config URIs do not match: '%s' != '%s'",

if fullTriggerURI != fullSourceURI {
return fmt.Errorf("%w: material and config URIs do not match: %q != %q",
serrors.ErrorInvalidDssePayload,
fullConfigURI, materialSourceURI)
fullTriggerURI, fullSourceURI)
}

return nil
}

// sourceFromURI retrieves the source repository given a repository URI with ref.
//
// NOTE: `allowNoRef` is to allow for verification of npm packages
// generated before GA. Their provenance did not have a ref,
// see https://github.com/slsa-framework/slsa-verifier/issues/492.
// `allowNoRef` should be set to `false` for all other cases.
func sourceFromURI(uri string, allowNoRef bool) (string, error) {
if uri == "" {
return "", fmt.Errorf("%w: empty uri", serrors.ErrorMalformedURI)
}

r := strings.Split(uri, "@")
if len(r) < 2 && !allowNoRef {
return "", fmt.Errorf("%w: %s", serrors.ErrorMalformedURI,
uri)
}
if len(r) < 1 {
return "", fmt.Errorf("%w: %s", serrors.ErrorMalformedURI,
uri)
}

return r[0], nil
}

// Verify Subject Digest from the provenance statement.
func verifyDigest(prov iface.Provenance, expectedHash string) error {
subjects, err := prov.Subjects()
Expand Down
7 changes: 7 additions & 0 deletions verifiers/internal/gha/provenance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,13 @@ func Test_verifySourceURI(t *testing.T) {
expectedSourceURI: "git+https://github.com/some/repo",
err: serrors.ErrorMalformedURI,
},
{
name: "not github repo",
provTriggerURI: "git+https://notgithub.mirror.nvdadr.com/some/repo@v1.2.3",
provMaterialsURI: "git+https://notgithub.mirror.nvdadr.com/some/repo@v1.2.3",
expectedSourceURI: "git+https://notgithub.mirror.nvdadr.com/some/repo",
err: serrors.ErrorMalformedURI,
},
{
name: "match source",
provTriggerURI: "git+https://github.com/some/repo@v1.2.3",
Expand Down
55 changes: 45 additions & 10 deletions verifiers/utils/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,62 @@ import (
serrors "github.com/slsa-framework/slsa-verifier/v2/errors"
)

// ParseGitRef validates that the given git ref is a valid ref of the given type and returns its name.
func ParseGitRef(refType, ref string) (string, error) {
refPrefix := fmt.Sprintf("refs/%s/", refType)
if !strings.HasPrefix(ref, refPrefix) {
return "", fmt.Errorf("%w: %s: not of the form '%s<name>'", serrors.ErrorInvalidRef, ref, refPrefix)
// NormalizeGitURI normalizes a git URI to include a git+https:// prefix.
func NormalizeGitURI(s string) string {
if !strings.HasPrefix(s, "git+") {
if !strings.Contains(s, "://") {
return "git+https://" + s
}
return "git+" + s
}
return s
}

// ParseGitURIAndRef retrieves the URI and ref from the given URI.
func ParseGitURIAndRef(uri string) (string, string, error) {
if uri == "" {
return "", "", fmt.Errorf("%w: empty uri", serrors.ErrorMalformedURI)
}
if !strings.HasPrefix(uri, "git+") {
return "", "", fmt.Errorf("%w: not a git URI: %q", serrors.ErrorMalformedURI, uri)
}

r := strings.SplitN(uri, "@", 2)
if len(r) < 2 {
return r[0], "", nil
}

name := strings.TrimPrefix(ref, refPrefix)
if strings.TrimSpace(name) == "" {
return "", fmt.Errorf("%w: %s: not of the form '%s<name>'", serrors.ErrorInvalidRef, ref, refPrefix)
return r[0], r[1], nil
}

// ParseGitRef parses the git ref and returns its type and name.
func ParseGitRef(ref string) (string, string) {
parts := strings.SplitN(ref, "/", 3)
if len(parts) < 3 || parts[0] != "refs" {
return "", ref
}
return parts[1], parts[2]
}

// ValidateGitRef validates that the given git ref is a valid ref of the given type and returns its name.
func ValidateGitRef(refType, ref string) (string, error) {
typ, name := ParseGitRef(ref)
if typ != refType {
return "", fmt.Errorf("%w: %q: unexpected ref type: %q", serrors.ErrorInvalidRef, ref, typ)
}
if name == "" {
return "", fmt.Errorf("%w: %q: empty ref name", serrors.ErrorInvalidRef, ref)
}

return name, nil
}

// TagFromGitRef returns the tagname from a tag ref.
func TagFromGitRef(ref string) (string, error) {
return ParseGitRef("tags", ref)
return ValidateGitRef("tags", ref)
}

// BranchFromGitRef returns the tagname from a tag ref.
func BranchFromGitRef(ref string) (string, error) {
return ParseGitRef("heads", ref)
return ValidateGitRef("heads", ref)
}
Loading

0 comments on commit 965f578

Please sign in to comment.