diff --git a/cli/slsa-verifier/main.go b/cli/slsa-verifier/main.go index 3243d45..446670c 100644 --- a/cli/slsa-verifier/main.go +++ b/cli/slsa-verifier/main.go @@ -10,11 +10,13 @@ import ( "log" "os" - "github.com/slsa-framework/slsa-verifier/verification" + "github.com/slsa-framework/slsa-verifier/options" + "github.com/slsa-framework/slsa-verifier/verifiers" ) var ( provenancePath string + builderID string artifactPath string source string branch string @@ -24,6 +26,7 @@ var ( ) func main() { + flag.StringVar(&builderID, "builder-id", "", "EXPERIMENTAL: the unique builder ID who created the provenance") flag.StringVar(&provenancePath, "provenance", "", "path to a provenance file") flag.StringVar(&artifactPath, "artifact-path", "", "path to an artifact to verify") flag.StringVar(&source, "source", "", @@ -41,21 +44,26 @@ func main() { os.Exit(1) } - var ptag, pversiontag *string + var pbuilderID, ptag, pversiontag *string + // Note: nil tag, version-tag and builder-id means we ignore them during verification. if isFlagPassed("tag") { ptag = &tag } if isFlagPassed("versioned-tag") { pversiontag = &versiontag } + if isFlagPassed("builder-id") { + pbuilderID = &builderID + } if ptag != nil && pversiontag != nil { fmt.Fprintf(os.Stderr, "'version' and 'tag' options cannot be used together\n") os.Exit(1) } - verifiedProvenance, err := runVerify(artifactPath, provenancePath, source, branch, ptag, pversiontag) + verifiedProvenance, _, err := runVerify(artifactPath, provenancePath, source, + branch, pbuilderID, ptag, pversiontag) if err != nil { fmt.Fprintf(os.Stderr, "FAILED: SLSA verification failed: %v\n", err) os.Exit(2) @@ -78,7 +86,9 @@ func isFlagPassed(name string) bool { return found } -func runVerify(artifactPath, provenancePath, source, branch string, ptag, pversiontag *string) ([]byte, error) { +func runVerify(artifactPath, provenancePath, source, branch string, + builderID, ptag, pversiontag *string, +) ([]byte, string, error) { f, err := os.Open(artifactPath) if err != nil { log.Fatal(err) @@ -87,7 +97,7 @@ func runVerify(artifactPath, provenancePath, source, branch string, ptag, pversi provenance, err := os.ReadFile(provenancePath) if err != nil { - return nil, err + return nil, "", err } h := sha256.New() @@ -96,15 +106,19 @@ func runVerify(artifactPath, provenancePath, source, branch string, ptag, pversi } artifactHash := hex.EncodeToString(h.Sum(nil)) - provenanceOpts := &verification.ProvenanceOpts{ + provenanceOpts := &options.ProvenanceOpts{ + ExpectedSourceURI: source, ExpectedBranch: branch, ExpectedDigest: artifactHash, ExpectedVersionedTag: pversiontag, ExpectedTag: ptag, } + builderOpts := &options.BuilderOpts{ + ExpectedID: builderID, + } + ctx := context.Background() - return verification.Verify(ctx, provenance, - artifactHash, - source, provenanceOpts) + return verifiers.Verify(ctx, provenance, + artifactHash, provenanceOpts, builderOpts) } diff --git a/cli/slsa-verifier/main_test.go b/cli/slsa-verifier/main_test.go index f522697..b320605 100644 --- a/cli/slsa-verifier/main_test.go +++ b/cli/slsa-verifier/main_test.go @@ -9,10 +9,10 @@ import ( "golang.org/x/mod/semver" - "github.com/slsa-framework/slsa-verifier/verification" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + + serrors "github.com/slsa-framework/slsa-verifier/errors" ) func errCmp(e1, e2 error) bool { @@ -42,6 +42,8 @@ func Test_runVerify(t *testing.T) { branch string ptag *string pversiontag *string + pbuilderID *string + builderID string err error // noversion is a special case where we are not testing all builder versions // for example, testdata for the builder at head in trusted repo workflows @@ -58,6 +60,18 @@ func Test_runVerify(t *testing.T) { artifact: "binary-linux-amd64-workflow_dispatch", source: "github.com/slsa-framework/example-package", }, + { + name: "valid main branch default", + artifact: "binary-linux-amd64-workflow_dispatch", + source: "github.com/slsa-framework/example-package", + }, + { + name: "valid main branch default - invalid builderID", + artifact: "binary-linux-amd64-workflow_dispatch", + source: "github.com/slsa-framework/example-package", + pbuilderID: pString("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/not-trusted.yml"), + err: serrors.ErrorUntrustedReusableWorkflow, + }, { name: "valid main branch set", artifact: "binary-linux-amd64-workflow_dispatch", @@ -69,39 +83,39 @@ func Test_runVerify(t *testing.T) { artifact: "binary-linux-amd64-workflow_dispatch", source: "github.com/slsa-framework/example-package", branch: "master", - err: verification.ErrorMismatchBranch, + err: serrors.ErrorMismatchBranch, }, { name: "wrong source append A", artifact: "binary-linux-amd64-workflow_dispatch", source: "github.com/laurentsimon/slsa-verifier-test-genA", - err: verification.ErrorMismatchRepository, + err: serrors.ErrorMismatchSource, }, { name: "wrong source prepend A", artifact: "binary-linux-amd64-workflow_dispatch", source: "github.com/laurentsimon/slsa-verifier-test-gen", - err: verification.ErrorMismatchRepository, + err: serrors.ErrorMismatchSource, }, { name: "wrong source middle A", artifact: "binary-linux-amd64-workflow_dispatch", source: "github.com/Alaurentsimon/slsa-verifier-test-gen", - err: verification.ErrorMismatchRepository, + err: serrors.ErrorMismatchSource, }, { name: "tag no match empty tag workflow_dispatch", artifact: "binary-linux-amd64-workflow_dispatch", source: "github.com/slsa-framework/example-package", ptag: pString("v1.2.3"), - err: verification.ErrorMismatchTag, + err: serrors.ErrorMismatchTag, }, { name: "versioned tag no match empty tag workflow_dispatch", artifact: "binary-linux-amd64-workflow_dispatch", source: "github.com/slsa-framework/example-package", pversiontag: pString("v1"), - err: verification.ErrorInvalidSemver, + err: serrors.ErrorInvalidSemver, }, // Provenance contains tag = v13.0.30. { @@ -109,21 +123,21 @@ func Test_runVerify(t *testing.T) { artifact: "binary-linux-amd64-push-v13.0.30", source: "github.com/slsa-framework/example-package", ptag: pString("v13.0.29"), - err: verification.ErrorMismatchTag, + err: serrors.ErrorMismatchTag, }, { name: "tag v13.0 no match v13.0.30", artifact: "binary-linux-amd64-push-v13.0.30", source: "github.com/slsa-framework/example-package", ptag: pString("v13.0"), - err: verification.ErrorMismatchTag, + err: serrors.ErrorMismatchTag, }, { name: "tag v13 no match v13.0.30", artifact: "binary-linux-amd64-push-v13.0.30", source: "github.com/slsa-framework/example-package", ptag: pString("v13"), - err: verification.ErrorMismatchTag, + err: serrors.ErrorMismatchTag, }, { name: "versioned v13.0.30 match push-v13.0.30", @@ -148,42 +162,42 @@ func Test_runVerify(t *testing.T) { artifact: "binary-linux-amd64-push-v13.0.30", source: "github.com/slsa-framework/example-package", pversiontag: pString("v2"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, { name: "versioned v0 no match push-v13.0.30", artifact: "binary-linux-amd64-push-v13.0.30", source: "github.com/slsa-framework/example-package", pversiontag: pString("v0"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, { name: "versioned v13.1 no match push-v13.0.30", artifact: "binary-linux-amd64-push-v13.0.30", source: "github.com/slsa-framework/example-package", pversiontag: pString("v13.1"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, { name: "versioned v12.9 no match push-v13.0.30", artifact: "binary-linux-amd64-push-v13.0.30", source: "github.com/slsa-framework/example-package", pversiontag: pString("v12.9"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, { name: "versioned v13.0.29 no match push-v13.0.30", artifact: "binary-linux-amd64-push-v13.0.30", source: "github.com/slsa-framework/example-package", pversiontag: pString("v13.0.29"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, { name: "versioned v13.0.31 no match push-v13.0.30", artifact: "binary-linux-amd64-push-v13.0.30", source: "github.com/slsa-framework/example-package", pversiontag: pString("v13.0.31"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, // Provenance contains tag = v14. { @@ -203,42 +217,42 @@ func Test_runVerify(t *testing.T) { artifact: "binary-linux-amd64-push-v14", source: "github.com/slsa-framework/example-package", pversiontag: pString("v14.1"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, { name: "versioned v13 no match push-v14", artifact: "binary-linux-amd64-push-v14", source: "github.com/slsa-framework/example-package", pversiontag: pString("v13"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, { name: "versioned v15 no match push-v14", artifact: "binary-linux-amd64-push-v14", source: "github.com/slsa-framework/example-package", pversiontag: pString("v15"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, { name: "versioned v13.2 no match push-v14", artifact: "binary-linux-amd64-push-v14", source: "github.com/slsa-framework/example-package", pversiontag: pString("v13.2"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, { name: "versioned v15 no match push-v14", artifact: "binary-linux-amd64-push-v14", source: "github.com/slsa-framework/example-package", pversiontag: pString("v15"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, { name: "versioned v0 no match push-v14", artifact: "binary-linux-amd64-push-v14", source: "github.com/slsa-framework/example-package", pversiontag: pString("v0"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, // Provenance contains tag = v14.2 { @@ -252,14 +266,14 @@ func Test_runVerify(t *testing.T) { artifact: "binary-linux-amd64-push-v14.2", source: "github.com/slsa-framework/example-package", pversiontag: pString("v14.2.1"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, { name: "versioned v14.2.3 match push-v14.2", artifact: "binary-linux-amd64-push-v14.2", source: "github.com/slsa-framework/example-package", pversiontag: pString("v14.2.3"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, { name: "versioned v14 match push-v14.2", @@ -272,42 +286,42 @@ func Test_runVerify(t *testing.T) { artifact: "binary-linux-amd64-push-v14.2", source: "github.com/slsa-framework/example-package", pversiontag: pString("v14.1"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, { name: "versioned v14.1.1 no match push-v14.2", artifact: "binary-linux-amd64-push-v14.2", source: "github.com/slsa-framework/example-package", pversiontag: pString("v14.1.1"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, { name: "versioned v14.3.1 no match push-v14.2", artifact: "binary-linux-amd64-push-v14.2", source: "github.com/slsa-framework/example-package", pversiontag: pString("v14.3.1"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, { name: "versioned v13 no match push-v14.2", artifact: "binary-linux-amd64-push-v14.2", source: "github.com/slsa-framework/example-package", pversiontag: pString("v13"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, { name: "versioned v15 no match push-v14.2", artifact: "binary-linux-amd64-push-v14.2", source: "github.com/slsa-framework/example-package", pversiontag: pString("v15"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, { name: "versioned v15.1 no match push-v14.2", artifact: "binary-linux-amd64-push-v14.2", source: "github.com/slsa-framework/example-package", pversiontag: pString("v15.1"), - err: verification.ErrorMismatchVersionedTag, + err: serrors.ErrorMismatchVersionedTag, }, // Multiple subjects in version v1.2.0+ { @@ -324,6 +338,15 @@ func Test_runVerify(t *testing.T) { minversion: "v1.2.0", builders: []string{"generic"}, }, + { + name: "multiple subject second match - builderID", + artifact: "binary-linux-amd64-multi-subject-second", + source: "github.com/slsa-framework/example-package", + minversion: "v1.2.0", + builders: []string{"generic"}, + pbuilderID: pString("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml"), + builderID: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml", + }, // Special case of the e2e test repository building builder from head. { name: "e2e test repository verified with builder at head", @@ -331,27 +354,28 @@ func Test_runVerify(t *testing.T) { source: "github.com/slsa-framework/example-package", branch: "main", noversion: true, + builderID: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_go_slsa3.yml", }, // Malicious builders and workflows. { name: "rekor upload bypassed", artifact: "binary-linux-amd64-no-tlog-upload", source: "github.com/slsa-framework/example-package", - err: verification.ErrorNoValidRekorEntries, + err: serrors.ErrorNoValidRekorEntries, noversion: true, }, { name: "malicious: untrusted builder", artifact: "binary-linux-amd64-untrusted-builder", source: "github.com/slsa-framework/example-package", - err: verification.ErrorUntrustedReusableWorkflow, + err: serrors.ErrorUntrustedReusableWorkflow, noversion: true, }, { name: "malicious: invalid signature expired certificate", artifact: "binary-linux-amd64-expired-cert", source: "github.com/slsa-framework/example-package", - err: verification.ErrorNoValidRekorEntries, + err: serrors.ErrorNoValidRekorEntries, noversion: true, }, // Regression test of sharded UUID @@ -413,14 +437,22 @@ func Test_runVerify(t *testing.T) { artifactPath := filepath.Clean(filepath.Join(TEST_DIR, v, tt.artifact)) provenancePath := fmt.Sprintf("%s.intoto.jsonl", artifactPath) - _, err := runVerify(artifactPath, + _, builderID, err := runVerify(artifactPath, provenancePath, - tt.source, branch, + tt.source, branch, tt.pbuilderID, tt.ptag, tt.pversiontag) if !errCmp(err, tt.err) { t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) } + + if err != nil { + return + } + + if tt.builderID != "" && builderID != tt.builderID { + t.Errorf(cmp.Diff(builderID, tt.builderID)) + } } }) } diff --git a/verification/errors.go b/errors/errors.go similarity index 60% rename from verification/errors.go rename to errors/errors.go index 362c2c8..7e6614c 100644 --- a/verification/errors.go +++ b/errors/errors.go @@ -5,14 +5,17 @@ import "errors" var ( ErrorInvalidDssePayload = errors.New("invalid DSSE envelope payload") ErrorMismatchBranch = errors.New("branch used to generate the binary does not match provenance") - ErrorMismatchRepository = errors.New("repository used to generate the binary does not match provenance") + ErrorMismatchBuilderID = errors.New("builderID does not match provenance") + ErrorMismatchSource = errors.New("source used to generate the binary does not match provenance") + ErrorMalformedURI = errors.New("URI is malformed") ErrorMismatchTag = errors.New("tag used to generate the binary does not match provenance") ErrorMismatchVersionedTag = errors.New("tag used to generate the binary does not match provenance") ErrorInvalidSemver = errors.New("invalid semantic version") ErrorRekorSearch = errors.New("error searching rekor entries") - errorMismatchHash = errors.New("binary artifact hash does not match provenance subject") - errorInvalidRef = errors.New("invalid ref") - errorMalformedWorkflowURI = errors.New("malformed URI for workflow") + ErrorMismatchHash = errors.New("binary artifact hash does not match provenance subject") + ErrorInvalidRef = errors.New("invalid ref") ErrorUntrustedReusableWorkflow = errors.New("untrusted reusable workflow") ErrorNoValidRekorEntries = errors.New("could not find a matching valid signature entry") + ErrorVerifierNotSupported = errors.New("no verifier support the builder") + ErrorNotSupported = errors.New("not supported") ) diff --git a/experimental/rest/service.go b/experimental/rest/service.go index c6b37ae..c43301a 100644 --- a/experimental/rest/service.go +++ b/experimental/rest/service.go @@ -9,7 +9,8 @@ import ( "io" "net/http" - "github.com/slsa-framework/slsa-verifier/verification" + "github.com/slsa-framework/slsa-verifier/options" + "github.com/slsa-framework/slsa-verifier/verifiers" ) var errInvalid = errors.New("invalid") @@ -20,6 +21,7 @@ type v1Query struct { ArtifactHash string `json:"artifactHash"` DsseEnvelope string `json:"provenanceContent"` // Optional fields. + BuilderID *string `json:"builderID"` Tag *string `json:"tag"` Branch *string `json:"branch"` VersionedTag *string `json:"versionedTag"` @@ -37,6 +39,7 @@ type v1Result struct { Version uint `json:"version"` Error *string `json:"error,omitempty"` Validation validation `json:"validation"` + BuilderID string `json:"builderID"` IntotoStatement *string `json:"provenanceContent,omitempty"` } @@ -81,6 +84,11 @@ func (r *v1Result) withValidation(v validation) *v1Result { return r } +func (r *v1Result) withBuilderID(id string) *v1Result { + r.BuilderID = id + return r +} + func (r *v1Result) withIntotoStatement(c []byte) *v1Result { b := base64.StdEncoding.EncodeToString(c) r.IntotoStatement = &b @@ -112,16 +120,21 @@ func verifyHandlerV1(r *http.Request) *v1Result { if query.Branch != nil { branch = *query.Branch } - provenanceOpts := &verification.ProvenanceOpts{ + provenanceOpts := &options.ProvenanceOpts{ + ExpectedSourceURI: query.Source, ExpectedBranch: branch, ExpectedDigest: query.ArtifactHash, ExpectedVersionedTag: query.VersionedTag, ExpectedTag: query.Tag, } + builderOpts := &options.BuilderOpts{ + ExpectedID: query.BuilderID, + } + ctx := context.Background() - p, err := verification.Verify(ctx, []byte(query.DsseEnvelope), - query.ArtifactHash, query.Source, provenanceOpts) + p, builderID, err := verifiers.Verify(ctx, []byte(query.DsseEnvelope), + query.ArtifactHash, provenanceOpts, builderOpts) if err != nil { return results.withError(err) } @@ -130,7 +143,7 @@ func verifyHandlerV1(r *http.Request) *v1Result { results = results.withIntotoStatement(p) } - return results.withValidation(validationSuccess) + return results.withBuilderID(builderID).withValidation(validationSuccess) } func queryFromString(content []byte) (*v1Query, error) { @@ -165,5 +178,7 @@ func (q *v1Query) validate() error { return fmt.Errorf("%w: tag and versionedTag are mutually exclusive", errInvalid) } + // BuilderID is optional, so not additional validation needed. + return nil } diff --git a/verification/options.go b/options/options.go similarity index 52% rename from verification/options.go rename to options/options.go index f9d7d2b..7bc7b37 100644 --- a/verification/options.go +++ b/options/options.go @@ -1,17 +1,29 @@ -package verification +package options // ProvenanceOpts are the options for checking provenance information. type ProvenanceOpts struct { - // ExpectedDigest is the expected artifact sha included in the provenance + // ExpectedDigest is the expected artifact sha included in the provenance. ExpectedDigest string // ExpectedBranch is the expected branch (github_ref or github_base_ref) in // the invocation parameters. ExpectedBranch string + // ExpectedSourceURI is the expected source URI in the provenance. + ExpectedSourceURI string + // ExpectedTag is the expected tag, github_ref, in the invocation parameters. ExpectedTag *string - // ExpectedVersionedTag is the expected versioned tag + // ExpectedVersionedTag is the expected versioned tag. ExpectedVersionedTag *string + + // ExpectedBuilderID is the expected builder ID. + ExpectedBuilderID string +} + +// BuildOpts are the options for checking the builder. +type BuilderOpts struct { + // ExpectedID is the expected builder ID. + ExpectedID *string } diff --git a/register/register.go b/register/register.go new file mode 100644 index 0000000..e0c6f80 --- /dev/null +++ b/register/register.go @@ -0,0 +1,34 @@ +package register + +import ( + "context" + + "github.com/slsa-framework/slsa-verifier/options" +) + +var SLSAVerifiers = make(map[string]SLSAVerifier) + +type SLSAVerifier interface { + // IsAuthoritativeFor checks whether a verifier can + // verify provenance for a given builder identified by its + // `BuilderID`. + IsAuthoritativeFor(builderID 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) + + // VerifyImage verifies a provenance for a supplied OCI image. + VerifyImage(ctx context.Context, + provenance []byte, artifactHash string, + provenanceOpts *options.ProvenanceOpts, + builderOpts *options.BuilderOpts, + ) ([]byte, string, error) +} + +func RegisterVerifier(name string, verifier SLSAVerifier) { + SLSAVerifiers[name] = verifier +} diff --git a/verification/verify.go b/verification/verify.go deleted file mode 100644 index cd86b54..0000000 --- a/verification/verify.go +++ /dev/null @@ -1,49 +0,0 @@ -package verification - -import ( - "context" - "encoding/base64" - "fmt" - "os" - - "github.com/sigstore/cosign/cmd/cosign/cli/rekor" -) - -func Verify(ctx context.Context, - provenance []byte, artifactHash, source string, provenanceOpts *ProvenanceOpts, -) ([]byte, error) { - rClient, err := rekor.NewClient(defaultRekorAddr) - if err != nil { - return nil, err - } - - /* Verify signature on the intoto attestation. */ - env, cert, err := VerifyProvenanceSignature(ctx, rClient, provenance, artifactHash) - if err != nil { - return nil, err - } - - /* Verify properties of the signing identity. */ - // Get the workflow info given the certificate information. - workflowInfo, err := GetWorkflowInfoFromCertificate(cert) - if err != nil { - return nil, err - } - - // Verify the workflow identity. - if err := VerifyWorkflowIdentity(workflowInfo, source); err != nil { - return nil, err - } - - /* Verify properties of the SLSA provenance. */ - // Unpack and verify info in the provenance, including the Subject Digest. - if err := VerifyProvenance(env, provenanceOpts); err != nil { - return nil, err - } - - fmt.Fprintf(os.Stderr, "Verified build using builder https://github.com%s at commit %s\n", - workflowInfo.JobWobWorkflowRef, - workflowInfo.CallerHash) - // Return verified provenance. - return base64.StdEncoding.DecodeString(env.Payload) -} diff --git a/verifiers/internal/gcb/verifier.go b/verifiers/internal/gcb/verifier.go new file mode 100644 index 0000000..4377a98 --- /dev/null +++ b/verifiers/internal/gcb/verifier.go @@ -0,0 +1,48 @@ +package gha + +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" +) + +const VerifierName = "GCB" + +//nolint:gochecknoinits +func init() { + register.RegisterVerifier(VerifierName, GCBVerifierNew()) +} + +type GCBVerifier struct{} + +func GCBVerifierNew() *GCBVerifier { + return &GCBVerifier{} +} + +// IsAuthoritativeFor returns true of the verifier can verify provenance +// generated by the builderID. +func (v *GCBVerifier) IsAuthoritativeFor(builderID string) bool { + // This verifier only supports the GCB builders. + return strings.HasPrefix(builderID, "https://cloudbuild.googleapis.com/GoogleHostedWorker@") +} + +// VerifyArtifact verifies provenance for an artifact. +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 +} + +// VerifyImage verifies provenance for an OCI image. +func (v *GCBVerifier) VerifyImage(ctx context.Context, + provenance []byte, artifactHash string, + provenanceOpts *options.ProvenanceOpts, + builderOpts *options.BuilderOpts, +) ([]byte, string, error) { + return nil, "todo", serrors.ErrorNotSupported +} diff --git a/verification/builder.go b/verifiers/internal/gha/builder.go similarity index 65% rename from verification/builder.go rename to verifiers/internal/gha/builder.go index 33fe2ae..7168cb0 100644 --- a/verification/builder.go +++ b/verifiers/internal/gha/builder.go @@ -1,4 +1,4 @@ -package verification +package gha import ( "crypto/x509" @@ -7,6 +7,9 @@ import ( "strings" "golang.org/x/mod/semver" + + serrors "github.com/slsa-framework/slsa-verifier/errors" + "github.com/slsa-framework/slsa-verifier/options" ) var ( @@ -15,44 +18,66 @@ var ( certOidcIssuer = "https://token.actions.githubusercontent.com" ) -var trustedReusableWorkflows = map[string]bool{ +var defaultTrustedReusableWorkflows = map[string]bool{ trustedBuilderRepository + "/.github/workflows/generator_generic_slsa3.yml": true, trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml": true, } // VerifyWorkflowIdentity verifies the signing certificate information -func VerifyWorkflowIdentity(id *WorkflowIdentity, source string) error { +func VerifyWorkflowIdentity(id *WorkflowIdentity, builderOpts *options.BuilderOpts, source string) (string, error) { // cert URI path is /org/repo/path/to/workflow@ref workflowPath := strings.SplitN(id.JobWobWorkflowRef, "@", 2) if len(workflowPath) < 2 { - return fmt.Errorf("%w: %s", errorMalformedWorkflowURI, id.JobWobWorkflowRef) + return "", fmt.Errorf("%w: workflow uri: %s", serrors.ErrorMalformedURI, id.JobWobWorkflowRef) } // Trusted workflow verification by name. - reusableWorkflowName := strings.Trim(workflowPath[0], "/") - if _, ok := trustedReusableWorkflows[reusableWorkflowName]; !ok { - return fmt.Errorf("%w: %s", ErrorUntrustedReusableWorkflow, reusableWorkflowName) + reusableWorkflowPath := strings.Trim(workflowPath[0], "/") + builderID, err := verifyTrustedBuilderID(reusableWorkflowPath, builderOpts.ExpectedID) + if err != nil { + return "", err } // Verify the ref. if err := verifyTrustedBuilderRef(id, strings.Trim(workflowPath[1], "/")); err != nil { - return err + return "", err } // Issuer verification. if !strings.EqualFold(id.Issuer, certOidcIssuer) { - return fmt.Errorf("untrusted token issuer: %s", id.Issuer) + return "", fmt.Errorf("untrusted token issuer: %s", id.Issuer) } // The caller repository in the x509 extension is not fully qualified. It only contains // {org}/{repository}. - expectedSource := strings.TrimPrefix(source, "github.com/") + expectedSource := strings.TrimPrefix(source, "git+https://") + expectedSource = strings.TrimPrefix(expectedSource, "github.com/") if !strings.EqualFold(id.CallerRepository, expectedSource) { - return fmt.Errorf("%w: expected source '%s', got '%s'", ErrorMismatchRepository, + return "", fmt.Errorf("%w: expected source '%s', got '%s'", serrors.ErrorMismatchSource, expectedSource, id.CallerRepository) } - return nil + // Return the builder and its tag. + return builderID, nil +} + +func verifyTrustedBuilderID(path string, builderID *string) (string, error) { + // No builder ID provided by user: use the default trusted workflows. + if builderID == nil || *builderID == "" { + if _, ok := defaultTrustedReusableWorkflows[path]; !ok { + return "", fmt.Errorf("%w: %s", serrors.ErrorUntrustedReusableWorkflow, path) + } + } else { + // Verify the builderID. + // We only accept IDs on github.com. + url := "https://github.com/" + path + if url != *builderID { + return "", fmt.Errorf("%w: expected buildID '%s', got '%s'", serrors.ErrorUntrustedReusableWorkflow, + *builderID, url) + } + } + + return "https://github.com/" + path, nil } // Only allow `@refs/heads/main` for the builder and the e2e tests that need to work at HEAD. @@ -66,7 +91,7 @@ func verifyTrustedBuilderRef(id *WorkflowIdentity, ref string) error { } if !strings.HasPrefix(ref, "refs/tags/") { - return fmt.Errorf("%w: %s: not of the form 'refs/tags/name'", errorInvalidRef, ref) + return fmt.Errorf("%w: %s: not of the form 'refs/tags/name'", serrors.ErrorInvalidRef, ref) } // Valid semver of the form vX.Y.Z with no metadata. @@ -75,7 +100,7 @@ func verifyTrustedBuilderRef(id *WorkflowIdentity, ref string) error { len(strings.Split(pin, ".")) == 3 && semver.Prerelease(pin) == "" && semver.Build(pin) == "") { - return fmt.Errorf("%w: %s: not of the form vX.Y.Z", errorInvalidRef, pin) + return fmt.Errorf("%w: %s: not of the form vX.Y.Z", serrors.ErrorInvalidRef, pin) } return nil } diff --git a/verification/builder_test.go b/verifiers/internal/gha/builder_test.go similarity index 54% rename from verification/builder_test.go rename to verifiers/internal/gha/builder_test.go index 25cdb83..3000bd0 100644 --- a/verification/builder_test.go +++ b/verifiers/internal/gha/builder_test.go @@ -1,19 +1,24 @@ -package verification +package gha import ( "testing" "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" ) func Test_VerifyWorkflowIdentity(t *testing.T) { t.Parallel() tests := []struct { - name string - workflow *WorkflowIdentity - source string - err error + name string + workflow *WorkflowIdentity + buildOpts *options.BuilderOpts + builderID string + source string + err error }{ { name: "invalid job workflow ref", @@ -25,7 +30,7 @@ func Test_VerifyWorkflowIdentity(t *testing.T) { Issuer: "https://token.actions.githubusercontent.com", }, source: "asraa/slsa-on-github-test", - err: errorMalformedWorkflowURI, + err: serrors.ErrorMalformedURI, }, { name: "untrusted job workflow ref", @@ -37,7 +42,7 @@ func Test_VerifyWorkflowIdentity(t *testing.T) { Issuer: "https://token.actions.githubusercontent.com", }, source: "asraa/slsa-on-github-test", - err: ErrorUntrustedReusableWorkflow, + err: serrors.ErrorUntrustedReusableWorkflow, }, { name: "untrusted job workflow ref for general repos", @@ -49,7 +54,7 @@ func Test_VerifyWorkflowIdentity(t *testing.T) { Issuer: "https://bad.issuer.com", }, source: "asraa/slsa-on-github-test", - err: errorInvalidRef, + err: serrors.ErrorInvalidRef, }, { name: "valid main ref for trusted builder", @@ -60,7 +65,8 @@ func Test_VerifyWorkflowIdentity(t *testing.T) { Trigger: "workflow_dispatch", Issuer: "https://token.actions.githubusercontent.com", }, - source: trustedBuilderRepository, + source: trustedBuilderRepository, + builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml", }, { name: "valid main ref for e2e test", @@ -71,7 +77,38 @@ func Test_VerifyWorkflowIdentity(t *testing.T) { Trigger: "workflow_dispatch", Issuer: certOidcIssuer, }, + source: e2eTestRepository, + builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml", + }, + { + name: "valid main ref for e2e test - match builderID", + workflow: &WorkflowIdentity{ + CallerRepository: e2eTestRepository, + CallerHash: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", + JobWobWorkflowRef: trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml@refs/heads/main", + Trigger: "workflow_dispatch", + Issuer: certOidcIssuer, + }, source: e2eTestRepository, + buildOpts: &options.BuilderOpts{ + ExpectedID: asStringPointer("https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml"), + }, + builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml", + }, + { + name: "valid main ref for e2e test - mismatch builderID", + workflow: &WorkflowIdentity{ + CallerRepository: e2eTestRepository, + CallerHash: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", + JobWobWorkflowRef: trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml@refs/heads/main", + Trigger: "workflow_dispatch", + Issuer: certOidcIssuer, + }, + source: e2eTestRepository, + buildOpts: &options.BuilderOpts{ + ExpectedID: asStringPointer("some-other-builderID"), + }, + err: serrors.ErrorUntrustedReusableWorkflow, }, { name: "unexpected source for e2e test", @@ -82,8 +119,9 @@ func Test_VerifyWorkflowIdentity(t *testing.T) { Trigger: "workflow_dispatch", Issuer: certOidcIssuer, }, - source: "malicious/source", - err: ErrorMismatchRepository, + source: "malicious/source", + err: serrors.ErrorMismatchSource, + builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml", }, { name: "valid main ref for builder", @@ -94,7 +132,7 @@ func Test_VerifyWorkflowIdentity(t *testing.T) { Issuer: certOidcIssuer, }, source: "malicious/source", - err: ErrorMismatchRepository, + err: serrors.ErrorMismatchSource, }, { name: "unexpected source", @@ -106,7 +144,7 @@ func Test_VerifyWorkflowIdentity(t *testing.T) { Issuer: certOidcIssuer, }, source: "asraa/slsa-on-github-test", - err: ErrorMismatchRepository, + err: serrors.ErrorMismatchSource, }, { name: "valid workflow identity", @@ -117,7 +155,38 @@ func Test_VerifyWorkflowIdentity(t *testing.T) { Trigger: "workflow_dispatch", Issuer: certOidcIssuer, }, + source: "asraa/slsa-on-github-test", + builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml", + }, + { + name: "valid workflow identity - match builderID", + workflow: &WorkflowIdentity{ + CallerRepository: "asraa/slsa-on-github-test", + CallerHash: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", + JobWobWorkflowRef: trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml@refs/tags/v1.2.3", + Trigger: "workflow_dispatch", + Issuer: certOidcIssuer, + }, source: "asraa/slsa-on-github-test", + buildOpts: &options.BuilderOpts{ + ExpectedID: asStringPointer("https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml"), + }, + builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml", + }, + { + name: "valid workflow identity - mismatch builderID", + workflow: &WorkflowIdentity{ + CallerRepository: "asraa/slsa-on-github-test", + CallerHash: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", + JobWobWorkflowRef: trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml@refs/tags/v1.2.3", + Trigger: "workflow_dispatch", + Issuer: certOidcIssuer, + }, + source: "asraa/slsa-on-github-test", + buildOpts: &options.BuilderOpts{ + ExpectedID: asStringPointer("some-other-builderID"), + }, + err: serrors.ErrorUntrustedReusableWorkflow, }, { name: "invalid workflow identity with prerelease", @@ -128,8 +197,9 @@ func Test_VerifyWorkflowIdentity(t *testing.T) { Trigger: "workflow_dispatch", Issuer: certOidcIssuer, }, - source: "asraa/slsa-on-github-test", - err: errorInvalidRef, + source: "asraa/slsa-on-github-test", + err: serrors.ErrorInvalidRef, + builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml", }, { name: "invalid workflow identity with build", @@ -141,7 +211,7 @@ func Test_VerifyWorkflowIdentity(t *testing.T) { Issuer: certOidcIssuer, }, source: "asraa/slsa-on-github-test", - err: errorInvalidRef, + err: serrors.ErrorInvalidRef, }, { name: "invalid workflow identity with metadata", @@ -153,7 +223,7 @@ func Test_VerifyWorkflowIdentity(t *testing.T) { Issuer: certOidcIssuer, }, source: "asraa/slsa-on-github-test", - err: errorInvalidRef, + err: serrors.ErrorInvalidRef, }, { name: "valid workflow identity with fully qualified source", @@ -164,17 +234,124 @@ func Test_VerifyWorkflowIdentity(t *testing.T) { Trigger: "workflow_dispatch", Issuer: certOidcIssuer, }, + source: "github.com/asraa/slsa-on-github-test", + builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml", + }, + { + name: "valid workflow identity with fully qualified source - match builderID", + workflow: &WorkflowIdentity{ + CallerRepository: "asraa/slsa-on-github-test", + CallerHash: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", + JobWobWorkflowRef: trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml@refs/tags/v1.2.3", + Trigger: "workflow_dispatch", + Issuer: certOidcIssuer, + }, source: "github.com/asraa/slsa-on-github-test", + buildOpts: &options.BuilderOpts{ + ExpectedID: asStringPointer("https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml"), + }, + builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml", + }, + { + name: "valid workflow identity with fully qualified source - mismatch builderID", + workflow: &WorkflowIdentity{ + CallerRepository: "asraa/slsa-on-github-test", + CallerHash: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", + JobWobWorkflowRef: trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml@refs/tags/v1.2.3", + Trigger: "workflow_dispatch", + Issuer: certOidcIssuer, + }, + source: "github.com/asraa/slsa-on-github-test", + buildOpts: &options.BuilderOpts{ + ExpectedID: asStringPointer("some-other-builderID"), + }, + err: serrors.ErrorUntrustedReusableWorkflow, }, } 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() - err := VerifyWorkflowIdentity(tt.workflow, tt.source) + opts := tt.buildOpts + if opts == nil { + opts = &options.BuilderOpts{} + } + id, err := VerifyWorkflowIdentity(tt.workflow, opts, tt.source) if !errCmp(err, tt.err) { t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) } + if err != nil { + return + } + if id != tt.builderID { + t.Errorf(cmp.Diff(id, tt.builderID)) + } + }) + } +} + +func asStringPointer(s string) *string { + return &s +} + +func Test_verifyTrustedBuilderID(t *testing.T) { + t.Parallel() + tests := []struct { + name string + id *string + path string + expected error + }{ + { + name: "default trusted", + path: trustedBuilderRepository + "/.github/workflows/generator_generic_slsa3.yml", + }, + { + name: "valid ID for GitHub builder", + path: "some/repo/someBuilderID", + id: asStringPointer("https://github.com/some/repo/someBuilderID"), + }, + { + name: "non GitHub builder ID", + path: "some/repo/someBuilderID", + id: asStringPointer("https://not-github.com/some/repo/someBuilderID"), + expected: serrors.ErrorUntrustedReusableWorkflow, + }, + { + name: "mismatch org GitHub", + path: "some/repo/someBuilderID", + id: asStringPointer("https://github.com/other/repo/someBuilderID"), + expected: serrors.ErrorUntrustedReusableWorkflow, + }, + { + name: "mismatch name GitHub", + path: "some/repo/someBuilderID", + id: asStringPointer("https://github.com/some/other/someBuilderID"), + expected: serrors.ErrorUntrustedReusableWorkflow, + }, + { + name: "mismatch id GitHub", + path: "some/repo/someBuilderID", + id: asStringPointer("https://github.com/some/repo/ID"), + expected: serrors.ErrorUntrustedReusableWorkflow, + }, + } + 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() + + id, err := verifyTrustedBuilderID(tt.path, tt.id) + if !errCmp(err, tt.expected) { + t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors())) + } + if err != nil { + return + } + expectedID := "https://github.com/" + tt.path + if id != expectedID { + t.Errorf(cmp.Diff(id, expectedID)) + } }) } } @@ -202,31 +379,31 @@ func Test_verifyTrustedBuilderRef(t *testing.T) { name: "no patch semver for other builder", callerRepo: trustedBuilderRepository, builderRef: "refs/tags/v1.2", - expected: errorInvalidRef, + expected: serrors.ErrorInvalidRef, }, { name: "no min semver for builder", callerRepo: trustedBuilderRepository, builderRef: "refs/tags/v1", - expected: errorInvalidRef, + expected: serrors.ErrorInvalidRef, }, { name: "full semver with prerelease for builder", callerRepo: trustedBuilderRepository, builderRef: "refs/tags/v1.2.3-alpha", - expected: errorInvalidRef, + expected: serrors.ErrorInvalidRef, }, { name: "full semver with build for builder", callerRepo: trustedBuilderRepository, builderRef: "refs/tags/v1.2.3+123", - expected: errorInvalidRef, + expected: serrors.ErrorInvalidRef, }, { name: "full semver with build/prerelease for builder", callerRepo: trustedBuilderRepository, builderRef: "refs/tags/v1.2.3-alpha+123", - expected: errorInvalidRef, + expected: serrors.ErrorInvalidRef, }, // E2e tests repo. { @@ -243,38 +420,38 @@ func Test_verifyTrustedBuilderRef(t *testing.T) { name: "no patch semver for test repo", callerRepo: e2eTestRepository, builderRef: "refs/tags/v1.2", - expected: errorInvalidRef, + expected: serrors.ErrorInvalidRef, }, { name: "no min semver for test repo", callerRepo: e2eTestRepository, builderRef: "refs/tags/v1", - expected: errorInvalidRef, + expected: serrors.ErrorInvalidRef, }, { name: "full semver with prerelease for test repo", callerRepo: e2eTestRepository, builderRef: "refs/tags/v1.2.3-alpha", - expected: errorInvalidRef, + expected: serrors.ErrorInvalidRef, }, { name: "full semver with build for test repo", callerRepo: e2eTestRepository, builderRef: "refs/tags/v1.2.3+123", - expected: errorInvalidRef, + expected: serrors.ErrorInvalidRef, }, { name: "full semver with build/prerelease for test repo", callerRepo: e2eTestRepository, builderRef: "refs/tags/v1.2.3-alpha+123", - expected: errorInvalidRef, + expected: serrors.ErrorInvalidRef, }, // Other repos. { name: "main not allowed for other repos", callerRepo: "some/repo", builderRef: "refs/heads/main", - expected: errorInvalidRef, + expected: serrors.ErrorInvalidRef, }, { name: "full semver for other repos", @@ -285,31 +462,31 @@ func Test_verifyTrustedBuilderRef(t *testing.T) { name: "no patch semver for other repos", callerRepo: "some/repo", builderRef: "refs/tags/v1.2", - expected: errorInvalidRef, + expected: serrors.ErrorInvalidRef, }, { name: "no min semver for other repos", callerRepo: "some/repo", builderRef: "refs/tags/v1", - expected: errorInvalidRef, + expected: serrors.ErrorInvalidRef, }, { name: "full semver with prerelease for other repos", callerRepo: "some/repo", builderRef: "refs/tags/v1.2.3-alpha", - expected: errorInvalidRef, + expected: serrors.ErrorInvalidRef, }, { name: "full semver with build for other repos", callerRepo: "some/repo", builderRef: "refs/tags/v1.2.3+123", - expected: errorInvalidRef, + expected: serrors.ErrorInvalidRef, }, { name: "full semver with build/prerelease for other repos", callerRepo: "some/repo", builderRef: "refs/tags/v1.2.3-alpha+123", - expected: errorInvalidRef, + expected: serrors.ErrorInvalidRef, }, } for _, tt := range tests { diff --git a/verification/provenance.go b/verifiers/internal/gha/provenance.go similarity index 59% rename from verification/provenance.go rename to verifiers/internal/gha/provenance.go index 2bb9d2b..73db3ee 100644 --- a/verification/provenance.go +++ b/verifiers/internal/gha/provenance.go @@ -1,4 +1,4 @@ -package verification +package gha import ( "context" @@ -14,6 +14,9 @@ import ( intoto "github.com/in-toto/in-toto-golang/in_toto" dsselib "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/sigstore/rekor/pkg/generated/client" + + serrors "github.com/slsa-framework/slsa-verifier/errors" + "github.com/slsa-framework/slsa-verifier/options" ) func EnvelopeFromBytes(payload []byte) (env *dsselib.Envelope, err error) { @@ -25,34 +28,128 @@ func EnvelopeFromBytes(payload []byte) (env *dsselib.Envelope, err error) { func provenanceFromEnv(env *dsselib.Envelope) (prov *intoto.ProvenanceStatement, err error) { pyld, err := base64.StdEncoding.DecodeString(env.Payload) if err != nil { - return nil, fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "decoding payload") + return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "decoding payload") } prov = &intoto.ProvenanceStatement{} if err := json.Unmarshal(pyld, prov); err != nil { - return nil, fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "unmarshalling json") + return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "unmarshalling json") } return } +// Verify Builder ID in provenance statement. +func verifyBuilderID(prov *intoto.ProvenanceStatement, builderID string) error { + // Check that the BuilderID is well-formed. + provid, err := sourceFromURI(prov.Predicate.Builder.ID, false) + if err != nil { + return err + } + // Note: builderID does not contain the tag. + // TODO(#189): support cases where user wants to match on the full builderID, including the tag. + bid, err := sourceFromURI(builderID, true) + if err != nil { + return err + } + if provid != bid { + return fmt.Errorf("%w: expected '%s' in builder.id, got '%s'", serrors.ErrorMismatchBuilderID, + bid, provid) + } + 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 *intoto.ProvenanceStatement, expectedSourceURI string) error { + source := asURI(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, + source) + } + + // Verify source from ConfigSource field. + configURI, err := sourceFromURI(prov.Predicate.Invocation.ConfigSource.URI, false) + if err != nil { + return err + } + if configURI != source { + return fmt.Errorf("%w: expected source '%s' in configSource.uri, got '%s'", serrors.ErrorMismatchSource, + source, prov.Predicate.Invocation.ConfigSource.URI) + } + + // Verify source from material section. + if len(prov.Predicate.Materials) == 0 { + return fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "no material") + } + materialURI, err := sourceFromURI(prov.Predicate.Materials[0].URI, false) + if err != nil { + return err + } + if materialURI != source { + return fmt.Errorf("%w: expected source '%s' in material section, got '%s'", serrors.ErrorMismatchSource, + source, prov.Predicate.Materials[0].URI) + } + + // Last, verify that both fields match. + // We use the full URI to match on the tag as well. + if prov.Predicate.Invocation.ConfigSource.URI != prov.Predicate.Materials[0].URI { + return fmt.Errorf("%w: material and config URIs do not match: '%s' != '%s'", + serrors.ErrorInvalidDssePayload, + prov.Predicate.Invocation.ConfigSource.URI, prov.Predicate.Materials[0].URI) + } + + return nil +} + +func sourceFromURI(uri string, allowNotTag bool) (string, error) { + if uri == "" { + return "", fmt.Errorf("%w: empty uri", serrors.ErrorMalformedURI) + } + + r := strings.SplitN(uri, "@", 2) + if len(r) < 2 && !allowNotTag { + 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 SHA256 Subject Digest from the provenance statement. func verifySha256Digest(prov *intoto.ProvenanceStatement, expectedHash string) error { if len(prov.Subject) == 0 { - return fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "no subjects") + return fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "no subjects") } for _, subject := range prov.Subject { digestSet := subject.Digest hash, exists := digestSet["sha256"] if !exists { - return fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "no sha256 subject digest") + return fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "no sha256 subject digest") } - if strings.EqualFold(hash, expectedHash) { + if hash == expectedHash { return nil } } - return fmt.Errorf("expected hash '%s' not found: %w", expectedHash, errorMismatchHash) + return fmt.Errorf("expected hash '%s' not found: %w", expectedHash, serrors.ErrorMismatchHash) } // VerifyProvenanceSignature returns the verified DSSE envelope containing the provenance @@ -85,32 +182,42 @@ func VerifyProvenanceSignature(ctx context.Context, rClient *client.Rekor, prove return env, cert, nil } -func VerifyProvenance(env *dsselib.Envelope, opts *ProvenanceOpts) error { +func VerifyProvenance(env *dsselib.Envelope, provenanceOpts *options.ProvenanceOpts) error { prov, err := provenanceFromEnv(env) if err != nil { return err } + // Verify Builder ID. + if err := verifyBuilderID(prov, provenanceOpts.ExpectedBuilderID); err != nil { + return err + } + + // Verify source. + if err := verifySourceURI(prov, provenanceOpts.ExpectedSourceURI); err != nil { + return err + } + // Verify subject digest. - if err := verifySha256Digest(prov, opts.ExpectedDigest); err != nil { + if err := verifySha256Digest(prov, provenanceOpts.ExpectedDigest); err != nil { return err } // Verify the branch. - if err := VerifyBranch(prov, opts.ExpectedBranch); err != nil { + if err := VerifyBranch(prov, provenanceOpts.ExpectedBranch); err != nil { return err } // Verify the tag. - if opts.ExpectedTag != nil { - if err := VerifyTag(prov, *opts.ExpectedTag); err != nil { + if provenanceOpts.ExpectedTag != nil { + if err := VerifyTag(prov, *provenanceOpts.ExpectedTag); err != nil { return err } } // Verify the versioned tag. - if opts.ExpectedVersionedTag != nil { - if err := VerifyVersionedTag(prov, *opts.ExpectedVersionedTag); err != nil { + if provenanceOpts.ExpectedVersionedTag != nil { + if err := VerifyVersionedTag(prov, *provenanceOpts.ExpectedVersionedTag); err != nil { return err } } @@ -126,7 +233,7 @@ func VerifyBranch(prov *intoto.ProvenanceStatement, expectedBranch string) error expectedBranch = "refs/heads/" + expectedBranch if !strings.EqualFold(branch, expectedBranch) { - return fmt.Errorf("expected branch '%s', got '%s': %w", expectedBranch, branch, ErrorMismatchBranch) + return fmt.Errorf("expected branch '%s', got '%s': %w", expectedBranch, branch, serrors.ErrorMismatchBranch) } return nil @@ -140,7 +247,7 @@ func VerifyTag(prov *intoto.ProvenanceStatement, expectedTag string) error { expectedTag = "refs/tags/" + expectedTag if !strings.EqualFold(tag, expectedTag) { - return fmt.Errorf("expected tag '%s', got '%s': %w", expectedTag, tag, ErrorMismatchTag) + return fmt.Errorf("expected tag '%s', got '%s': %w", expectedTag, tag, serrors.ErrorMismatchTag) } return nil @@ -149,7 +256,7 @@ func VerifyTag(prov *intoto.ProvenanceStatement, expectedTag string) error { func VerifyVersionedTag(prov *intoto.ProvenanceStatement, expectedTag string) error { // Validate and canonicalize the provenance tag. if !semver.IsValid(expectedTag) { - return fmt.Errorf("%s: %w", expectedTag, ErrorInvalidSemver) + return fmt.Errorf("%s: %w", expectedTag, serrors.ErrorInvalidSemver) } // Retrieve, validate and canonicalize the provenance tag. @@ -162,7 +269,7 @@ func VerifyVersionedTag(prov *intoto.ProvenanceStatement, expectedTag string) er } semTag := semver.Canonical(strings.TrimPrefix(tag, "refs/tags/")) if !semver.IsValid(semTag) { - return fmt.Errorf("%s: %w", expectedTag, ErrorInvalidSemver) + return fmt.Errorf("%s: %w", expectedTag, serrors.ErrorInvalidSemver) } // Major should always be the same. @@ -170,7 +277,7 @@ func VerifyVersionedTag(prov *intoto.ProvenanceStatement, expectedTag string) er major := semver.Major(semTag) if major != expectedMajor { return fmt.Errorf("%w: major version expected '%s', got '%s'", - ErrorMismatchVersionedTag, expectedMajor, major) + serrors.ErrorMismatchVersionedTag, expectedMajor, major) } expectedMinor, err := minorVersion(expectedTag) @@ -183,7 +290,7 @@ func VerifyVersionedTag(prov *intoto.ProvenanceStatement, expectedTag string) er if minor != expectedMinor { return fmt.Errorf("%w: minor version expected '%s', got '%s'", - ErrorMismatchVersionedTag, expectedMinor, minor) + serrors.ErrorMismatchVersionedTag, expectedMinor, minor) } } @@ -197,7 +304,7 @@ func VerifyVersionedTag(prov *intoto.ProvenanceStatement, expectedTag string) er if patch != expectedPatch { return fmt.Errorf("%w: patch version expected '%s', got '%s'", - ErrorMismatchVersionedTag, expectedPatch, patch) + serrors.ErrorMismatchVersionedTag, expectedPatch, patch) } } @@ -220,7 +327,7 @@ func patchVersion(v string) (string, error) { func extractFromVersion(v string, i int) (string, error) { parts := strings.Split(v, ".") if len(parts) <= i { - return "", fmt.Errorf("%s: %w", v, ErrorInvalidSemver) + return "", fmt.Errorf("%s: %w", v, serrors.ErrorInvalidSemver) } return parts[i], nil } @@ -228,13 +335,13 @@ func extractFromVersion(v string, i int) (string, error) { func getAsString(environment map[string]interface{}, field string) (string, error) { value, ok := environment[field] if !ok { - return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, + return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, fmt.Sprintf("environment type for %s", field)) } i, ok := value.(string) if !ok { - return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "environment type string") + return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "environment type string") } return i, nil } @@ -242,12 +349,12 @@ func getAsString(environment map[string]interface{}, field string) (string, erro func getEventPayload(environment map[string]interface{}) (map[string]interface{}, error) { eventPayload, ok := environment["github_event_payload"] if !ok { - return nil, fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "parameters type event payload") + return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type event payload") } payload, ok := eventPayload.(map[string]interface{}) if !ok { - return nil, fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "parameters type payload") + return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type payload") } return payload, nil @@ -302,12 +409,12 @@ func getTargetCommittish(environment map[string]interface{}) (string, error) { // For a release event, we look for release.target_commitish. releasePayload, ok := payload["release"] if !ok { - return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "release absent from payload") + return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "release absent from payload") } release, ok := releasePayload.(map[string]interface{}) if !ok { - return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "parameters type releasePayload") + return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type releasePayload") } branch, err := getAsString(release, "target_commitish") @@ -333,7 +440,7 @@ func getBranchForTag(environment map[string]interface{}) (string, error) { func getTag(prov *intoto.ProvenanceStatement) (string, error) { environment, ok := prov.Predicate.Invocation.Environment.(map[string]interface{}) if !ok { - return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "parameters type") + return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type") } refType, err := getAsString(environment, "github_ref_type") @@ -347,7 +454,7 @@ func getTag(prov *intoto.ProvenanceStatement) (string, error) { case "tag": return getAsString(environment, "github_ref") default: - return "", fmt.Errorf("%w: %s %s", ErrorInvalidDssePayload, + return "", fmt.Errorf("%w: %s %s", serrors.ErrorInvalidDssePayload, "unknown ref type", refType) } } @@ -356,7 +463,7 @@ func getTag(prov *intoto.ProvenanceStatement) (string, error) { func getBranch(prov *intoto.ProvenanceStatement) (string, error) { environment, ok := prov.Predicate.Invocation.Environment.(map[string]interface{}) if !ok { - return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "parameters type") + return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type") } refType, err := getAsString(environment, "github_ref_type") @@ -370,7 +477,7 @@ func getBranch(prov *intoto.ProvenanceStatement) (string, error) { case "tag": return getBranchForTag(environment) default: - return "", fmt.Errorf("%w: %s %s", ErrorInvalidDssePayload, + return "", fmt.Errorf("%w: %s %s", serrors.ErrorInvalidDssePayload, "unknown ref type", refType) } } diff --git a/verification/provenance_test.go b/verifiers/internal/gha/provenance_test.go similarity index 54% rename from verification/provenance_test.go rename to verifiers/internal/gha/provenance_test.go index 4f2c630..f166892 100644 --- a/verification/provenance_test.go +++ b/verifiers/internal/gha/provenance_test.go @@ -1,4 +1,4 @@ -package verification +package gha import ( "fmt" @@ -7,6 +7,9 @@ import ( "github.com/google/go-cmp/cmp" intoto "github.com/in-toto/in-toto-golang/in_toto" + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" + + serrors "github.com/slsa-framework/slsa-verifier/errors" ) func provenanceFromBytes(payload []byte) (*intoto.ProvenanceStatement, error) { @@ -29,25 +32,25 @@ func Test_VerifySha256Subject(t *testing.T) { name: "invalid dsse: not SLSA predicate", path: "./testdata/dsse-not-slsa.intoto.jsonl", artifactHash: "0ae7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e", - expected: ErrorInvalidDssePayload, + expected: serrors.ErrorInvalidDssePayload, }, { name: "invalid dsse: nil subject", path: "./testdata/dsse-no-subject.intoto.jsonl", artifactHash: "0ae7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e", - expected: ErrorInvalidDssePayload, + expected: serrors.ErrorInvalidDssePayload, }, { name: "invalid dsse: no sha256 subject digest", path: "./testdata/dsse-no-subject-hash.intoto.jsonl", artifactHash: "0ae7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e", - expected: ErrorInvalidDssePayload, + expected: serrors.ErrorInvalidDssePayload, }, { name: "mismatched artifact hash with env", path: "./testdata/dsse-valid.intoto.jsonl", artifactHash: "1ae7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e", - expected: errorMismatchHash, + expected: serrors.ErrorMismatchHash, }, { name: "valid entry", @@ -71,7 +74,7 @@ func Test_VerifySha256Subject(t *testing.T) { name: "multiple subjects invalid hash", path: "./testdata/dsse-valid-multi-subjects.intoto.jsonl", artifactHash: "04e7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e", - expected: errorMismatchHash, + expected: serrors.ErrorMismatchHash, }, } for _, tt := range tests { @@ -96,6 +99,347 @@ func Test_VerifySha256Subject(t *testing.T) { } } +func Test_verifySourceURI(t *testing.T) { + t.Parallel() + tests := []struct { + name string + prov *intoto.ProvenanceStatement + sourceURI string + expected error + }{ + { + name: "source has no @", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Invocation: slsa.ProvenanceInvocation{ + ConfigSource: slsa.ConfigSource{ + URI: "git+https://github.com/some/repo", + }, + }, + }, + }, + sourceURI: "git+https://github.com/some/repo", + expected: serrors.ErrorMalformedURI, + }, + { + name: "empty materials", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Invocation: slsa.ProvenanceInvocation{ + ConfigSource: slsa.ConfigSource{ + URI: "git+https://github.com/some/repo@v1.2.3", + }, + }, + }, + }, + sourceURI: "git+https://github.com/some/repo", + expected: serrors.ErrorInvalidDssePayload, + }, + { + name: "empty configSource", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Materials: []slsa.ProvenanceMaterial{ + { + URI: "git+https://github.com/some/repo@v1.2.3", + }, + }, + }, + }, + sourceURI: "git+https://github.com/some/repo", + expected: serrors.ErrorMalformedURI, + }, + { + name: "empty uri materials", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Materials: []slsa.ProvenanceMaterial{ + { + URI: "", + }, + }, + }, + }, + sourceURI: "git+https://github.com/some/repo", + expected: serrors.ErrorMalformedURI, + }, + { + name: "no tag uri materials", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Materials: []slsa.ProvenanceMaterial{ + { + URI: "git+https://github.com/some/repo", + }, + }, + }, + }, + sourceURI: "git+https://github.com/some/repo", + expected: serrors.ErrorMalformedURI, + }, + { + name: "no tag uri configSource", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Materials: []slsa.ProvenanceMaterial{ + { + URI: "git+https://github.com/some/repo", + }, + }, + }, + }, + sourceURI: "git+https://github.com/some/repo", + expected: serrors.ErrorMalformedURI, + }, + { + name: "match source", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Invocation: slsa.ProvenanceInvocation{ + ConfigSource: slsa.ConfigSource{ + URI: "git+https://github.com/some/repo@v1.2.3", + }, + }, + Materials: []slsa.ProvenanceMaterial{ + { + URI: "git+https://github.com/some/repo@v1.2.3", + }, + }, + }, + }, + sourceURI: "git+https://github.com/some/repo", + }, + { + name: "match source no git", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Invocation: slsa.ProvenanceInvocation{ + ConfigSource: slsa.ConfigSource{ + URI: "git+https://github.com/some/repo@v1.2.3", + }, + }, + Materials: []slsa.ProvenanceMaterial{ + { + URI: "git+https://github.com/some/repo@v1.2.3", + }, + }, + }, + }, + sourceURI: "https://github.com/some/repo", + }, + { + name: "match source no git+https", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Invocation: slsa.ProvenanceInvocation{ + ConfigSource: slsa.ConfigSource{ + URI: "git+https://github.com/some/repo@v1.2.3", + }, + }, + Materials: []slsa.ProvenanceMaterial{ + { + URI: "git+https://github.com/some/repo@v1.2.3", + }, + }, + }, + }, + sourceURI: "github.com/some/repo", + }, + { + name: "match source no repo", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Invocation: slsa.ProvenanceInvocation{ + ConfigSource: slsa.ConfigSource{ + URI: "git+https://github.com/some/repo@v1.2.3", + }, + }, + Materials: []slsa.ProvenanceMaterial{ + { + URI: "git+https://github.com/some/repo@v1.2.3", + }, + }, + }, + }, + sourceURI: "some/repo", + expected: serrors.ErrorMalformedURI, + }, + { + name: "mismatch materials configSource tag", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Invocation: slsa.ProvenanceInvocation{ + ConfigSource: slsa.ConfigSource{ + URI: "git+https://github.com/some/repo@v1.2.4", + }, + }, + Materials: []slsa.ProvenanceMaterial{ + { + URI: "git+https://github.com/some/repo@v1.2.3", + }, + }, + }, + }, + sourceURI: "git+https://github.com/some/repo", + expected: serrors.ErrorInvalidDssePayload, + }, + { + name: "mismatch materials configSource org", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Invocation: slsa.ProvenanceInvocation{ + ConfigSource: slsa.ConfigSource{ + URI: "git+https://github.com/other/repo@v1.2.3", + }, + }, + Materials: []slsa.ProvenanceMaterial{ + { + URI: "git+https://github.com/some/repo@v1.2.3", + }, + }, + }, + }, + sourceURI: "git+https://github.com/some/repo", + expected: serrors.ErrorMismatchSource, + }, + { + name: "mismatch materials configSource name", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Invocation: slsa.ProvenanceInvocation{ + ConfigSource: slsa.ConfigSource{ + URI: "git+https://github.com/some/other@v1.2.3", + }, + }, + Materials: []slsa.ProvenanceMaterial{ + { + URI: "git+https://github.com/some/repo@v1.2.3", + }, + }, + }, + }, + sourceURI: "git+https://github.com/some/repo", + expected: serrors.ErrorMismatchSource, + }, + { + name: "not github.com repo", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Invocation: slsa.ProvenanceInvocation{ + ConfigSource: slsa.ConfigSource{ + URI: "git+https://not-github.com/some/repo@v1.2.3", + }, + }, + Materials: []slsa.ProvenanceMaterial{ + { + URI: "git+https://not-github.com/some/repo@v1.2.3", + }, + }, + }, + }, + sourceURI: "git+https://not-github.com/some/repo", + expected: serrors.ErrorMalformedURI, + }, + } + 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() + + err := verifySourceURI(tt.prov, tt.sourceURI) + if !errCmp(err, tt.expected) { + t.Errorf(cmp.Diff(err, tt.expected)) + } + }) + } +} + +func Test_verifyBuilderID(t *testing.T) { + t.Parallel() + tests := []struct { + name string + prov *intoto.ProvenanceStatement + id string + expected error + }{ + { + name: "id has no @", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Builder: slsa.ProvenanceBuilder{ + ID: "some/builderID", + }, + }, + }, + id: "some/builderID", + expected: serrors.ErrorMalformedURI, + }, + { + name: "same builderID", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Builder: slsa.ProvenanceBuilder{ + ID: "some/builderID@v1.2.3", + }, + }, + }, + id: "some/builderID", + }, + { + name: "same builderID full match", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Builder: slsa.ProvenanceBuilder{ + ID: "some/builderID@v1.2.3", + }, + }, + }, + id: "some/builderID@v1.2.3", + }, + { + name: "same builderID mismatch version", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Builder: slsa.ProvenanceBuilder{ + ID: "some/builderID@v1.2.3", + }, + }, + }, + id: "some/builderID@v1.2.4", + // TODO(#189): this should fail. + }, + { + name: "mismatch builderID", + prov: &intoto.ProvenanceStatement{ + Predicate: slsa.ProvenancePredicate{ + Builder: slsa.ProvenanceBuilder{ + ID: "tome/builderID@v1.2.3", + }, + }, + }, + id: "some/builderID", + expected: serrors.ErrorMismatchBuilderID, + }, + { + name: "empty builderID", + prov: &intoto.ProvenanceStatement{}, + id: "some/builderID", + expected: serrors.ErrorMalformedURI, + }, + } + 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() + + err := verifyBuilderID(tt.prov, tt.id) + if !errCmp(err, tt.expected) { + t.Errorf(cmp.Diff(err, tt.expected)) + } + }) + } +} + func Test_VerifyBranch(t *testing.T) { t.Parallel() tests := []struct { @@ -117,7 +461,7 @@ func Test_VerifyBranch(t *testing.T) { { name: "invalid ref type", path: "./testdata/dsse-invalid-ref-type.intoto.jsonl", - expected: ErrorInvalidDssePayload, + expected: serrors.ErrorInvalidDssePayload, }, { name: "tag branch2 push trigger", @@ -163,17 +507,17 @@ func Test_VerifyTag(t *testing.T) { { name: "ref main", path: "./testdata/dsse-main-ref.intoto.jsonl", - expected: ErrorMismatchTag, + expected: serrors.ErrorMismatchTag, }, { name: "ref branch3", path: "./testdata/dsse-branch3-ref.intoto.jsonl", - expected: ErrorMismatchTag, + expected: serrors.ErrorMismatchTag, }, { name: "invalid ref type", path: "./testdata/dsse-invalid-ref-type.intoto.jsonl", - expected: ErrorInvalidDssePayload, + expected: serrors.ErrorInvalidDssePayload, }, { name: "tag vslsa1", @@ -214,38 +558,38 @@ func Test_VerifyVersionedTag(t *testing.T) { { name: "ref main", path: "./testdata/dsse-main-ref.intoto.jsonl", - expected: ErrorInvalidSemver, + expected: serrors.ErrorInvalidSemver, tag: "v1.2.3", }, { name: "ref branch3", path: "./testdata/dsse-branch3-ref.intoto.jsonl", - expected: ErrorInvalidSemver, + expected: serrors.ErrorInvalidSemver, tag: "v1.2.3", }, { name: "tag v1.2 invalid versioning", path: "./testdata/dsse-v1.2-tag.intoto.jsonl", tag: "1.2", - expected: ErrorInvalidSemver, + expected: serrors.ErrorInvalidSemver, }, { name: "invalid ref", path: "./testdata/dsse-invalid-ref-type.intoto.jsonl", - expected: ErrorInvalidDssePayload, + expected: serrors.ErrorInvalidDssePayload, tag: "v1.2.3", }, { name: "tag vslsa1 invalid", path: "./testdata/dsse-vslsa1-tag.intoto.jsonl", tag: "vslsa1", - expected: ErrorInvalidSemver, + expected: serrors.ErrorInvalidSemver, }, { name: "tag vslsa1 invalid semver", path: "./testdata/dsse-vslsa1-tag.intoto.jsonl", tag: "v1.2.3", - expected: ErrorInvalidSemver, + expected: serrors.ErrorInvalidSemver, }, { name: "tag v1.2.3 exact match", @@ -266,25 +610,25 @@ func Test_VerifyVersionedTag(t *testing.T) { name: "tag v1.2.3 no match v2", path: "./testdata/dsse-v1.2.3-tag.intoto.jsonl", tag: "v2", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1.2.3 no match v1.3", path: "./testdata/dsse-v1.2.3-tag.intoto.jsonl", tag: "v1.3", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1.2.3 no match v1.2.4", path: "./testdata/dsse-v1.2.3-tag.intoto.jsonl", tag: "v1.2.4", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1.2.3 no match v1.2.2", path: "./testdata/dsse-v1.2.3-tag.intoto.jsonl", tag: "v1.2.2", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1.2 exact v1.2", @@ -300,25 +644,25 @@ func Test_VerifyVersionedTag(t *testing.T) { name: "tag v1.1 no match v1.3", path: "./testdata/dsse-v1.2-tag.intoto.jsonl", tag: "v1.1", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v0 no match v1.3", path: "./testdata/dsse-v1.2-tag.intoto.jsonl", tag: "v0", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1.2 no match v1.3", path: "./testdata/dsse-v1.2-tag.intoto.jsonl", tag: "v1.3", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1.2 no match v1.2.3", path: "./testdata/dsse-v1.2-tag.intoto.jsonl", tag: "v1.2.3", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1.2 match v1.2.0", @@ -334,37 +678,37 @@ func Test_VerifyVersionedTag(t *testing.T) { name: "invalid v1.2+123", path: "./testdata/dsse-v1.2-tag.intoto.jsonl", tag: "v1.2+123", - expected: ErrorInvalidSemver, + expected: serrors.ErrorInvalidSemver, }, { name: "invalid v1.2-alpha", path: "./testdata/dsse-v1.2-tag.intoto.jsonl", tag: "v1.2-alpha", - expected: ErrorInvalidSemver, + expected: serrors.ErrorInvalidSemver, }, { name: "invalid v1-alpha", path: "./testdata/dsse-v1.2-tag.intoto.jsonl", tag: "v1-alpha", - expected: ErrorInvalidSemver, + expected: serrors.ErrorInvalidSemver, }, { name: "invalid v1+123", path: "./testdata/dsse-v1.2-tag.intoto.jsonl", tag: "v1+123", - expected: ErrorInvalidSemver, + expected: serrors.ErrorInvalidSemver, }, { name: "invalid v1-alpha+123", path: "./testdata/dsse-v1.2-tag.intoto.jsonl", tag: "v1-alpha+123", - expected: ErrorInvalidSemver, + expected: serrors.ErrorInvalidSemver, }, { name: "invalid v1.2-alpha+123", path: "./testdata/dsse-v1.2-tag.intoto.jsonl", tag: "v1.2-alpha+123", - expected: ErrorInvalidSemver, + expected: serrors.ErrorInvalidSemver, }, { name: "tag v1.2.3-alpha match v1.2.3-alpha", @@ -375,7 +719,7 @@ func Test_VerifyVersionedTag(t *testing.T) { name: "tag v1.2.3-alpha no match v1.2.3", path: "./testdata/dsse-v1.2.3-alpha-tag.intoto.jsonl", tag: "v1.2.3", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1.2.3-alpha+123 match v1.2.3-alpha", @@ -401,19 +745,19 @@ func Test_VerifyVersionedTag(t *testing.T) { name: "tag v1.2.3-alpha no match v1.2.3-beta+123", path: "./testdata/dsse-v1.2.3-alpha-tag.intoto.jsonl", tag: "v1.2.3-beta+123", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1.2.3+123 no match v1.2.3-alpha+123", path: "./testdata/dsse-v1.2.3+123-tag.intoto.jsonl", tag: "v1.2.3-alpha+123", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1.2.3+123 no match v1.2.3-alpha", path: "./testdata/dsse-v1.2.3+123-tag.intoto.jsonl", tag: "v1.2.3-alpha", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1.2.3+123 match v1.2.3+123", @@ -434,19 +778,19 @@ func Test_VerifyVersionedTag(t *testing.T) { name: "tag v1.2.3 no match v1.2.3-aplha", path: "./testdata/dsse-v1.2.3-tag.intoto.jsonl", tag: "v1.2.3-alpha", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1.2.3-alpha no match v1.2.3-beta", path: "./testdata/dsse-v1.2.3-alpha-tag.intoto.jsonl", tag: "v1.2.3-beta", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1.2 no match v1.2.3-beta", path: "./testdata/dsse-v1.2.3-alpha-tag.intoto.jsonl", tag: "v1.2.3-beta", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1.2.3 match v1.2.3+123", @@ -457,13 +801,13 @@ func Test_VerifyVersionedTag(t *testing.T) { name: "tag v1.2 no match v1.2.0-aplha+123", path: "./testdata/dsse-v1.2-tag.intoto.jsonl", tag: "v1.2.0-alpha+123", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1.2 no match v2", path: "./testdata/dsse-v1.2-tag.intoto.jsonl", tag: "v2", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1 exact match", @@ -474,25 +818,25 @@ func Test_VerifyVersionedTag(t *testing.T) { name: "tag v1 no match v2", path: "./testdata/dsse-v1-tag.intoto.jsonl", tag: "v2", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1 no match v1.2", path: "./testdata/dsse-v1-tag.intoto.jsonl", tag: "v1.2", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1 no match v0", path: "./testdata/dsse-v1-tag.intoto.jsonl", tag: "v0", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1 no match v1.2.3", path: "./testdata/dsse-v1-tag.intoto.jsonl", tag: "v1.2.3", - expected: ErrorMismatchVersionedTag, + expected: serrors.ErrorMismatchVersionedTag, }, { name: "tag v1 match v1.0", @@ -508,7 +852,7 @@ func Test_VerifyVersionedTag(t *testing.T) { name: "invalid v1-alpha", path: "./testdata/dsse-v1-tag.intoto.jsonl", tag: "v1-alpha", - expected: ErrorInvalidSemver, + expected: serrors.ErrorInvalidSemver, }, } for _, tt := range tests { diff --git a/verification/rekor.go b/verifiers/internal/gha/rekor.go similarity index 95% rename from verification/rekor.go rename to verifiers/internal/gha/rekor.go index 3dcf752..06e83df 100644 --- a/verification/rekor.go +++ b/verifiers/internal/gha/rekor.go @@ -1,4 +1,4 @@ -package verification +package gha import ( "bytes" @@ -37,6 +37,8 @@ import ( "github.com/sigstore/sigstore/pkg/signature/dsse" "github.com/slsa-framework/slsa-github-generator/signing/envelope" "github.com/transparency-dev/merkle/proof" + + serrors "github.com/slsa-framework/slsa-verifier/errors" ) const ( @@ -265,11 +267,11 @@ func GetRekorEntries(rClient *client.Rekor, artifactHash string) ([]string, erro params.Query = &models.SearchIndex{Hash: fmt.Sprintf("sha256:%v", artifactHash)} resp, err := rClient.Index.SearchIndex(params) if err != nil { - return nil, fmt.Errorf("%w: %s", ErrorRekorSearch, err.Error()) + return nil, fmt.Errorf("%w: %s", serrors.ErrorRekorSearch, err.Error()) } if len(resp.Payload) == 0 { - return nil, fmt.Errorf("%w: no matching entries found", ErrorRekorSearch) + return nil, fmt.Errorf("%w: no matching entries found", serrors.ErrorRekorSearch) } return resp.GetPayload(), nil @@ -300,11 +302,11 @@ func GetRekorEntriesWithCert(rClient *client.Rekor, provenance []byte) (*dsselib params.SetEntry(&searchLogQuery) resp, err := rClient.Entries.SearchLogQuery(params) if err != nil { - return nil, nil, fmt.Errorf("%w: %s", ErrorRekorSearch, err.Error()) + return nil, nil, fmt.Errorf("%w: %s", serrors.ErrorRekorSearch, err.Error()) } if len(resp.GetPayload()) != 1 { - return nil, nil, fmt.Errorf("%w: %s", ErrorRekorSearch, "no matching rekor entries") + return nil, nil, fmt.Errorf("%w: %s", serrors.ErrorRekorSearch, "no matching rekor entries") } logEntry := resp.Payload[0] @@ -394,5 +396,5 @@ func FindSigningCertificate(ctx context.Context, uuids []string, dssePayload dss return cert, nil } - return nil, fmt.Errorf("%w: got unexpected errors %s", ErrorNoValidRekorEntries, strings.Join(errs, ", ")) + return nil, fmt.Errorf("%w: got unexpected errors %s", serrors.ErrorNoValidRekorEntries, strings.Join(errs, ", ")) } diff --git a/verification/rekor_test.go b/verifiers/internal/gha/rekor_test.go similarity index 93% rename from verification/rekor_test.go rename to verifiers/internal/gha/rekor_test.go index 334ebcc..2d8c285 100644 --- a/verification/rekor_test.go +++ b/verifiers/internal/gha/rekor_test.go @@ -1,4 +1,4 @@ -package verification +package gha import ( "errors" @@ -8,6 +8,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/client/index" + + serrors "github.com/slsa-framework/slsa-verifier/errors" ) type searchResult struct { @@ -46,7 +48,7 @@ func Test_GetRekorEntries(t *testing.T) { res: searchResult{ err: index.NewSearchIndexDefault(500), }, - expected: ErrorRekorSearch, + expected: serrors.ErrorRekorSearch, }, { name: "no rekor entries found", @@ -57,7 +59,7 @@ func Test_GetRekorEntries(t *testing.T) { Payload: []string{}, }, }, - expected: ErrorRekorSearch, + expected: serrors.ErrorRekorSearch, }, { name: "valid rekor entries found", diff --git a/verification/testdata/dsse-branch2-tag.intoto.jsonl b/verifiers/internal/gha/testdata/dsse-branch2-tag.intoto.jsonl similarity index 100% rename from verification/testdata/dsse-branch2-tag.intoto.jsonl rename to verifiers/internal/gha/testdata/dsse-branch2-tag.intoto.jsonl diff --git a/verification/testdata/dsse-branch3-ref.intoto.jsonl b/verifiers/internal/gha/testdata/dsse-branch3-ref.intoto.jsonl similarity index 100% rename from verification/testdata/dsse-branch3-ref.intoto.jsonl rename to verifiers/internal/gha/testdata/dsse-branch3-ref.intoto.jsonl diff --git a/verification/testdata/dsse-invalid-ref-type.intoto.jsonl b/verifiers/internal/gha/testdata/dsse-invalid-ref-type.intoto.jsonl similarity index 100% rename from verification/testdata/dsse-invalid-ref-type.intoto.jsonl rename to verifiers/internal/gha/testdata/dsse-invalid-ref-type.intoto.jsonl diff --git a/verification/testdata/dsse-main-ref.intoto.jsonl b/verifiers/internal/gha/testdata/dsse-main-ref.intoto.jsonl similarity index 100% rename from verification/testdata/dsse-main-ref.intoto.jsonl rename to verifiers/internal/gha/testdata/dsse-main-ref.intoto.jsonl diff --git a/verification/testdata/dsse-no-subject-hash.intoto.jsonl b/verifiers/internal/gha/testdata/dsse-no-subject-hash.intoto.jsonl similarity index 100% rename from verification/testdata/dsse-no-subject-hash.intoto.jsonl rename to verifiers/internal/gha/testdata/dsse-no-subject-hash.intoto.jsonl diff --git a/verification/testdata/dsse-no-subject.intoto.jsonl b/verifiers/internal/gha/testdata/dsse-no-subject.intoto.jsonl similarity index 100% rename from verification/testdata/dsse-no-subject.intoto.jsonl rename to verifiers/internal/gha/testdata/dsse-no-subject.intoto.jsonl diff --git a/verification/testdata/dsse-not-slsa.intoto.jsonl b/verifiers/internal/gha/testdata/dsse-not-slsa.intoto.jsonl similarity index 100% rename from verification/testdata/dsse-not-slsa.intoto.jsonl rename to verifiers/internal/gha/testdata/dsse-not-slsa.intoto.jsonl diff --git a/verification/testdata/dsse-v1-tag.intoto.jsonl b/verifiers/internal/gha/testdata/dsse-v1-tag.intoto.jsonl similarity index 100% rename from verification/testdata/dsse-v1-tag.intoto.jsonl rename to verifiers/internal/gha/testdata/dsse-v1-tag.intoto.jsonl diff --git a/verification/testdata/dsse-v1.2-tag.intoto.jsonl b/verifiers/internal/gha/testdata/dsse-v1.2-tag.intoto.jsonl similarity index 100% rename from verification/testdata/dsse-v1.2-tag.intoto.jsonl rename to verifiers/internal/gha/testdata/dsse-v1.2-tag.intoto.jsonl diff --git a/verification/testdata/dsse-v1.2.3+123-tag.intoto.jsonl b/verifiers/internal/gha/testdata/dsse-v1.2.3+123-tag.intoto.jsonl similarity index 100% rename from verification/testdata/dsse-v1.2.3+123-tag.intoto.jsonl rename to verifiers/internal/gha/testdata/dsse-v1.2.3+123-tag.intoto.jsonl diff --git a/verification/testdata/dsse-v1.2.3-alpha+123-tag.intoto.jsonl b/verifiers/internal/gha/testdata/dsse-v1.2.3-alpha+123-tag.intoto.jsonl similarity index 100% rename from verification/testdata/dsse-v1.2.3-alpha+123-tag.intoto.jsonl rename to verifiers/internal/gha/testdata/dsse-v1.2.3-alpha+123-tag.intoto.jsonl diff --git a/verification/testdata/dsse-v1.2.3-alpha-tag.intoto.jsonl b/verifiers/internal/gha/testdata/dsse-v1.2.3-alpha-tag.intoto.jsonl similarity index 100% rename from verification/testdata/dsse-v1.2.3-alpha-tag.intoto.jsonl rename to verifiers/internal/gha/testdata/dsse-v1.2.3-alpha-tag.intoto.jsonl diff --git a/verification/testdata/dsse-v1.2.3-tag.intoto.jsonl b/verifiers/internal/gha/testdata/dsse-v1.2.3-tag.intoto.jsonl similarity index 100% rename from verification/testdata/dsse-v1.2.3-tag.intoto.jsonl rename to verifiers/internal/gha/testdata/dsse-v1.2.3-tag.intoto.jsonl diff --git a/verification/testdata/dsse-v10.0.1-release.intoto.jsonl b/verifiers/internal/gha/testdata/dsse-v10.0.1-release.intoto.jsonl similarity index 100% rename from verification/testdata/dsse-v10.0.1-release.intoto.jsonl rename to verifiers/internal/gha/testdata/dsse-v10.0.1-release.intoto.jsonl diff --git a/verification/testdata/dsse-valid-multi-subjects.intoto.jsonl b/verifiers/internal/gha/testdata/dsse-valid-multi-subjects.intoto.jsonl similarity index 100% rename from verification/testdata/dsse-valid-multi-subjects.intoto.jsonl rename to verifiers/internal/gha/testdata/dsse-valid-multi-subjects.intoto.jsonl diff --git a/verification/testdata/dsse-valid.intoto.jsonl b/verifiers/internal/gha/testdata/dsse-valid.intoto.jsonl similarity index 100% rename from verification/testdata/dsse-valid.intoto.jsonl rename to verifiers/internal/gha/testdata/dsse-valid.intoto.jsonl diff --git a/verification/testdata/dsse-vslsa1-tag.intoto.jsonl b/verifiers/internal/gha/testdata/dsse-vslsa1-tag.intoto.jsonl similarity index 100% rename from verification/testdata/dsse-vslsa1-tag.intoto.jsonl rename to verifiers/internal/gha/testdata/dsse-vslsa1-tag.intoto.jsonl diff --git a/verifiers/internal/gha/verifier.go b/verifiers/internal/gha/verifier.go new file mode 100644 index 0000000..ee51453 --- /dev/null +++ b/verifiers/internal/gha/verifier.go @@ -0,0 +1,90 @@ +package gha + +import ( + "context" + "encoding/base64" + "fmt" + "os" + "strings" + + "github.com/sigstore/cosign/cmd/cosign/cli/rekor" + + serrors "github.com/slsa-framework/slsa-verifier/errors" + "github.com/slsa-framework/slsa-verifier/options" + "github.com/slsa-framework/slsa-verifier/register" +) + +const VerifierName = "GHA" + +//nolint:gochecknoinits +func init() { + register.RegisterVerifier(VerifierName, GHAVerifierNew()) +} + +type GHAVerifier struct{} + +func GHAVerifierNew() *GHAVerifier { + return &GHAVerifier{} +} + +// IsAuthoritativeFor returns true of the verifier can verify provenance +// generated by the builderID. +func (v *GHAVerifier) IsAuthoritativeFor(builderID string) bool { + // This verifier only supports builders defined on GitHub. + return strings.HasPrefix(builderID, "https://github.com/") +} + +// VerifyArtifact verifies provenance for an artifact. +func (v *GHAVerifier) VerifyArtifact(ctx context.Context, + provenance []byte, artifactHash string, + provenanceOpts *options.ProvenanceOpts, + builderOpts *options.BuilderOpts, +) ([]byte, string, error) { + rClient, err := rekor.NewClient(defaultRekorAddr) + if err != nil { + return nil, "", err + } + + /* Verify signature on the intoto attestation. */ + env, cert, err := VerifyProvenanceSignature(ctx, rClient, provenance, artifactHash) + if err != nil { + return nil, "", err + } + + /* Verify properties of the signing identity. */ + // Get the workflow info given the certificate information. + workflowInfo, err := GetWorkflowInfoFromCertificate(cert) + if err != nil { + return nil, "", err + } + + // Verify the workflow identity. + builderID, err := VerifyWorkflowIdentity(workflowInfo, builderOpts, + provenanceOpts.ExpectedSourceURI) + if err != nil { + return 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 + } + + fmt.Fprintf(os.Stderr, "Verified build using builder https://github.com%s at commit %s\n", + workflowInfo.JobWobWorkflowRef, + workflowInfo.CallerHash) + // Return verified provenance. + r, err := base64.StdEncoding.DecodeString(env.Payload) + return r, builderID, err +} + +// VerifyImage verifies provenance for an OCI image. +func (v *GHAVerifier) VerifyImage(ctx context.Context, + provenance []byte, artifactHash string, + provenanceOpts *options.ProvenanceOpts, + builderOpts *options.BuilderOpts, +) ([]byte, string, error) { + return nil, "todo", serrors.ErrorNotSupported +} diff --git a/verifiers/verifier.go b/verifiers/verifier.go new file mode 100644 index 0000000..d3b7872 --- /dev/null +++ b/verifiers/verifier.go @@ -0,0 +1,36 @@ +package verifiers + +import ( + "context" + "fmt" + + serrors "github.com/slsa-framework/slsa-verifier/errors" + "github.com/slsa-framework/slsa-verifier/options" + "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" +) + +func Verify(ctx context.Context, + provenance []byte, artifactHash string, + provenanceOpts *options.ProvenanceOpts, + builderOpts *options.BuilderOpts, +) ([]byte, string, error) { + // If user provids a builderID, find the right verifier + // based on its ID. + if builderOpts.ExpectedID != nil && + *builderOpts.ExpectedID != "" { + for _, v := range register.SLSAVerifiers { + if v.IsAuthoritativeFor(*builderOpts.ExpectedID) { + return v.VerifyArtifact(ctx, provenance, artifactHash, + provenanceOpts, builderOpts) + } + } + // No builder found. + return nil, "", fmt.Errorf("%w: %s", serrors.ErrorVerifierNotSupported, *builderOpts.ExpectedID) + } + + // By default, try the GHA builders. + return register.SLSAVerifiers[gha.VerifierName].VerifyArtifact(ctx, provenance, artifactHash, + provenanceOpts, builderOpts) +}