diff --git a/cli/slsa-verifier/main_test.go b/cli/slsa-verifier/main_test.go index 073578d54..7c994f43b 100644 --- a/cli/slsa-verifier/main_test.go +++ b/cli/slsa-verifier/main_test.go @@ -21,7 +21,7 @@ import ( "github.com/slsa-framework/slsa-verifier/cli/slsa-verifier/verify" serrors "github.com/slsa-framework/slsa-verifier/errors" - "github.com/slsa-framework/slsa-verifier/verifiers/container" + "github.com/slsa-framework/slsa-verifier/verifiers/utils/container" ) func errCmp(e1, e2 error) bool { @@ -511,10 +511,14 @@ func Test_runVerifyArtifactPath(t *testing.T) { return } - if tt.outBuilderID != "" && outBuilderID != tt.outBuilderID { - t.Errorf(cmp.Diff(outBuilderID, tt.outBuilderID)) + // Validate against test's expected builderID, if provided. + if tt.outBuilderID != "" { + if err := outBuilderID.Matches(tt.outBuilderID); err != nil { + t.Errorf(fmt.Sprintf("matches failed: %v", err)) + } } + // TODO: verify using Matches(). } }) } @@ -675,9 +679,14 @@ func Test_runVerifyGHAArtifactImage(t *testing.T) { return } - if tt.outBuilderID != "" && outBuilderID != tt.outBuilderID { - t.Errorf(cmp.Diff(outBuilderID, tt.outBuilderID)) + // Validate against test's expected builderID, if provided. + if tt.outBuilderID != "" { + if err := outBuilderID.Matches(tt.outBuilderID); err != nil { + t.Errorf(fmt.Sprintf("matches failed: %v", err)) + } } + + // TODO: verify using Matches(). } }) } @@ -729,6 +738,29 @@ func Test_runVerifyGCBArtifactImage(t *testing.T) { provenance: "gcloud-container-github.json", source: "github.com/laurentsimon/gcb-tests", }, + { + name: "mismatch input builder version", + artifact: "gcloud-container-github", + provenance: "gcloud-container-github.json", + source: "github.com/laurentsimon/gcb-tests", + pBuilderID: pString(builder + "@v0.4"), + err: serrors.ErrorMismatchBuilderID, + }, + { + name: "unsupported builder", + artifact: "gcloud-container-github", + provenance: "gcloud-container-github.json", + source: "github.com/laurentsimon/gcb-tests", + pBuilderID: pString(builder + "a"), + err: serrors.ErrorVerifierNotSupported, + }, + { + name: "match output builder name", + artifact: "gcloud-container-github", + provenance: "gcloud-container-github.json", + source: "github.com/laurentsimon/gcb-tests", + outBuilderID: builder, + }, { name: "invalid repo name", artifact: "gcloud-container-github", @@ -858,17 +890,20 @@ func Test_runVerifyGCBArtifactImage(t *testing.T) { for _, v := range checkVersions { semver := path.Base(v) - builderID := pString(builder + "@" + semver) + // For each test, we run 2 sub-tests: + // 1. With the the full builderID including the semver. + // 2. With only the name of the builder. + builderIDs := []string{builder + "@" + semver, builder} provenance := filepath.Clean(filepath.Join(TEST_DIR, v, tt.provenance)) image := tt.artifact var fn verify.ComputeDigestFn // If builder ID is set, use it. if tt.pBuilderID != nil { - if !tt.noversion { - panic("builderID set but not noversion option") - } - builderID = tt.pBuilderID + // if !tt.noversion { + // panic("builderID set but not noversion option") + // } + builderIDs = []string{*tt.pBuilderID} } // Select the right image according to the builder version we are testing. @@ -889,30 +924,42 @@ func Test_runVerifyGCBArtifactImage(t *testing.T) { fn = localDigestComputeFn } - cmd := verify.VerifyImageCommand{ - SourceURI: tt.source, - SourceBranch: nil, - BuilderID: builderID, - SourceTag: nil, - SourceVersionTag: nil, - DigestFn: fn, - ProvenancePath: &provenance, - } + // We run the test for each builderID, in order to test + // a builderID provided by name and one containing both the name + // and semver. + for _, bid := range builderIDs { + cmd := verify.VerifyImageCommand{ + SourceURI: tt.source, + SourceBranch: nil, + BuilderID: &bid, + SourceTag: nil, + SourceVersionTag: nil, + DigestFn: fn, + ProvenancePath: &provenance, + } - outBuilderID, err := cmd.Exec(context.Background(), []string{image}) + outBuilderID, err := cmd.Exec(context.Background(), []string{image}) - if !errCmp(err, tt.err) { - t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) - } + if !errCmp(err, tt.err) { + t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) + } - if err != nil { - return - } + if err != nil { + return + } - if tt.outBuilderID != "" && outBuilderID != tt.outBuilderID { - t.Errorf(cmp.Diff(outBuilderID, tt.outBuilderID)) - } + // Validate against test's expected builderID, if provided. + if tt.outBuilderID != "" { + if err := outBuilderID.Matches(tt.outBuilderID); err != nil { + t.Errorf(fmt.Sprintf("matches failed: %v", err)) + } + } + // Validate against builderID we generated automatically. + if err := outBuilderID.Matches(bid); err != nil { + t.Errorf(fmt.Sprintf("matches failed: %v", err)) + } + } } }) } diff --git a/cli/slsa-verifier/verify/verify_artifact.go b/cli/slsa-verifier/verify/verify_artifact.go index 6c40e58bd..16ad8bd2d 100644 --- a/cli/slsa-verifier/verify/verify_artifact.go +++ b/cli/slsa-verifier/verify/verify_artifact.go @@ -24,6 +24,7 @@ import ( "github.com/slsa-framework/slsa-verifier/options" "github.com/slsa-framework/slsa-verifier/verifiers" + "github.com/slsa-framework/slsa-verifier/verifiers/utils" ) // Note: nil branch, tag, version-tag and builder-id means we ignore them during verification. @@ -38,10 +39,10 @@ type VerifyArtifactCommand struct { PrintProvenance bool } -func (c *VerifyArtifactCommand) Exec(ctx context.Context, artifacts []string) (string, error) { +func (c *VerifyArtifactCommand) Exec(ctx context.Context, artifacts []string) (*utils.BuilderID, error) { artifactHash, err := getArtifactHash(artifacts[0]) if err != nil { - return "", err + return nil, err } provenanceOpts := &options.ProvenanceOpts{ @@ -59,12 +60,12 @@ func (c *VerifyArtifactCommand) Exec(ctx context.Context, artifacts []string) (s provenance, err := os.ReadFile(c.ProvenancePath) if err != nil { - return "", err + return nil, err } verifiedProvenance, outBuilderID, err := verifiers.VerifyArtifact(ctx, provenance, artifactHash, provenanceOpts, builderOpts) if err != nil { - return "", err + return nil, err } if c.PrintProvenance { diff --git a/cli/slsa-verifier/verify/verify_image.go b/cli/slsa-verifier/verify/verify_image.go index 064a4df2b..489b2c792 100644 --- a/cli/slsa-verifier/verify/verify_image.go +++ b/cli/slsa-verifier/verify/verify_image.go @@ -21,7 +21,8 @@ import ( "github.com/slsa-framework/slsa-verifier/options" "github.com/slsa-framework/slsa-verifier/verifiers" - "github.com/slsa-framework/slsa-verifier/verifiers/container" + "github.com/slsa-framework/slsa-verifier/verifiers/utils" + "github.com/slsa-framework/slsa-verifier/verifiers/utils/container" ) type ComputeDigestFn func(string) (string, error) @@ -40,7 +41,7 @@ type VerifyImageCommand struct { DigestFn ComputeDigestFn } -func (c *VerifyImageCommand) Exec(ctx context.Context, artifacts []string) (string, error) { +func (c *VerifyImageCommand) Exec(ctx context.Context, artifacts []string) (*utils.BuilderID, error) { artifactImage := artifacts[0] // Retrieve the image digest. if c.DigestFn == nil { @@ -48,12 +49,12 @@ func (c *VerifyImageCommand) Exec(ctx context.Context, artifacts []string) (stri } digest, err := c.DigestFn(artifactImage) if err != nil { - return "", err + return nil, err } // Verify that the reference is immutable. if err := container.ValidateArtifactReference(artifactImage, digest); err != nil { - return "", err + return nil, err } provenanceOpts := &options.ProvenanceOpts{ @@ -73,13 +74,13 @@ func (c *VerifyImageCommand) Exec(ctx context.Context, artifacts []string) (stri if c.ProvenancePath != nil { provenance, err = os.ReadFile(*c.ProvenancePath) if err != nil { - return "", err + return nil, err } } verifiedProvenance, outBuilderID, err := verifiers.VerifyImage(ctx, artifacts[0], provenance, provenanceOpts, builderOpts) if err != nil { - return "", err + return nil, err } if c.PrintProvenance { diff --git a/experimental/rest/service.go b/experimental/rest/service.go index b3f58719c..cfe2df229 100644 --- a/experimental/rest/service.go +++ b/experimental/rest/service.go @@ -139,7 +139,7 @@ func verifyHandlerV1(r *http.Request) *v1Result { results = results.withIntotoStatement(p) } - return results.withBuilderID(builderID).withValidation(validationSuccess) + return results.withBuilderID(builderID.String()).withValidation(validationSuccess) } func queryFromString(content []byte) (*v1Query, error) { diff --git a/register/register.go b/register/register.go index 32622cbb1..a7fd2f630 100644 --- a/register/register.go +++ b/register/register.go @@ -4,6 +4,7 @@ import ( "context" "github.com/slsa-framework/slsa-verifier/options" + "github.com/slsa-framework/slsa-verifier/verifiers/utils" ) var SLSAVerifiers = make(map[string]SLSAVerifier) @@ -12,21 +13,21 @@ type SLSAVerifier interface { // IsAuthoritativeFor checks whether a verifier can // verify provenance for a given builder identified by its // `BuilderID`. - IsAuthoritativeFor(builderID string) bool + IsAuthoritativeFor(builderIDName string) bool // VerifyArtifact verifies a provenance for a supplied artifact. VerifyArtifact(ctx context.Context, provenance []byte, artifactHash string, provenanceOpts *options.ProvenanceOpts, builderOpts *options.BuilderOpts, - ) ([]byte, string, error) + ) ([]byte, *utils.BuilderID, error) // VerifyImage verifies a provenance for a supplied OCI image. VerifyImage(ctx context.Context, provenance []byte, artifactImage string, provenanceOpts *options.ProvenanceOpts, builderOpts *options.BuilderOpts, - ) ([]byte, string, error) + ) ([]byte, *utils.BuilderID, error) } func RegisterVerifier(name string, verifier SLSAVerifier) { diff --git a/verifiers/internal/gcb/provenance.go b/verifiers/internal/gcb/provenance.go index 19dba9ad1..b6d0d542a 100644 --- a/verifiers/internal/gcb/provenance.go +++ b/verifiers/internal/gcb/provenance.go @@ -19,6 +19,7 @@ import ( serrors "github.com/slsa-framework/slsa-verifier/errors" "github.com/slsa-framework/slsa-verifier/options" "github.com/slsa-framework/slsa-verifier/verifiers/internal/gcb/keys" + "github.com/slsa-framework/slsa-verifier/verifiers/utils" ) var GCBBuilderIDs = []string{ @@ -221,28 +222,18 @@ func isValidBuilderID(id string) error { return serrors.ErrorInvalidBuilderID } -func getBuilderVersion(builderID string) (string, error) { - parts := strings.Split(builderID, "@") - if len(parts) != 2 { - return "", fmt.Errorf("%w: '%s'", serrors.ErrorInvalidFormat, parts) - } - return parts[1], nil -} - -func validateRecipeType(builderID, recipeType string) error { - v, err := getBuilderVersion(builderID) - if err != nil { - return err - } +func validateRecipeType(builderID utils.BuilderID, recipeType string) error { + var err error + v := builderID.Version() switch v { case "v0.2": // In this version, the recipe type should be the same as // the builder ID. - if builderID == recipeType { + if builderID.String() == recipeType { return nil } err = fmt.Errorf("%w: expected '%s', got '%s'", - serrors.ErrorInvalidRecipe, builderID, recipeType) + serrors.ErrorInvalidRecipe, builderID.String(), recipeType) case "v0.3": // In this version, two recipe types are allowed, depending how the @@ -270,10 +261,10 @@ func validateRecipeType(builderID, recipeType string) error { // VerifyBuilder verifies the builder in the DSSE payload: // - in the recipe type // - the recipe argument type -// - the predicate builder ID -func (self *Provenance) VerifyBuilder(builderOpts *options.BuilderOpts) (string, error) { +// - the predicate builder ID. +func (self *Provenance) VerifyBuilder(builderOpts *options.BuilderOpts) (*utils.BuilderID, error) { if err := self.isVerified(); err != nil { - return "", err + return nil, err } statement := self.verifiedIntotoStatement @@ -281,39 +272,43 @@ func (self *Provenance) VerifyBuilder(builderOpts *options.BuilderOpts) (string, // Sanity check the builderID. if err := isValidBuilderID(predicateBuilderID); err != nil { - return "", err + return nil, err + } + + provBuilderID, err := utils.BuilderIDNew(predicateBuilderID) + if err != nil { + return nil, err } // Validate with user-provided value. if builderOpts != nil && builderOpts.ExpectedID != nil { - if *builderOpts.ExpectedID != predicateBuilderID { - return "", fmt.Errorf("%w: expected '%s', got '%s'", serrors.ErrorMismatchBuilderID, - *builderOpts.ExpectedID, predicateBuilderID) + if err := provBuilderID.Matches(*builderOpts.ExpectedID); err != nil { + return nil, err } } // Valiate the recipe type. - if err := validateRecipeType(predicateBuilderID, statement.Predicate.Recipe.Type); err != nil { - return "", err + if err := validateRecipeType(*provBuilderID, statement.Predicate.Recipe.Type); err != nil { + return nil, err } // Validate the recipe argument type. expectedType := "type.googleapis.com/google.devtools.cloudbuild.v1.Build" args, ok := statement.Predicate.Recipe.Arguments.(map[string]interface{}) if !ok { - return "", fmt.Errorf("%w: recipe arguments is not a map", serrors.ErrorInvalidDssePayload) + return nil, fmt.Errorf("%w: recipe arguments is not a map", serrors.ErrorInvalidDssePayload) } ts, err := getAsString(args, "@type") if err != nil { - return "", err + return nil, err } if ts != expectedType { - return "", fmt.Errorf("%w: expected '%s', got '%s'", serrors.ErrorMismatchBuilderID, + return nil, fmt.Errorf("%w: expected '%s', got '%s'", serrors.ErrorMismatchBuilderID, expectedType, ts) } - return predicateBuilderID, nil + return provBuilderID, nil } func getAsString(m map[string]interface{}, key string) (string, error) { @@ -351,7 +346,7 @@ func (self *Provenance) VerifySubjectDigest(expectedHash string) error { } // Verify source URI in provenance statement. -func (self *Provenance) VerifySourceURI(expectedSourceURI, builderID string) error { +func (self *Provenance) VerifySourceURI(expectedSourceURI string, builderID utils.BuilderID) error { if err := self.isVerified(); err != nil { return err } @@ -366,11 +361,8 @@ func (self *Provenance) VerifySourceURI(expectedSourceURI, builderID string) err expectedSourceURI = "https://" + expectedSourceURI } - v, err := getBuilderVersion(builderID) - if err != nil { - return err - } - + var err error + v := builderID.Version() switch v { case "v0.2": // In v0.2, it uses format diff --git a/verifiers/internal/gcb/provenance_test.go b/verifiers/internal/gcb/provenance_test.go index a6095249c..993766d53 100644 --- a/verifiers/internal/gcb/provenance_test.go +++ b/verifiers/internal/gcb/provenance_test.go @@ -7,13 +7,12 @@ import ( "strings" "testing" - //"time" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" serrors "github.com/slsa-framework/slsa-verifier/errors" "github.com/slsa-framework/slsa-verifier/options" + "github.com/slsa-framework/slsa-verifier/verifiers/utils" ) // This function sets the statement of the proveannce, as if @@ -158,6 +157,39 @@ func Test_VerifyBuilder(t *testing.T) { builderID: "https://cloudbuild.googleapis.com/GoogleHostedWorker@v0.3", expected: serrors.ErrorInvalidRecipe, }, + { + name: "v0.2 valid builder - name only", + path: "./testdata/gcloud-container-github.json", + builderID: "https://cloudbuild.googleapis.com/GoogleHostedWorker", + }, + { + name: "v0.2 mismatch builder - name only", + path: "./testdata/gcloud-container-github.json", + builderID: "https://cloudbuild.googleapis.com/GoogleHostedWorke", + expected: serrors.ErrorMismatchBuilderID, + }, + { + name: "v0.3 valid builder CloudBuildSteps - name only", + path: "./testdata/gcloud-container-invalid-recipetypestepsv03.json", + builderID: "https://cloudbuild.googleapis.com/GoogleHostedWorker", + }, + { + name: "v0.3 valid builder CloudBuildYaml - name only", + path: "./testdata/gcloud-container-invalid-recipetypecloudv03.json", + builderID: "https://cloudbuild.googleapis.com/GoogleHostedWorker", + }, + { + name: "v0.3 mismatch builder CloudBuildSteps - name only", + path: "./testdata/gcloud-container-invalid-recipetypestepsv03.json", + builderID: "https://cloudbuild.googleapis.com/GoogleHostedWorke", + expected: serrors.ErrorMismatchBuilderID, + }, + { + name: "v0.3 mismatch CloudBuildYaml - name only", + path: "./testdata/gcloud-container-invalid-recipetypecloudv03.json", + builderID: "https://cloudbuild.googleapis.com/GoogleHostedWorke", + expected: serrors.ErrorMismatchBuilderID, + }, } for _, tt := range tests { tt := tt // Re-initializing variable so it is not changed while executing the closure below @@ -191,8 +223,12 @@ func Test_VerifyBuilder(t *testing.T) { return } - if outBuilderID != tt.builderID { - t.Errorf(cmp.Diff(outBuilderID, tt.builderID)) + if outBuilderID == nil { + panic("outBuilderID is nil") + } + + if err := outBuilderID.Matches(tt.builderID); err != nil { + t.Errorf(fmt.Sprintf("matches failed: %v", err)) } }) } @@ -254,7 +290,11 @@ func Test_validateRecipeType(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - err := validateRecipeType(tt.builderID, tt.recipeType) + builderID, err := utils.BuilderIDNew(tt.builderID) + if err != nil { + panic(fmt.Errorf("BuilderIDNew: %w", err)) + } + err = validateRecipeType(*builderID, tt.recipeType) if !cmp.Equal(err, tt.expected, cmpopts.EquateErrors()) { t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors())) } @@ -376,7 +416,11 @@ func Test_VerifySourceURI(t *testing.T) { panic(fmt.Errorf("ProvenanceFromBytes: %w", err)) } - err = prov.VerifySourceURI(tt.source, tt.builderID) + builderID, err := utils.BuilderIDNew(tt.builderID) + if err != nil { + panic(fmt.Errorf("BuilderIDNew: %w", err)) + } + err = prov.VerifySourceURI(tt.source, *builderID) if !cmp.Equal(err, tt.expected, cmpopts.EquateErrors()) { t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors())) } diff --git a/verifiers/internal/gcb/verifier.go b/verifiers/internal/gcb/verifier.go index 083236287..fbe727d0c 100644 --- a/verifiers/internal/gcb/verifier.go +++ b/verifiers/internal/gcb/verifier.go @@ -2,12 +2,12 @@ package gcb import ( "context" - "strings" serrors "github.com/slsa-framework/slsa-verifier/errors" "github.com/slsa-framework/slsa-verifier/options" register "github.com/slsa-framework/slsa-verifier/register" _ "github.com/slsa-framework/slsa-verifier/verifiers/internal/gcb/keys" + "github.com/slsa-framework/slsa-verifier/verifiers/utils" ) const VerifierName = "GCB" @@ -25,9 +25,9 @@ func GCBVerifierNew() *GCBVerifier { // IsAuthoritativeFor returns true of the verifier can verify provenance // generated by the builderID. -func (v *GCBVerifier) IsAuthoritativeFor(builderID string) bool { +func (v *GCBVerifier) IsAuthoritativeFor(builderIDName string) bool { // This verifier only supports the GCB builders. - return strings.HasPrefix(builderID, "https://cloudbuild.googleapis.com/GoogleHostedWorker@") + return builderIDName == "https://cloudbuild.googleapis.com/GoogleHostedWorker" } // VerifyArtifact verifies provenance for an artifact. @@ -35,8 +35,8 @@ func (v *GCBVerifier) VerifyArtifact(ctx context.Context, provenance []byte, artifactHash string, provenanceOpts *options.ProvenanceOpts, builderOpts *options.BuilderOpts, -) ([]byte, string, error) { - return nil, "todo", serrors.ErrorNotSupported +) ([]byte, *utils.BuilderID, error) { + return nil, nil, serrors.ErrorNotSupported } // VerifyImage verifies provenance for an OCI image. @@ -44,81 +44,81 @@ func (v *GCBVerifier) VerifyImage(ctx context.Context, provenance []byte, artifactImage string, provenanceOpts *options.ProvenanceOpts, builderOpts *options.BuilderOpts, -) ([]byte, string, error) { +) ([]byte, *utils.BuilderID, error) { prov, err := ProvenanceFromBytes(provenance) if err != nil { - return nil, "", err + return nil, nil, err } // Verify signature on the intoto attestation. if err = prov.VerifySignature(); err != nil { - return nil, "", err + return nil, nil, err } // Verify intoto header. if err = prov.VerifyIntotoHeaders(); err != nil { - return nil, "", err + return nil, nil, err } // Verify the builder. builderID, err := prov.VerifyBuilder(builderOpts) if err != nil { - return nil, "", err + return nil, nil, err } // Verify subject digest. if err = prov.VerifySubjectDigest(provenanceOpts.ExpectedDigest); err != nil { - return nil, "", err + return nil, nil, err } // Verify source. - if err = prov.VerifySourceURI(provenanceOpts.ExpectedSourceURI, builderID); err != nil { - return nil, "", err + if err = prov.VerifySourceURI(provenanceOpts.ExpectedSourceURI, *builderID); err != nil { + return nil, nil, err } // Verify metadata. // This is metadata that GCB appends to the DSSE content. if err = prov.VerifyMetadata(provenanceOpts); err != nil { - return nil, "", err + return nil, nil, err } // Verify the summary. // This is an additional structure that GCB prepends to the provenance. if err = prov.VerifySummary(provenanceOpts); err != nil { - return nil, "", err + return nil, nil, err } // Verify the text provenance. // This is an additional structure that GCB prepends to the provenance, // intended for humans. It reflect the DSSE payload. if err = prov.VerifyTextProvenance(); err != nil { - return nil, "", err + return nil, nil, err } // Verify branch. if provenanceOpts.ExpectedBranch != nil { if err = prov.VerifyBranch(*provenanceOpts.ExpectedBranch); err != nil { - return nil, "", err + return nil, nil, err } } // Verify the tag. if provenanceOpts.ExpectedTag != nil { if err := prov.VerifyTag(*provenanceOpts.ExpectedTag); err != nil { - return nil, "", err + return nil, nil, err } } // Verify the versioned tag. if provenanceOpts.ExpectedVersionedTag != nil { if err := prov.VerifyVersionedTag(*provenanceOpts.ExpectedVersionedTag); err != nil { - return nil, "", err + return nil, nil, err } } content, err := prov.GetVerifiedIntotoStatement() if err != nil { - return nil, "", err + return nil, nil, err } return content, builderID, nil } diff --git a/verifiers/internal/gha/verifier.go b/verifiers/internal/gha/verifier.go index e3a7896f8..4fc87a358 100644 --- a/verifiers/internal/gha/verifier.go +++ b/verifiers/internal/gha/verifier.go @@ -15,7 +15,8 @@ import ( "github.com/slsa-framework/slsa-verifier/options" "github.com/slsa-framework/slsa-verifier/register" - "github.com/slsa-framework/slsa-verifier/verifiers/container" + "github.com/slsa-framework/slsa-verifier/verifiers/utils" + "github.com/slsa-framework/slsa-verifier/verifiers/utils/container" ) const VerifierName = "GHA" @@ -43,26 +44,26 @@ func verifyEnvAndCert(env *dsse.Envelope, provenanceOpts *options.ProvenanceOpts, builderOpts *options.BuilderOpts, defaultBuilders map[string]bool, -) ([]byte, string, error) { +) ([]byte, *utils.BuilderID, error) { /* Verify properties of the signing identity. */ // Get the workflow info given the certificate information. workflowInfo, err := GetWorkflowInfoFromCertificate(cert) if err != nil { - return nil, "", err + return nil, nil, err } // Verify the workflow identity. builderID, err := VerifyWorkflowIdentity(workflowInfo, builderOpts, provenanceOpts.ExpectedSourceURI, defaultBuilders) if err != nil { - return nil, "", err + return nil, nil, err } // Verify properties of the SLSA provenance. // Unpack and verify info in the provenance, including the Subject Digest. provenanceOpts.ExpectedBuilderID = builderID if err := VerifyProvenance(env, provenanceOpts); err != nil { - return nil, "", err + return nil, nil, err } fmt.Fprintf(os.Stderr, "Verified build using builder https://github.com%s at commit %s\n", @@ -70,7 +71,17 @@ func verifyEnvAndCert(env *dsse.Envelope, workflowInfo.CallerHash) // Return verified provenance. r, err := base64.StdEncoding.DecodeString(env.Payload) - return r, builderID, err + if err != nil { + return nil, nil, err + } + + // Temporary code. + // TODO: remove `SetName` and `SetVersion` function once GHA is supported, + // and use: bid, err := utils.BuilderIDNew(builderID) + bid := &utils.BuilderID{} + bid.SetName(builderID) + bid.SetVersion(strings.Split(workflowInfo.JobWobWorkflowRef, "@")[1]) + return r, bid, nil } // VerifyArtifact verifies provenance for an artifact. @@ -78,16 +89,16 @@ func (v *GHAVerifier) VerifyArtifact(ctx context.Context, provenance []byte, artifactHash string, provenanceOpts *options.ProvenanceOpts, builderOpts *options.BuilderOpts, -) ([]byte, string, error) { +) ([]byte, *utils.BuilderID, error) { rClient, err := rekor.NewClient(defaultRekorAddr) if err != nil { - return nil, "", err + return nil, nil, err } /* Verify signature on the intoto attestation. */ env, cert, err := VerifyProvenanceSignature(ctx, rClient, provenance, artifactHash) if err != nil { - return nil, "", err + return nil, nil, err } return verifyEnvAndCert(env, cert, @@ -100,11 +111,11 @@ func (v *GHAVerifier) VerifyImage(ctx context.Context, provenance []byte, artifactImage string, provenanceOpts *options.ProvenanceOpts, builderOpts *options.BuilderOpts, -) ([]byte, string, error) { +) ([]byte, *utils.BuilderID, error) { /* Retrieve any valid signed attestations that chain up to Fulcio root CA. */ roots, err := fulcio.GetRoots() if err != nil { - return nil, "", err + return nil, nil, err } opts := &cosign.CheckOpts{ RootCerts: roots, @@ -113,12 +124,12 @@ func (v *GHAVerifier) VerifyImage(ctx context.Context, atts, _, err := container.RunCosignImageVerification(ctx, artifactImage, opts) if err != nil { - return nil, "", err + return nil, nil, err } /* Now verify properties of the attestations */ var verifyErr error - var builderID string + var builderID *utils.BuilderID var verifiedProvenance []byte for _, att := range atts { pyld, err := att.Payload() @@ -144,5 +155,5 @@ func (v *GHAVerifier) VerifyImage(ctx context.Context, } } - return nil, "", fmt.Errorf("no valid attestations found on OCI registry: %w", verifyErr) + return nil, nil, fmt.Errorf("no valid attestations found on OCI registry: %w", verifyErr) } diff --git a/verifiers/utils/builder.go b/verifiers/utils/builder.go new file mode 100644 index 000000000..182cb575b --- /dev/null +++ b/verifiers/utils/builder.go @@ -0,0 +1,86 @@ +package utils + +import ( + "fmt" + "strings" + + serrors "github.com/slsa-framework/slsa-verifier/errors" +) + +type BuilderID struct { + name, version string +} + +// BuilderIDNew creates a new BuilderID structure. +func BuilderIDNew(builderID string) (*BuilderID, error) { + name, version, err := ParseBuilderID(builderID, true) + if err != nil { + return nil, err + } + + return &BuilderID{ + name: name, + version: version, + }, nil +} + +// Matches matches the builderID string against the reference builderID. +// If the builderID contains a semver, the full builderID must match. +// Otherwise, only the name needs to match. +func (b *BuilderID) Matches(builderID string) error { + name, version, err := ParseBuilderID(builderID, false) + if err != nil { + return err + } + + if name != b.name { + return fmt.Errorf("%w: expected name '%s', got '%s'", serrors.ErrorMismatchBuilderID, + name, b.name) + } + + if version != "" && version != b.version { + return fmt.Errorf("%w: expected version '%s', got '%s'", serrors.ErrorMismatchBuilderID, + version, b.version) + } + + return nil +} + +func (b *BuilderID) Name() string { + return b.name +} + +func (b *BuilderID) Version() string { + return b.version +} + +func (b *BuilderID) String() string { + return fmt.Sprintf("%s@%s", b.name, b.version) +} + +// TODO: remove this function once GHA is supported. +func (b *BuilderID) SetName(name string) { + b.name = name +} + +func (b *BuilderID) SetVersion(version string) { + b.version = version +} + +func ParseBuilderID(id string, needVersion bool) (string, string, error) { + parts := strings.Split(id, "@") + if len(parts) == 2 { + if parts[1] == "" { + return "", "", fmt.Errorf("%w: builderID: '%s'", + serrors.ErrorInvalidFormat, id) + } + return parts[0], parts[1], nil + } + + if len(parts) == 1 && !needVersion { + return parts[0], "", nil + } + + return "", "", fmt.Errorf("%w: builderID: '%s'", + serrors.ErrorInvalidFormat, id) +} diff --git a/verifiers/utils/builder_test.go b/verifiers/utils/builder_test.go new file mode 100644 index 000000000..23f1d4932 --- /dev/null +++ b/verifiers/utils/builder_test.go @@ -0,0 +1,221 @@ +package utils + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + serrors "github.com/slsa-framework/slsa-verifier/errors" +) + +func Test_ParseBuilderID(t *testing.T) { + t.Parallel() + tests := []struct { + name string + builderID string + needVersion bool + builderName string + builderVersion string + err error + }{ + { + name: "valid builder with version - need version", + builderID: "some/name@v1.2.3", + needVersion: true, + builderName: "some/name", + builderVersion: "v1.2.3", + }, + { + name: "valid builder with version - no need version", + builderID: "some/name@v1.2.3", + builderName: "some/name", + builderVersion: "v1.2.3", + }, + { + name: "valid builder without version - no need version", + builderID: "some/name", + builderName: "some/name", + }, + { + name: "no version ID - need version", + needVersion: true, + err: serrors.ErrorInvalidFormat, + }, + { + name: "too many '@' - need version", + builderID: "some/name@vla@blo", + needVersion: true, + err: serrors.ErrorInvalidFormat, + }, + { + name: "too many '@' - no need version", + builderID: "some/name@vla@blo", + err: serrors.ErrorInvalidFormat, + }, + { + name: "empty version - need version", + builderID: "some/name@", + needVersion: true, + err: serrors.ErrorInvalidFormat, + }, + { + name: "empty version - no need version", + builderID: "some/name@", + err: serrors.ErrorInvalidFormat, + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + name, version, err := ParseBuilderID(tt.builderID, tt.needVersion) + if !cmp.Equal(err, tt.err, cmpopts.EquateErrors()) { + t.Errorf(cmp.Diff(err, tt.err)) + } + + if err != nil { + return + } + + if name != tt.builderName { + t.Errorf(cmp.Diff(name, tt.builderName)) + } + + if version != tt.builderVersion { + t.Errorf(cmp.Diff(version, tt.builderVersion)) + } + }) + } +} + +func Test_BuilderIDNew(t *testing.T) { + t.Parallel() + tests := []struct { + name string + builderID string + builderName string + builderVersion string + err error + }{ + { + name: "valid", + builderID: "some/name@v1.2.3", + builderName: "some/name", + builderVersion: "v1.2.3", + }, + { + name: "empty version", + builderID: "some/name@", + err: serrors.ErrorInvalidFormat, + }, + { + name: "too many '@' - need version", + builderID: "some/name@vla@blo", + err: serrors.ErrorInvalidFormat, + }, + { + name: "too many '@' - no need version", + builderID: "some/name@vla@blo", + err: serrors.ErrorInvalidFormat, + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + builderID, err := BuilderIDNew(tt.builderID) + if !cmp.Equal(err, tt.err, cmpopts.EquateErrors()) { + t.Errorf(cmp.Diff(err, tt.err)) + } + + if err != nil { + return + } + + name := builderID.Name() + version := builderID.Version() + full := builderID.String() + + if name != tt.builderName { + t.Errorf(cmp.Diff(tt.builderName, name)) + } + if version != tt.builderVersion { + t.Errorf(cmp.Diff(tt.builderVersion, version)) + } + if full != tt.builderID { + t.Errorf(cmp.Diff(tt.builderID, full)) + } + }) + } +} + +func Test_Matches(t *testing.T) { + t.Parallel() + tests := []struct { + name string + builderID string + match string + err error + }{ + { + name: "match full", + builderID: "some/name@v1.2.3", + match: "some/name@v1.2.3", + }, + { + name: "match name", + builderID: "some/name@v1.2.3", + match: "some/name", + }, + { + name: "mismatch name", + builderID: "some/name@v1.2.3", + match: "some/name2", + err: serrors.ErrorMismatchBuilderID, + }, + { + name: "mismatch version", + builderID: "some/name@v1.2.3", + match: "some/name@v1.2.4", + err: serrors.ErrorMismatchBuilderID, + }, + { + name: "invalid empty version", + builderID: "some/name@v1.2.3", + match: "some/name@", + err: serrors.ErrorInvalidFormat, + }, + { + name: "too many '@' - need version", + builderID: "some/name@v1.2.3", + match: "some/name@vla@blo", + err: serrors.ErrorInvalidFormat, + }, + { + name: "too many '@' - no need version", + builderID: "some/name@v1.2.3", + match: "some/name@vla@blo", + err: serrors.ErrorInvalidFormat, + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + builderID, err := BuilderIDNew(tt.builderID) + if err != nil { + panic(fmt.Errorf("BuilderIDNew: %w", err)) + } + + err = builderID.Matches(tt.match) + if !cmp.Equal(err, tt.err, cmpopts.EquateErrors()) { + t.Errorf(cmp.Diff(err, tt.err)) + } + }) + } +} diff --git a/verifiers/container/container.go b/verifiers/utils/container/container.go similarity index 100% rename from verifiers/container/container.go rename to verifiers/utils/container/container.go diff --git a/verifiers/container/cosign.go b/verifiers/utils/container/cosign.go similarity index 100% rename from verifiers/container/cosign.go rename to verifiers/utils/container/cosign.go diff --git a/verifiers/verifier.go b/verifiers/verifier.go index 77706793b..88a8b327b 100644 --- a/verifiers/verifier.go +++ b/verifiers/verifier.go @@ -9,6 +9,7 @@ import ( "github.com/slsa-framework/slsa-verifier/register" _ "github.com/slsa-framework/slsa-verifier/verifiers/internal/gcb" "github.com/slsa-framework/slsa-verifier/verifiers/internal/gha" + "github.com/slsa-framework/slsa-verifier/verifiers/utils" ) func getVerifier(builderOpts *options.BuilderOpts) (register.SLSAVerifier, error) { @@ -18,8 +19,12 @@ func getVerifier(builderOpts *options.BuilderOpts) (register.SLSAVerifier, error // If user provids a builderID, find the right verifier based on its ID. if builderOpts.ExpectedID != nil && *builderOpts.ExpectedID != "" { + name, _, err := utils.ParseBuilderID(*builderOpts.ExpectedID, false) + if err != nil { + return nil, err + } for _, v := range register.SLSAVerifiers { - if v.IsAuthoritativeFor(*builderOpts.ExpectedID) { + if v.IsAuthoritativeFor(name) { return v, nil } } @@ -34,10 +39,10 @@ func VerifyImage(ctx context.Context, artifactImage string, provenance []byte, provenanceOpts *options.ProvenanceOpts, builderOpts *options.BuilderOpts, -) ([]byte, string, error) { +) ([]byte, *utils.BuilderID, error) { verifier, err := getVerifier(builderOpts) if err != nil { - return nil, "", err + return nil, nil, err } return verifier.VerifyImage(ctx, provenance, artifactImage, provenanceOpts, builderOpts) @@ -47,10 +52,10 @@ func VerifyArtifact(ctx context.Context, provenance []byte, artifactHash string, provenanceOpts *options.ProvenanceOpts, builderOpts *options.BuilderOpts, -) ([]byte, string, error) { +) ([]byte, *utils.BuilderID, error) { verifier, err := getVerifier(builderOpts) if err != nil { - return nil, "", err + return nil, nil, err } return verifier.VerifyArtifact(ctx, provenance, artifactHash,