From e7fc7a4621f988046b72ddfb34a61b372b348ae2 Mon Sep 17 00:00:00 2001 From: Ian Lewis Date: Tue, 25 Jul 2023 11:29:15 +0900 Subject: [PATCH] feat: Verification for when sha1 is specified in BYOB TRW (#641) Fixes #600 --------- Signed-off-by: Ian Lewis Signed-off-by: laurentsimon <64505099+laurentsimon@users.noreply.github.com> Co-authored-by: laurentsimon <64505099+laurentsimon@users.noreply.github.com> --- errors/errors.go | 1 + verifiers/internal/gha/builder.go | 15 +- verifiers/internal/gha/npm.go | 46 ++- verifiers/internal/gha/npm_test.go | 309 +++++++++++++++--- verifiers/internal/gha/provenance.go | 52 +-- verifiers/internal/gha/provenance_test.go | 70 ++-- .../gha/slsaprovenance/common/buildtypes.go | 30 ++ .../gha/slsaprovenance/iface/provenance.go | 3 + .../gha/slsaprovenance/slsaprovenance.go | 26 +- .../gha/slsaprovenance/slsaprovenance_test.go | 27 +- .../internal/gha/slsaprovenance/v0.2/base.go | 13 + .../internal/gha/slsaprovenance/v0.2/byob.go | 91 ++++++ .../gha/slsaprovenance/v0.2/byob_test.go | 280 ++++++++++++++++ .../gha/slsaprovenance/v0.2/provenance.go | 69 ++-- .../internal/gha/slsaprovenance/v1.0/base.go | 188 +++++++++++ .../internal/gha/slsaprovenance/v1.0/byob.go | 249 +++----------- .../gha/slsaprovenance/v1.0/byob_test.go | 279 ++++++++++++++++ .../slsaprovenance/v1.0/container_based.go | 6 +- .../gha/slsaprovenance/v1.0/provenance.go | 55 +++- .../slsaprovenance/v1.0/provenance_test.go | 118 +++++++ verifiers/internal/gha/verifier.go | 29 +- 21 files changed, 1559 insertions(+), 397 deletions(-) create mode 100644 verifiers/internal/gha/slsaprovenance/common/buildtypes.go create mode 100644 verifiers/internal/gha/slsaprovenance/v0.2/byob.go create mode 100644 verifiers/internal/gha/slsaprovenance/v0.2/byob_test.go create mode 100644 verifiers/internal/gha/slsaprovenance/v1.0/base.go create mode 100644 verifiers/internal/gha/slsaprovenance/v1.0/byob_test.go create mode 100644 verifiers/internal/gha/slsaprovenance/v1.0/provenance_test.go diff --git a/errors/errors.go b/errors/errors.go index cfed1a0a8..8deeea0d5 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -9,6 +9,7 @@ var ( ErrorMismatchPackageName = errors.New("package name does not match provenance") ErrorMismatchBuilderID = errors.New("builderID does not match provenance") ErrorInvalidBuilderID = errors.New("builderID is invalid") + ErrorInvalidBuildType = errors.New("buildType is invalid") ErrorMismatchSource = errors.New("source used to generate the binary does not match provenance") ErrorMismatchWorkflowInputs = errors.New("workflow input does not match") ErrorMalformedURI = errors.New("URI is malformed") diff --git a/verifiers/internal/gha/builder.go b/verifiers/internal/gha/builder.go index 42a38c3d6..062905056 100644 --- a/verifiers/internal/gha/builder.go +++ b/verifiers/internal/gha/builder.go @@ -8,9 +8,10 @@ import ( "strings" fulcio "github.com/sigstore/fulcio/pkg/certificate" + serrors "github.com/slsa-framework/slsa-verifier/v2/errors" "github.com/slsa-framework/slsa-verifier/v2/options" - "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" + ghacommon "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" "github.com/slsa-framework/slsa-verifier/v2/verifiers/utils" ) @@ -27,18 +28,18 @@ var ( ) var defaultArtifactTrustedReusableWorkflows = map[string]bool{ - common.GenericGeneratorBuilderID: true, - common.GoBuilderID: true, - common.ContainerBasedBuilderID: true, + ghacommon.GenericGeneratorBuilderID: true, + ghacommon.GoBuilderID: true, + ghacommon.ContainerBasedBuilderID: true, } var defaultContainerTrustedReusableWorkflows = map[string]bool{ - common.ContainerGeneratorBuilderID: true, + ghacommon.ContainerGeneratorBuilderID: true, } var defaultBYOBReusableWorkflows = map[string]bool{ - common.GenericDelegatorBuilderID: true, - common.GenericLowPermsDelegatorBuilderID: true, + ghacommon.GenericDelegatorBuilderID: true, + ghacommon.GenericLowPermsDelegatorBuilderID: true, } var JReleaserRepository = httpsGithubCom + jReleaserActionRepository diff --git a/verifiers/internal/gha/npm.go b/verifiers/internal/gha/npm.go index 8eb87ac03..1b35b7834 100644 --- a/verifiers/internal/gha/npm.go +++ b/verifiers/internal/gha/npm.go @@ -15,6 +15,7 @@ import ( intoto "github.com/in-toto/in-toto-golang/in_toto" "github.com/secure-systems-lab/go-securesystemslib/dsse" serrors "github.com/slsa-framework/slsa-verifier/v2/errors" + "github.com/slsa-framework/slsa-verifier/v2/options" "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance" "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" "github.com/slsa-framework/slsa-verifier/v2/verifiers/utils" @@ -68,6 +69,7 @@ func (b *BundleBytes) UnmarshalJSON(data []byte) error { type Npm struct { ctx context.Context root *TrustedRoot + verifiedBuilderID *utils.TrustedBuilderID verifiedProvenanceAtt *SignedAttestation verifiedPublishAtt *SignedAttestation provenanceAttestation *attestation @@ -93,8 +95,9 @@ func NpmNew(ctx context.Context, root *TrustedRoot, attestationBytes []byte) (*N return nil, err } return &Npm{ - ctx: ctx, - root: root, + ctx: ctx, + root: root, + provenanceAttestation: prov, publishAttestation: pub, }, nil @@ -251,7 +254,7 @@ func (n *Npm) verifyPackageName(name *string) error { } // Verify subject name in provenance. - if err := verifyProvenanceSubjectName(n.verifiedProvenanceAtt, *name); err != nil { + if err := verifyProvenanceSubjectName(n.verifiedBuilderID, n.verifiedProvenanceAtt, *name); err != nil { return err } @@ -274,7 +277,7 @@ func (n *Npm) verifyPackageVersion(version *string) error { } // Verify subject version in provenance. - if err := verifyProvenanceSubjectVersion(n.verifiedProvenanceAtt, *version); err != nil { + if err := verifyProvenanceSubjectVersion(n.verifiedBuilderID, n.verifiedProvenanceAtt, *version); err != nil { return err } @@ -291,6 +294,25 @@ func (n *Npm) verifyPackageVersion(version *string) error { return nil } +func (n *Npm) verifyBuilderID( + provenanceOpts *options.ProvenanceOpts, + builderOpts *options.BuilderOpts, + defaultBuilders map[string]bool, +) (*utils.TrustedBuilderID, error) { + // Verify certificate information. + builder, err := verifyNpmEnvAndCert( + n.ProvenanceEnvelope(), + n.ProvenanceLeafCertificate(), + provenanceOpts, builderOpts, + defaultBuilders, + ) + if err != nil { + return nil, err + } + n.verifiedBuilderID = builder + return builder, err +} + func verifyPublishPredicateVersion(att *SignedAttestation, expectedVersion string) error { _, version, err := getPublishPredicateData(att) if err != nil { @@ -336,8 +358,8 @@ func getPublishPredicateData(att *SignedAttestation) (string, string, error) { return statement.Predicate.Name, statement.Predicate.Version, nil } -func verifyProvenanceSubjectVersion(att *SignedAttestation, expectedVersion string) error { - subject, err := getSubject(att) +func verifyProvenanceSubjectVersion(b *utils.TrustedBuilderID, att *SignedAttestation, expectedVersion string) error { + subject, err := getSubject(b, att) if err != nil { return err } @@ -378,15 +400,15 @@ func verifyPublishSubjectName(att *SignedAttestation, expectedName string) error return verifyName(name, expectedName) } -func verifyProvenanceSubjectName(att *SignedAttestation, expectedName string) error { - prov, err := slsaprovenance.ProvenanceFromEnvelope(att.Envelope) +func verifyProvenanceSubjectName(b *utils.TrustedBuilderID, att *SignedAttestation, expectedName string) error { + prov, err := slsaprovenance.ProvenanceFromEnvelope(b.Name(), att.Envelope) if err != nil { - return nil + return fmt.Errorf("reading provenance: %w", err) } subjects, err := prov.Subjects() if err != nil { - return fmt.Errorf("%w", serrors.ErrorInvalidDssePayload) + return fmt.Errorf("%w: %w", serrors.ErrorInvalidDssePayload, err) } if len(subjects) != 1 { return fmt.Errorf("%w: expected 1 subject, got %v", serrors.ErrorInvalidDssePayload, len(subjects)) @@ -443,8 +465,8 @@ func getPackageNameAndVersion(name string) (string, string, error) { return pkgname, pkgtag, nil } -func getSubject(att *SignedAttestation) (string, error) { - prov, err := slsaprovenance.ProvenanceFromEnvelope(att.Envelope) +func getSubject(b *utils.TrustedBuilderID, att *SignedAttestation) (string, error) { + prov, err := slsaprovenance.ProvenanceFromEnvelope(b.Name(), att.Envelope) if err != nil { return "", err } diff --git a/verifiers/internal/gha/npm_test.go b/verifiers/internal/gha/npm_test.go index 14788b3be..ddcd0b014 100644 --- a/verifiers/internal/gha/npm_test.go +++ b/verifiers/internal/gha/npm_test.go @@ -2,17 +2,20 @@ package gha import ( "context" + "encoding/base64" "fmt" "os" "path/filepath" "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" intoto "github.com/in-toto/in-toto-golang/in_toto" dsselib "github.com/secure-systems-lab/go-securesystemslib/dsse" serrors "github.com/slsa-framework/slsa-verifier/v2/errors" "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/utils" ) func Test_verifyName(t *testing.T) { @@ -81,7 +84,15 @@ func Test_verifyPublishSubjectVersion(t *testing.T) { att: &SignedAttestation{ Envelope: &dsselib.Envelope{ PayloadType: "application/vnd.in-toto+json", - Payload: "ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YwLjEiLAogICJwcmVkaWNhdGVUeXBlIjogImh0dHBzOi8vZ2l0aHViLmNvbS9ucG0vYXR0ZXN0YXRpb24vdHJlZS9tYWluL3NwZWNzL3B1Ymxpc2gvdjAuMSIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJuYW1lIjogIkBsYXVyZW50c2ltb24vcHJvdmVuYW5jZS1ucG0tdGVzdCIsCiAgICAidmVyc2lvbiI6ICIxLjAuMCIsCiAgICAicmVnaXN0cnkiOiAiaHR0cHM6Ly9yZWdpc3RyeS5ucG1qcy5vcmciCiAgfQp9Cg==", + Payload: base64.StdEncoding.EncodeToString([]byte(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://github.com/npm/attestation/tree/main/specs/publish/v0.1", + "predicate": { + "name": "@laurentsimon/provenance-npm-test", + "version": "1.0.0", + "registry": "https://registry.npmjs.org" + } + }`)), }, }, version: "1.0.0", @@ -91,7 +102,15 @@ func Test_verifyPublishSubjectVersion(t *testing.T) { att: &SignedAttestation{ Envelope: &dsselib.Envelope{ PayloadType: "application/vnd.in-toto+json", - Payload: "ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YwLjEiLAogICJwcmVkaWNhdGVUeXBlIjogImh0dHBzOi8vZ2l0aHViLmNvbS9ucG0vYXR0ZXN0YXRpb24vdHJlZS9tYWluL3NwZWNzL3B1Ymxpc2gvdjAuMSIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJuYW1lIjogIkBsYXVyZW50c2ltb24vcHJvdmVuYW5jZS1ucG0tdGVzdCIsCiAgICAidmVyc2lvbiI6ICIxLjAuMCIsCiAgICAicmVnaXN0cnkiOiAiaHR0cHM6Ly9yZWdpc3RyeS5ucG1qcy5vcmciCiAgfQp9Cg==", + Payload: base64.StdEncoding.EncodeToString([]byte(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://github.com/npm/attestation/tree/main/specs/publish/v0.1", + "predicate": { + "name": "@laurentsimon/provenance-npm-test", + "version": "1.0.0", + "registry": "https://registry.npmjs.org" + } + }`)), }, }, version: "1.0", @@ -102,7 +121,15 @@ func Test_verifyPublishSubjectVersion(t *testing.T) { att: &SignedAttestation{ Envelope: &dsselib.Envelope{ PayloadType: "application/vnd.in-toto+json", - Payload: "ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YwLjEiLAogICJwcmVkaWNhdGVUeXBlIjogImh0dHBzOi8vZ2l0aHViLmNvbS9ucG0vYXR0ZXN0YXRpb24vdHJlZS9tYWluL3NwZWNzL3B1Ymxpc2gvdjAuMSIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJuYW1lIjogIkBsYXVyZW50c2ltb24vcHJvdmVuYW5jZS1ucG0tdGVzdCIsCiAgICAidmVyc2lvbiI6ICIxLjAuMCIsCiAgICAicmVnaXN0cnkiOiAiaHR0cHM6Ly9yZWdpc3RyeS5ucG1qcy5vcmciCiAgfQp9Cg==", + Payload: base64.StdEncoding.EncodeToString([]byte(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://github.com/npm/attestation/tree/main/specs/publish/v0.1", + "predicate": { + "name": "@laurentsimon/provenance-npm-test", + "version": "1.0.0", + "registry": "https://registry.npmjs.org" + } + }`)), }, }, version: "1.0.1", @@ -113,7 +140,15 @@ func Test_verifyPublishSubjectVersion(t *testing.T) { att: &SignedAttestation{ Envelope: &dsselib.Envelope{ PayloadType: "application/vnd.in-toto+json", - Payload: "ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YwLjEiLAogICJwcmVkaWNhdGVUeXBlIjogImh0dHBzOi8vZ2l0aHViLmNvbS9ucG0vYXR0ZXN0YXRpb24vdHJlZS9tYWluL3NwZWNzL3B1Ymxpc2gvdjAuMSIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJuYW1lIjogIkBsYXVyZW50c2ltb24vcHJvdmVuYW5jZS1ucG0tdGVzdCIsCiAgICAidmVyc2lvbiI6ICIxLjAuMCIsCiAgICAicmVnaXN0cnkiOiAiaHR0cHM6Ly9yZWdpc3RyeS5ucG1qcy5vcmciCiAgfQp9Cg==", + Payload: base64.StdEncoding.EncodeToString([]byte(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://github.com/npm/attestation/tree/main/specs/publish/v0.1", + "predicate": { + "name": "@laurentsimon/provenance-npm-test", + "version": "1.0.0", + "registry": "https://registry.npmjs.org" + } + }`)), }, }, version: "1.1.0", @@ -124,7 +159,15 @@ func Test_verifyPublishSubjectVersion(t *testing.T) { att: &SignedAttestation{ Envelope: &dsselib.Envelope{ PayloadType: "application/vnd.in-toto+json", - Payload: "ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YwLjEiLAogICJwcmVkaWNhdGVUeXBlIjogImh0dHBzOi8vZ2l0aHViLmNvbS9ucG0vYXR0ZXN0YXRpb24vdHJlZS9tYWluL3NwZWNzL3B1Ymxpc2gvdjAuMSIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJuYW1lIjogIkBsYXVyZW50c2ltb24vcHJvdmVuYW5jZS1ucG0tdGVzdCIsCiAgICAidmVyc2lvbiI6ICIxLjAuMCIsCiAgICAicmVnaXN0cnkiOiAiaHR0cHM6Ly9yZWdpc3RyeS5ucG1qcy5vcmciCiAgfQp9Cg==", + Payload: base64.StdEncoding.EncodeToString([]byte(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://github.com/npm/attestation/tree/main/specs/publish/v0.1", + "predicate": { + "name": "@laurentsimon/provenance-npm-test", + "version": "1.0.0", + "registry": "https://registry.npmjs.org" + } + }`)), }, }, version: "2.0.0", @@ -138,8 +181,8 @@ func Test_verifyPublishSubjectVersion(t *testing.T) { err := verifyPublishSubjectVersion(tt.att, tt.version) - if !errCmp(err, tt.err) { - t.Errorf(cmp.Diff(err, tt.err)) + if diff := cmp.Diff(tt.err, err, cmpopts.EquateErrors()); diff != "" { + t.Fatalf("unexpected error (-want +got): \n%s", diff) } }) } @@ -149,60 +192,151 @@ func Test_verifyProvenanceSubjectVersion(t *testing.T) { t.Parallel() tests := []struct { - name string - att *SignedAttestation - version string - err error + name string + builderID string + att *SignedAttestation + version string + err error }{ { - name: "correct version", + name: "correct version", + builderID: common.NpmCLIHostedBuilderID, att: &SignedAttestation{ Envelope: &dsselib.Envelope{ PayloadType: "application/vnd.in-toto+json", - Payload: "ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YwLjEiLAogICJzdWJqZWN0IjogWwogICAgewogICAgICAibmFtZSI6ICJwa2c6bnBtLyU0MGxhdXJlbnRzaW1vbi9wcm92ZW5hbmNlLW5wbS10ZXN0QDEuMC4wIiwKICAgICAgImRpZ2VzdCI6IHsKICAgICAgICAic2hhNTEyIjogIjI5ZDE5ZjI2MjMzZjQ0NDEzMjg0MTJiMzRmZDczZWQxMDRlY2ZlZjYyZjE0MDk3ODkwY2NjZjc0NTViNTIxYjY1YzVhY2ZmODUxODQ5ZmFhODVjODUzOTVhYTIyZDQwMTQzNmYwMWYzYWZiNjFiMTljNzgwZTkwNmM4OGM3ZjIwIgogICAgICB9CiAgICB9CiAgXSwKICAicHJlZGljYXRlVHlwZSI6ICJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJidWlsZFR5cGUiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGkvZ2hhQHYxIiwKICAgICJidWlsZGVyIjogewogICAgICAiaWQiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGlAOS41LjAiCiAgICB9CiAgfQp9Cg==", + Payload: base64.StdEncoding.EncodeToString([]byte(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [ + { + "name": "pkg:npm/%40laurentsimon/provenance-npm-test@1.0.0", + "digest": { + "sha512": "29d19f26233f4441328412b34fd73ed104ecfef62f14097890cccf7455b521b65c5acff851849faa85c85395aa22d401436f01f3afb61b19c780e906c88c7f20" + } + } + ], + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicate": { + "buildType": "https://github.com/npm/cli/gha@v1", + "builder": { + "id": "https://github.com/npm/cli@9.5.0" + } + } + }`)), }, }, version: "1.0.0", }, { - name: "incorrect subset version", + name: "incorrect subset version", + builderID: common.NpmCLIHostedBuilderID, att: &SignedAttestation{ Envelope: &dsselib.Envelope{ PayloadType: "application/vnd.in-toto+json", - Payload: "ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YwLjEiLAogICJzdWJqZWN0IjogWwogICAgewogICAgICAibmFtZSI6ICJwa2c6bnBtLyU0MGxhdXJlbnRzaW1vbi9wcm92ZW5hbmNlLW5wbS10ZXN0QDEuMC4wIiwKICAgICAgImRpZ2VzdCI6IHsKICAgICAgICAic2hhNTEyIjogIjI5ZDE5ZjI2MjMzZjQ0NDEzMjg0MTJiMzRmZDczZWQxMDRlY2ZlZjYyZjE0MDk3ODkwY2NjZjc0NTViNTIxYjY1YzVhY2ZmODUxODQ5ZmFhODVjODUzOTVhYTIyZDQwMTQzNmYwMWYzYWZiNjFiMTljNzgwZTkwNmM4OGM3ZjIwIgogICAgICB9CiAgICB9CiAgXSwKICAicHJlZGljYXRlVHlwZSI6ICJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJidWlsZFR5cGUiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGkvZ2hhQHYxIiwKICAgICJidWlsZGVyIjogewogICAgICAiaWQiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGlAOS41LjAiCiAgICB9CiAgfQp9Cg==", + Payload: base64.StdEncoding.EncodeToString([]byte(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [ + { + "name": "pkg:npm/%40laurentsimon/provenance-npm-test@1.0.0", + "digest": { + "sha512": "29d19f26233f4441328412b34fd73ed104ecfef62f14097890cccf7455b521b65c5acff851849faa85c85395aa22d401436f01f3afb61b19c780e906c88c7f20" + } + } + ], + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicate": { + "buildType": "https://github.com/npm/cli/gha@v1", + "builder": { + "id": "https://github.com/npm/cli@9.5.0" + } + } + }`)), }, }, version: "1.0", err: serrors.ErrorMismatchPackageVersion, }, { - name: "incorrect patch version", + name: "incorrect patch version", + builderID: common.NpmCLIHostedBuilderID, att: &SignedAttestation{ Envelope: &dsselib.Envelope{ PayloadType: "application/vnd.in-toto+json", - Payload: "ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YwLjEiLAogICJzdWJqZWN0IjogWwogICAgewogICAgICAibmFtZSI6ICJwa2c6bnBtLyU0MGxhdXJlbnRzaW1vbi9wcm92ZW5hbmNlLW5wbS10ZXN0QDEuMC4wIiwKICAgICAgImRpZ2VzdCI6IHsKICAgICAgICAic2hhNTEyIjogIjI5ZDE5ZjI2MjMzZjQ0NDEzMjg0MTJiMzRmZDczZWQxMDRlY2ZlZjYyZjE0MDk3ODkwY2NjZjc0NTViNTIxYjY1YzVhY2ZmODUxODQ5ZmFhODVjODUzOTVhYTIyZDQwMTQzNmYwMWYzYWZiNjFiMTljNzgwZTkwNmM4OGM3ZjIwIgogICAgICB9CiAgICB9CiAgXSwKICAicHJlZGljYXRlVHlwZSI6ICJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJidWlsZFR5cGUiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGkvZ2hhQHYxIiwKICAgICJidWlsZGVyIjogewogICAgICAiaWQiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGlAOS41LjAiCiAgICB9CiAgfQp9Cg==", + Payload: base64.StdEncoding.EncodeToString([]byte(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [ + { + "name": "pkg:npm/%40laurentsimon/provenance-npm-test@1.0.0", + "digest": { + "sha512": "29d19f26233f4441328412b34fd73ed104ecfef62f14097890cccf7455b521b65c5acff851849faa85c85395aa22d401436f01f3afb61b19c780e906c88c7f20" + } + } + ], + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicate": { + "buildType": "https://github.com/npm/cli/gha@v1", + "builder": { + "id": "https://github.com/npm/cli@9.5.0" + } + } + }`)), }, }, version: "1.0.1", err: serrors.ErrorMismatchPackageVersion, }, { - name: "incorrect minor version", + name: "incorrect minor version", + builderID: common.NpmCLIHostedBuilderID, att: &SignedAttestation{ Envelope: &dsselib.Envelope{ PayloadType: "application/vnd.in-toto+json", - Payload: "ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YwLjEiLAogICJzdWJqZWN0IjogWwogICAgewogICAgICAibmFtZSI6ICJwa2c6bnBtLyU0MGxhdXJlbnRzaW1vbi9wcm92ZW5hbmNlLW5wbS10ZXN0QDEuMC4wIiwKICAgICAgImRpZ2VzdCI6IHsKICAgICAgICAic2hhNTEyIjogIjI5ZDE5ZjI2MjMzZjQ0NDEzMjg0MTJiMzRmZDczZWQxMDRlY2ZlZjYyZjE0MDk3ODkwY2NjZjc0NTViNTIxYjY1YzVhY2ZmODUxODQ5ZmFhODVjODUzOTVhYTIyZDQwMTQzNmYwMWYzYWZiNjFiMTljNzgwZTkwNmM4OGM3ZjIwIgogICAgICB9CiAgICB9CiAgXSwKICAicHJlZGljYXRlVHlwZSI6ICJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJidWlsZFR5cGUiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGkvZ2hhQHYxIiwKICAgICJidWlsZGVyIjogewogICAgICAiaWQiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGlAOS41LjAiCiAgICB9CiAgfQp9Cg==", + Payload: base64.StdEncoding.EncodeToString([]byte(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [ + { + "name": "pkg:npm/%40laurentsimon/provenance-npm-test@1.0.0", + "digest": { + "sha512": "29d19f26233f4441328412b34fd73ed104ecfef62f14097890cccf7455b521b65c5acff851849faa85c85395aa22d401436f01f3afb61b19c780e906c88c7f20" + } + } + ], + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicate": { + "buildType": "https://github.com/npm/cli/gha@v1", + "builder": { + "id": "https://github.com/npm/cli@9.5.0" + } + } + }`)), }, }, version: "1.1.0", err: serrors.ErrorMismatchPackageVersion, }, { - name: "incorrect major version", + name: "incorrect major version", + builderID: common.NpmCLIHostedBuilderID, att: &SignedAttestation{ Envelope: &dsselib.Envelope{ PayloadType: "application/vnd.in-toto+json", - Payload: "ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YwLjEiLAogICJzdWJqZWN0IjogWwogICAgewogICAgICAibmFtZSI6ICJwa2c6bnBtLyU0MGxhdXJlbnRzaW1vbi9wcm92ZW5hbmNlLW5wbS10ZXN0QDEuMC4wIiwKICAgICAgImRpZ2VzdCI6IHsKICAgICAgICAic2hhNTEyIjogIjI5ZDE5ZjI2MjMzZjQ0NDEzMjg0MTJiMzRmZDczZWQxMDRlY2ZlZjYyZjE0MDk3ODkwY2NjZjc0NTViNTIxYjY1YzVhY2ZmODUxODQ5ZmFhODVjODUzOTVhYTIyZDQwMTQzNmYwMWYzYWZiNjFiMTljNzgwZTkwNmM4OGM3ZjIwIgogICAgICB9CiAgICB9CiAgXSwKICAicHJlZGljYXRlVHlwZSI6ICJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJidWlsZFR5cGUiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGkvZ2hhQHYxIiwKICAgICJidWlsZGVyIjogewogICAgICAiaWQiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGlAOS41LjAiCiAgICB9CiAgfQp9Cg==", + Payload: base64.StdEncoding.EncodeToString([]byte(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [ + { + "name": "pkg:npm/%40laurentsimon/provenance-npm-test@1.0.0", + "digest": { + "sha512": "29d19f26233f4441328412b34fd73ed104ecfef62f14097890cccf7455b521b65c5acff851849faa85c85395aa22d401436f01f3afb61b19c780e906c88c7f20" + } + } + ], + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicate": { + "buildType": "https://github.com/npm/cli/gha@v1", + "builder": { + "id": "https://github.com/npm/cli@9.5.0" + } + } + }`)), }, }, version: "2.0.0", @@ -213,11 +347,14 @@ func Test_verifyProvenanceSubjectVersion(t *testing.T) { 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 := utils.TrustedBuilderIDNew(tt.builderID, false) + if err != nil { + panic(err) + } - err := verifyProvenanceSubjectVersion(tt.att, tt.version) - - if !errCmp(err, tt.err) { - t.Errorf(cmp.Diff(err, tt.err)) + err = verifyProvenanceSubjectVersion(builderID, tt.att, tt.version) + if diff := cmp.Diff(tt.err, err, cmpopts.EquateErrors()); diff != "" { + t.Fatalf("unexpected error (-want +got): \n%s", diff) } }) } @@ -439,49 +576,122 @@ func Test_verifyProvenanceSubjectName(t *testing.T) { t.Parallel() tests := []struct { - name string - att *SignedAttestation - subject string - err error + name string + builderID string + att *SignedAttestation + subject string + err error }{ { - name: "correct name", + name: "correct name", + builderID: common.NpmCLIHostedBuilderID, att: &SignedAttestation{ Envelope: &dsselib.Envelope{ PayloadType: "application/vnd.in-toto+json", - Payload: "ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YwLjEiLAogICJzdWJqZWN0IjogWwogICAgewogICAgICAibmFtZSI6ICJwa2c6bnBtLyU0MGxhdXJlbnRzaW1vbi9wcm92ZW5hbmNlLW5wbS10ZXN0QDEuMC4wIiwKICAgICAgImRpZ2VzdCI6IHsKICAgICAgICAic2hhNTEyIjogIjI5ZDE5ZjI2MjMzZjQ0NDEzMjg0MTJiMzRmZDczZWQxMDRlY2ZlZjYyZjE0MDk3ODkwY2NjZjc0NTViNTIxYjY1YzVhY2ZmODUxODQ5ZmFhODVjODUzOTVhYTIyZDQwMTQzNmYwMWYzYWZiNjFiMTljNzgwZTkwNmM4OGM3ZjIwIgogICAgICB9CiAgICB9CiAgXSwKICAicHJlZGljYXRlVHlwZSI6ICJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJidWlsZFR5cGUiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGkvZ2hhQHYxIiwKICAgICJidWlsZGVyIjogewogICAgICAiaWQiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGlAOS41LjAiCiAgICB9CiAgfQp9Cg==", + Payload: base64.StdEncoding.EncodeToString([]byte(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [ + { + "name": "pkg:npm/%40laurentsimon/provenance-npm-test@1.0.0", + "digest": { + "sha512": "29d19f26233f4441328412b34fd73ed104ecfef62f14097890cccf7455b521b65c5acff851849faa85c85395aa22d401436f01f3afb61b19c780e906c88c7f20" + } + } + ], + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicate": { + "buildType": "https://github.com/npm/cli/gha@v1", + "builder": { + "id": "https://github.com/npm/cli@9.5.0" + } + } + }`)), }, }, subject: "@laurentsimon/provenance-npm-test", }, { - name: "incorrect name", + name: "incorrect name", + builderID: common.NpmCLIHostedBuilderID, att: &SignedAttestation{ Envelope: &dsselib.Envelope{ PayloadType: "application/vnd.in-toto+json", - Payload: "ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YwLjEiLAogICJzdWJqZWN0IjogWwogICAgewogICAgICAibmFtZSI6ICJwa2c6bnBtLyU0MGxhdXJlbnRzaW1vbi9wcm92ZW5hbmNlLW5wbS10ZXN0QDEuMC4wIiwKICAgICAgImRpZ2VzdCI6IHsKICAgICAgICAic2hhNTEyIjogIjI5ZDE5ZjI2MjMzZjQ0NDEzMjg0MTJiMzRmZDczZWQxMDRlY2ZlZjYyZjE0MDk3ODkwY2NjZjc0NTViNTIxYjY1YzVhY2ZmODUxODQ5ZmFhODVjODUzOTVhYTIyZDQwMTQzNmYwMWYzYWZiNjFiMTljNzgwZTkwNmM4OGM3ZjIwIgogICAgICB9CiAgICB9CiAgXSwKICAicHJlZGljYXRlVHlwZSI6ICJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJidWlsZFR5cGUiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGkvZ2hhQHYxIiwKICAgICJidWlsZGVyIjogewogICAgICAiaWQiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGlAOS41LjAiCiAgICB9CiAgfQp9Cg==", + Payload: base64.StdEncoding.EncodeToString([]byte(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [ + { + "name": "pkg:npm/%40laurentsimon/provenance-npm-test@1.0.0", + "digest": { + "sha512": "29d19f26233f4441328412b34fd73ed104ecfef62f14097890cccf7455b521b65c5acff851849faa85c85395aa22d401436f01f3afb61b19c780e906c88c7f20" + } + } + ], + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicate": { + "buildType": "https://github.com/npm/cli/gha@v1", + "builder": { + "id": "https://github.com/npm/cli@9.5.0" + } + } + }`)), }, }, subject: "wrong name", err: serrors.ErrorMismatchPackageName, }, { - name: "incorrect scope", + name: "incorrect scope", + builderID: common.NpmCLIHostedBuilderID, att: &SignedAttestation{ Envelope: &dsselib.Envelope{ PayloadType: "application/vnd.in-toto+json", - Payload: "ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YwLjEiLAogICJzdWJqZWN0IjogWwogICAgewogICAgICAibmFtZSI6ICJwa2c6bnBtLyU0MGxhdXJlbnRzaW1vbi9wcm92ZW5hbmNlLW5wbS10ZXN0QDEuMC4wIiwKICAgICAgImRpZ2VzdCI6IHsKICAgICAgICAic2hhNTEyIjogIjI5ZDE5ZjI2MjMzZjQ0NDEzMjg0MTJiMzRmZDczZWQxMDRlY2ZlZjYyZjE0MDk3ODkwY2NjZjc0NTViNTIxYjY1YzVhY2ZmODUxODQ5ZmFhODVjODUzOTVhYTIyZDQwMTQzNmYwMWYzYWZiNjFiMTljNzgwZTkwNmM4OGM3ZjIwIgogICAgICB9CiAgICB9CiAgXSwKICAicHJlZGljYXRlVHlwZSI6ICJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJidWlsZFR5cGUiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGkvZ2hhQHYxIiwKICAgICJidWlsZGVyIjogewogICAgICAiaWQiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGlAOS41LjAiCiAgICB9CiAgfQp9Cg==", + Payload: base64.StdEncoding.EncodeToString([]byte(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [ + { + "name": "pkg:npm/%40laurentsimon/provenance-npm-test@1.0.0", + "digest": { + "sha512": "29d19f26233f4441328412b34fd73ed104ecfef62f14097890cccf7455b521b65c5acff851849faa85c85395aa22d401436f01f3afb61b19c780e906c88c7f20" + } + } + ], + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicate": { + "buildType": "https://github.com/npm/cli/gha@v1", + "builder": { + "id": "https://github.com/npm/cli@9.5.0" + } + } + }`)), }, }, subject: "laurentsimon/provenance-npm-test", err: serrors.ErrorMismatchPackageName, }, { - name: "incorrect with version", + name: "incorrect with version", + builderID: common.NpmCLIHostedBuilderID, att: &SignedAttestation{ Envelope: &dsselib.Envelope{ PayloadType: "application/vnd.in-toto+json", - Payload: "ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YwLjEiLAogICJzdWJqZWN0IjogWwogICAgewogICAgICAibmFtZSI6ICJwa2c6bnBtLyU0MGxhdXJlbnRzaW1vbi9wcm92ZW5hbmNlLW5wbS10ZXN0QDEuMC4wIiwKICAgICAgImRpZ2VzdCI6IHsKICAgICAgICAic2hhNTEyIjogIjI5ZDE5ZjI2MjMzZjQ0NDEzMjg0MTJiMzRmZDczZWQxMDRlY2ZlZjYyZjE0MDk3ODkwY2NjZjc0NTViNTIxYjY1YzVhY2ZmODUxODQ5ZmFhODVjODUzOTVhYTIyZDQwMTQzNmYwMWYzYWZiNjFiMTljNzgwZTkwNmM4OGM3ZjIwIgogICAgICB9CiAgICB9CiAgXSwKICAicHJlZGljYXRlVHlwZSI6ICJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJidWlsZFR5cGUiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGkvZ2hhQHYxIiwKICAgICJidWlsZGVyIjogewogICAgICAiaWQiOiAiaHR0cHM6Ly9naXRodWIuY29tL25wbS9jbGlAOS41LjAiCiAgICB9CiAgfQp9Cg==", + Payload: base64.StdEncoding.EncodeToString([]byte(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [ + { + "name": "pkg:npm/%40laurentsimon/provenance-npm-test@1.0.0", + "digest": { + "sha512": "29d19f26233f4441328412b34fd73ed104ecfef62f14097890cccf7455b521b65c5acff851849faa85c85395aa22d401436f01f3afb61b19c780e906c88c7f20" + } + } + ], + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicate": { + "buildType": "https://github.com/npm/cli/gha@v1", + "builder": { + "id": "https://github.com/npm/cli@9.5.0" + } + } + }`)), }, }, subject: "@laurentsimon/provenance-npm-test@1.0.0", @@ -492,11 +702,14 @@ func Test_verifyProvenanceSubjectName(t *testing.T) { 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 := utils.TrustedBuilderIDNew(tt.builderID, false) + if err != nil { + panic(err) + } - err := verifyProvenanceSubjectName(tt.att, tt.subject) - - if !errCmp(err, tt.err) { - t.Errorf(cmp.Diff(err, tt.err)) + err = verifyProvenanceSubjectName(builderID, tt.att, tt.subject) + if diff := cmp.Diff(tt.err, err, cmpopts.EquateErrors()); diff != "" { + t.Fatalf("unexpected error (-want +got): \n%s", diff) } }) } @@ -572,6 +785,11 @@ func Test_verifyPackageName(t *testing.T) { Envelope: env, } + npm.verifiedBuilderID, err = utils.TrustedBuilderIDNew(common.NpmCLIHostedBuilderID, false) + if err != nil { + panic(err) + } + err = npm.verifyPackageName(&tt.subject) if !errCmp(err, tt.err) { t.Errorf(cmp.Diff(err, tt.err)) @@ -642,6 +860,11 @@ func Test_verifyPackageVersion(t *testing.T) { Envelope: env, } + npm.verifiedBuilderID, err = utils.TrustedBuilderIDNew(common.NpmCLIHostedBuilderID, false) + if err != nil { + panic(err) + } + env, err = getEnvelopeFromBundleBytes(npm.publishAttestation.BundleBytes) if err != nil { panic(fmt.Errorf("getEnvelopeFromBundleBytes: %w", err)) @@ -886,8 +1109,8 @@ func Test_NpmNew(t *testing.T) { } _, err = NpmNew(ctx, trustedRoot, content) - if !errCmp(err, tt.err) { - t.Errorf(cmp.Diff(err, tt.err)) + if diff := cmp.Diff(tt.err, err, cmpopts.EquateErrors()); diff != "" { + t.Fatalf("unexpected error (-want +got): \n%s", diff) } }) } diff --git a/verifiers/internal/gha/provenance.go b/verifiers/internal/gha/provenance.go index 21797b6eb..e31c8c625 100644 --- a/verifiers/internal/gha/provenance.go +++ b/verifiers/internal/gha/provenance.go @@ -77,7 +77,7 @@ func verifyBuilderIDLooseMatch(prov iface.Provenance, expectedBuilderID string) } // Verify source URI in provenance statement. -func verifySourceURI(prov iface.Provenance, expectedSourceURI string, allowNoMaterialRef bool) error { +func verifySourceURI(prov iface.Provenance, expectedSourceURI string) error { source := utils.NormalizeGitURI(expectedSourceURI) // We expect github.com URIs only. @@ -97,7 +97,7 @@ func verifySourceURI(prov iface.Provenance, expectedSourceURI string, allowNoMat return err } if triggerURI != source { - return fmt.Errorf("%w: expected source '%s' in configSource.uri, got %q", serrors.ErrorMismatchSource, + return fmt.Errorf("%w: expected trigger %q to match source-uri %q", serrors.ErrorMismatchSource, source, fullTriggerURI) } // We expect the trigger URI to always have a ref. @@ -116,22 +116,37 @@ func verifySourceURI(prov iface.Provenance, expectedSourceURI string, allowNoMat return err } if sourceURI != source { - return fmt.Errorf("%w: expected source '%s' in material section, got %q", serrors.ErrorMismatchSource, - source, fullSourceURI) + return fmt.Errorf("%w: expected source %q to match source-uri %q", serrors.ErrorMismatchSource, + fullSourceURI, source) } + buildType, err := prov.BuildType() + if err != nil { + return fmt.Errorf("checking buildType: %v", err) + } if sourceRef == "" { - if allowNoMaterialRef { - // NOTE: this is an exception for npm packages built before GA, - // see https://github.com/slsa-framework/slsa-verifier/issues/492. - // We don't need to compare the ref since materialSourceURI does not contain it. + // NOTE: this is an exception for npm packages built before GA, + // see https://github.com/slsa-framework/slsa-verifier/issues/492. + // We don't need to compare the ref since materialSourceURI does not contain it. + if buildType == common.NpmCLIBuildTypeV1 { + return nil + } + // NOTE: BYOB builders can build from a different ref than the triggering ref. + // This most often happens when a TRW makes a commit as part of the release process. + // NOTE: Currently only building from a different git sha is supported + // which means the sourceRef is empty. Building from an arbitrary ref + // is currently not supported. + if buildType == common.BYOBBuildTypeV0 { return nil } return fmt.Errorf("%w: missing ref: %q", serrors.ErrorMalformedURI, fullSourceURI) } + // Last, verify that both fields match. We expect that the trigger URI and + // the source URI match but the ref used to trigger the build and source ref + // could be different. if fullTriggerURI != fullSourceURI { - return fmt.Errorf("%w: material and config URIs do not match: %q != %q", + return fmt.Errorf("%w: source and trigger URIs do not match: %q != %q", serrors.ErrorInvalidDssePayload, fullTriggerURI, fullSourceURI) } @@ -191,9 +206,9 @@ func VerifyProvenanceSignature(ctx context.Context, trustedRoot *TrustedRoot, // VerifyNpmPackageProvenance verifies provenance for an npm package. func VerifyNpmPackageProvenance(env *dsselib.Envelope, workflow *WorkflowIdentity, - provenanceOpts *options.ProvenanceOpts, isTrustedBuilder bool, + provenanceOpts *options.ProvenanceOpts, trustedBuilderID *utils.TrustedBuilderID, isTrustedBuilder bool, ) error { - prov, err := slsaprovenance.ProvenanceFromEnvelope(env) + prov, err := slsaprovenance.ProvenanceFromEnvelope(trustedBuilderID.Name(), env) if err != nil { return err } @@ -239,7 +254,7 @@ func VerifyNpmPackageProvenance(env *dsselib.Envelope, workflow *WorkflowIdentit } // Also, the GitHub context is not recorded for the default builder. - if err := VerifyProvenanceCommonOptions(prov, provenanceOpts, true); err != nil { + if err := VerifyProvenanceCommonOptions(prov, provenanceOpts); err != nil { return err } @@ -271,9 +286,8 @@ func isValidDelegatorBuilderID(prov iface.Provenance) error { } // VerifyProvenance verifies the provenance for the given DSSE envelope. -func VerifyProvenance(env *dsselib.Envelope, provenanceOpts *options.ProvenanceOpts, byob bool, -) error { - prov, err := slsaprovenance.ProvenanceFromEnvelope(env) +func VerifyProvenance(env *dsselib.Envelope, provenanceOpts *options.ProvenanceOpts, trustedBuilderID *utils.TrustedBuilderID, byob bool) error { + prov, err := slsaprovenance.ProvenanceFromEnvelope(trustedBuilderID.Name(), env) if err != nil { return err } @@ -295,15 +309,13 @@ func VerifyProvenance(env *dsselib.Envelope, provenanceOpts *options.ProvenanceO } } - return VerifyProvenanceCommonOptions(prov, provenanceOpts, false) + return VerifyProvenanceCommonOptions(prov, provenanceOpts) } // VerifyProvenanceCommonOptions verifies the given provenance. -func VerifyProvenanceCommonOptions(prov iface.Provenance, provenanceOpts *options.ProvenanceOpts, - allowNoMaterialRef bool, -) error { +func VerifyProvenanceCommonOptions(prov iface.Provenance, provenanceOpts *options.ProvenanceOpts) error { // Verify source. - if err := verifySourceURI(prov, provenanceOpts.ExpectedSourceURI, allowNoMaterialRef); err != nil { + if err := verifySourceURI(prov, provenanceOpts.ExpectedSourceURI); err != nil { return err } diff --git a/verifiers/internal/gha/provenance_test.go b/verifiers/internal/gha/provenance_test.go index 8be35ff6a..bdbe2d395 100644 --- a/verifiers/internal/gha/provenance_test.go +++ b/verifiers/internal/gha/provenance_test.go @@ -6,16 +6,18 @@ import ( "github.com/google/go-cmp/cmp" intoto "github.com/in-toto/in-toto-golang/in_toto" - "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" + slsacommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" serrors "github.com/slsa-framework/slsa-verifier/v2/errors" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/iface" ) type testProvenance struct { builderID string + buildType string sourceURI string triggerURI string subjects []intoto.Subject @@ -31,6 +33,7 @@ type testProvenance struct { } func (p *testProvenance) BuilderID() (string, error) { return p.builderID, nil } +func (p *testProvenance) BuildType() (string, error) { return p.buildType, nil } func (p *testProvenance) SourceURI() (string, error) { return p.sourceURI, nil } func (p *testProvenance) TriggerURI() (string, error) { return p.triggerURI, nil } func (p *testProvenance) Subjects() ([]intoto.Subject, error) { return p.subjects, nil } @@ -75,7 +78,7 @@ func Test_VerifyDigest(t *testing.T) { prov: &testProvenance{ subjects: []intoto.Subject{ { - Digest: common.DigestSet{ + Digest: slsacommon.DigestSet{ "sha1": "4506290e2e8feb1f34b27a044f7cc863c830ef6b", }, }, @@ -90,7 +93,7 @@ func Test_VerifyDigest(t *testing.T) { prov: &testProvenance{ subjects: []intoto.Subject{ { - Digest: common.DigestSet{ + Digest: slsacommon.DigestSet{ "sha1": "4506290e2e8feb1f34b27a044f7cc863c830ef6b", }, }, @@ -111,7 +114,7 @@ func Test_VerifyDigest(t *testing.T) { prov: &testProvenance{ subjects: []intoto.Subject{ { - Digest: common.DigestSet{ + Digest: slsacommon.DigestSet{ "sha256": "0ae7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e", }, }, @@ -125,7 +128,7 @@ func Test_VerifyDigest(t *testing.T) { prov: &testProvenance{ subjects: []intoto.Subject{ { - Digest: common.DigestSet{ + Digest: slsacommon.DigestSet{ "sha256": "0ae7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e", }, }, @@ -138,17 +141,17 @@ func Test_VerifyDigest(t *testing.T) { prov: &testProvenance{ subjects: []intoto.Subject{ { - Digest: common.DigestSet{ + Digest: slsacommon.DigestSet{ "sha256": "03e7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e", }, }, { - Digest: common.DigestSet{ + Digest: slsacommon.DigestSet{ "sha1": "4506290e2e8feb1f34b27a044f7cc863c830ef6b", }, }, { - Digest: common.DigestSet{ + Digest: slsacommon.DigestSet{ "sha256": "0ae7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e", }, }, @@ -161,17 +164,17 @@ func Test_VerifyDigest(t *testing.T) { prov: &testProvenance{ subjects: []intoto.Subject{ { - Digest: common.DigestSet{ + Digest: slsacommon.DigestSet{ "sha256": "03e7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e", }, }, { - Digest: common.DigestSet{ + Digest: slsacommon.DigestSet{ "sha256": "0ae7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e", }, }, { - Digest: common.DigestSet{ + Digest: slsacommon.DigestSet{ "sha1": "4506290e2e8feb1f34b27a044f7cc863c830ef6b", }, }, @@ -184,17 +187,17 @@ func Test_VerifyDigest(t *testing.T) { prov: &testProvenance{ subjects: []intoto.Subject{ { - Digest: common.DigestSet{ + Digest: slsacommon.DigestSet{ "sha256": "03e7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e", }, }, { - Digest: common.DigestSet{ + Digest: slsacommon.DigestSet{ "sha256": "0ae7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e", }, }, { - Digest: common.DigestSet{ + Digest: slsacommon.DigestSet{ "sha1": "4506290e2e8feb1f34b27a044f7cc863c830ef6b", }, }, @@ -220,6 +223,7 @@ func Test_verifySourceURI(t *testing.T) { t.Parallel() tests := []struct { name string + provBuildType string provMaterialsURI string provTriggerURI string expectedSourceURI string @@ -283,14 +287,37 @@ func Test_verifySourceURI(t *testing.T) { expectedSourceURI: "https://github.com/some/repo", }, { - name: "match source no git no material ref", - provTriggerURI: "git+https://github.com/some/repo@v1.2.3", - provMaterialsURI: "git+https://github.com/some/repo", - allowNoMaterialRef: true, - expectedSourceURI: "https://github.com/some/repo", + name: "match source no git no material ref (npm)", + provBuildType: common.NpmCLIBuildTypeV1, + provTriggerURI: "git+https://github.com/some/repo@v1.2.3", + provMaterialsURI: "git+https://github.com/some/repo", + expectedSourceURI: "https://github.com/some/repo", + }, + { + name: "mismatch source material ref (npm)", + provBuildType: common.NpmCLIBuildTypeV1, + provTriggerURI: "git+https://github.com/some/repo@v1.2.3", + provMaterialsURI: "git+https://github.com/some/repo@v1.2.4", + expectedSourceURI: "https://github.com/some/repo", + err: serrors.ErrorInvalidDssePayload, + }, + { + name: "match source no git no material ref (byob)", + provBuildType: common.BYOBBuildTypeV0, + provTriggerURI: "git+https://github.com/some/repo@v1.2.3", + provMaterialsURI: "git+https://github.com/some/repo", + expectedSourceURI: "https://github.com/some/repo", + }, + { + name: "mismatch source material ref (byob)", + provBuildType: common.BYOBBuildTypeV0, + provTriggerURI: "git+https://github.com/some/repo@v1.2.3", + provMaterialsURI: "git+https://github.com/some/repo@v1.2.4", + expectedSourceURI: "https://github.com/some/repo", + err: serrors.ErrorInvalidDssePayload, }, { - name: "match source no git no material ref ref not allowed", + name: "match source no git no material ref", provTriggerURI: "git+https://github.com/some/repo@v1.2.3", provMaterialsURI: "git+https://github.com/some/repo", expectedSourceURI: "https://github.com/some/repo", @@ -358,11 +385,12 @@ func Test_verifySourceURI(t *testing.T) { t.Parallel() prov02 := &testProvenance{ + buildType: tt.provBuildType, sourceURI: tt.provMaterialsURI, triggerURI: tt.provTriggerURI, } - err := verifySourceURI(prov02, tt.expectedSourceURI, tt.allowNoMaterialRef) + err := verifySourceURI(prov02, tt.expectedSourceURI) if !errCmp(err, tt.err) { t.Errorf(cmp.Diff(err, tt.err)) } diff --git a/verifiers/internal/gha/slsaprovenance/common/buildtypes.go b/verifiers/internal/gha/slsaprovenance/common/buildtypes.go new file mode 100644 index 000000000..2ecf8f823 --- /dev/null +++ b/verifiers/internal/gha/slsaprovenance/common/buildtypes.go @@ -0,0 +1,30 @@ +package common + +var ( + // BYOBBuildTypeV0 is the base buildType for BYOB delegated builders. + BYOBBuildTypeV0 = "https://github.com/slsa-framework/slsa-github-generator/delegator-generic@v0" + + // ContainerBasedBuildTypeV01Draft is the buildType for the container-based builder. + ContainerBasedBuildTypeV01Draft = "https://slsa.dev/container-based-build/v0.1?draft" + + // GoBuilderBuildTypeV1 is the buildType for the Go builder. + GoBuilderBuildTypeV1 = "https://github.com/slsa-framework/slsa-github-generator/go@v1" + + // GenericGeneratorBuildTypeV1 is the buildType for the generic generator. + GenericGeneratorBuildTypeV1 = "https://github.com/slsa-framework/slsa-github-generator/generic@v1" + + // ContainerGeneratorBuildTypeV1 is the buildType for the container generator. + ContainerGeneratorBuildTypeV1 = "https://github.com/slsa-framework/slsa-github-generator/container@v1" + + // NpmCLIBuildTypeV1 is the buildType for provenance generated by the npm cli. + NpmCLIBuildTypeV1 = "https://github.com/npm/cli/gha@v1" +) + +// Legacy buildTypes. +var ( + // LegacyGoBuilderBuildTypeV1 is a legacy Go builder buildType. + LegacyGoBuilderBuildTypeV1 = "https://github.com/slsa-framework/slsa-github-generator-go@v1" + + // LegacyBuilderBuildTypeV1 is a legacy generic build type for slsa-github-generator. + LegacyBuilderBuildTypeV1 = "https://github.com/slsa-framework/slsa-github-generator@v1" +) diff --git a/verifiers/internal/gha/slsaprovenance/iface/provenance.go b/verifiers/internal/gha/slsaprovenance/iface/provenance.go index cc7dcca6f..f294743d5 100644 --- a/verifiers/internal/gha/slsaprovenance/iface/provenance.go +++ b/verifiers/internal/gha/slsaprovenance/iface/provenance.go @@ -11,6 +11,9 @@ type Provenance interface { // BuilderID returns the builder id in the predicate. BuilderID() (string, error) + // BuildType returns the buildType. + BuildType() (string, error) + // SourceURI is the full URI (including tag) of the source material. SourceURI() (string, error) diff --git a/verifiers/internal/gha/slsaprovenance/slsaprovenance.go b/verifiers/internal/gha/slsaprovenance/slsaprovenance.go index a53450308..6adb41240 100644 --- a/verifiers/internal/gha/slsaprovenance/slsaprovenance.go +++ b/verifiers/internal/gha/slsaprovenance/slsaprovenance.go @@ -6,7 +6,6 @@ import ( "fmt" intoto "github.com/in-toto/in-toto-golang/in_toto" - slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" dsselib "github.com/secure-systems-lab/go-securesystemslib/dsse" serrors "github.com/slsa-framework/slsa-verifier/v2/errors" @@ -17,40 +16,43 @@ import ( ) // provenanceConstructor creates a new Provenance instance for the given payload as a json Decoder. -type provenanceConstructor func(payload []byte) (iface.Provenance, error) +type provenanceConstructor func(builderID string, payload []byte) (iface.Provenance, error) // predicateTypeMap stores the different provenance version types. It is a map of // predicate type -> ProvenanceConstructor. var predicateTypeMap = map[string]provenanceConstructor{ - common.ProvenanceV02Type: slsav02.New, - slsa1.PredicateSLSAProvenance: slsav1.New, + common.ProvenanceV02Type: slsav02.New, + common.ProvenanceV1Type: slsav1.New, } -// ProvenanceFromEnvelope returns a Provenance instance for the given DSSE Envelope. -func ProvenanceFromEnvelope(env *dsselib.Envelope) (iface.Provenance, error) { +// ProvenanceFromEnvelope returns a Provenance instance for the given builder +// ID and DSSE Envelope. The builder ID is retrieved from the signing certificate +// rather than from the payload itself in order to support delegated builders. +func ProvenanceFromEnvelope(builderID string, env *dsselib.Envelope) (iface.Provenance, error) { if env.PayloadType != intoto.PayloadType { - return nil, fmt.Errorf("%w: expected payload type %q, got '%s'", + return nil, fmt.Errorf("%w: expected payload type %q, got %q", serrors.ErrorInvalidDssePayload, intoto.PayloadType, env.PayloadType) } + pyld, err := base64.StdEncoding.DecodeString(env.Payload) if err != nil { - return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, err.Error()) + return nil, fmt.Errorf("%w: %w", serrors.ErrorInvalidDssePayload, err) } // Load the in-toto attestation statement header. pred := intoto.StatementHeader{} if err := json.Unmarshal(pyld, &pred); err != nil { - return nil, fmt.Errorf("%w: decoding json: %v", serrors.ErrorInvalidDssePayload, err) + return nil, fmt.Errorf("%w: decoding json: %w", serrors.ErrorInvalidDssePayload, err) } // Verify the predicate type is one we can handle. newProv, ok := predicateTypeMap[pred.PredicateType] if !ok { - return nil, fmt.Errorf("%w: unexpected predicate type '%s'", serrors.ErrorInvalidDssePayload, pred.PredicateType) + return nil, fmt.Errorf("%w: unexpected predicate type %q", serrors.ErrorInvalidDssePayload, pred.PredicateType) } - prov, err := newProv(pyld) + prov, err := newProv(builderID, pyld) if err != nil { - return nil, fmt.Errorf("%w: %v", serrors.ErrorInvalidDssePayload, err) + return nil, fmt.Errorf("%w: %w", serrors.ErrorInvalidDssePayload, err) } return prov, nil diff --git a/verifiers/internal/gha/slsaprovenance/slsaprovenance_test.go b/verifiers/internal/gha/slsaprovenance/slsaprovenance_test.go index b46e39df8..8d859729b 100644 --- a/verifiers/internal/gha/slsaprovenance/slsaprovenance_test.go +++ b/verifiers/internal/gha/slsaprovenance/slsaprovenance_test.go @@ -12,6 +12,7 @@ import ( "github.com/secure-systems-lab/go-securesystemslib/dsse" serrors "github.com/slsa-framework/slsa-verifier/v2/errors" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" slsav1 "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/v1.0" ) @@ -26,13 +27,15 @@ func mustJSON(o any) string { func Test_ProvenanceFromEnvelope(t *testing.T) { t.Parallel() tests := []struct { - name string - envelope *dsse.Envelope - path string - err error + name string + builderID string + envelope *dsse.Envelope + path string + err error }{ { - name: "valid dsse", + name: "valid dsse", + builderID: common.GenericDelegatorBuilderID, envelope: &dsse.Envelope{ PayloadType: intoto.PayloadType, Payload: mustJSON(&slsav1.Attestation{ @@ -48,7 +51,8 @@ func Test_ProvenanceFromEnvelope(t *testing.T) { }, }, { - name: "invalid dsse: not SLSA predicate", + name: "invalid dsse: not SLSA predicate", + builderID: common.GenericDelegatorBuilderID, envelope: &dsse.Envelope{ PayloadType: intoto.PayloadType, Payload: mustJSON(&intoto.StatementHeader{ @@ -59,7 +63,8 @@ func Test_ProvenanceFromEnvelope(t *testing.T) { err: serrors.ErrorInvalidDssePayload, }, { - name: "invalid dsse: not base64", + name: "invalid dsse: not base64", + builderID: common.GenericDelegatorBuilderID, envelope: &dsse.Envelope{ PayloadType: intoto.PayloadType, // NOTE: Not valid base64. @@ -68,7 +73,8 @@ func Test_ProvenanceFromEnvelope(t *testing.T) { err: serrors.ErrorInvalidDssePayload, }, { - name: "invalid dsse: not json", + name: "invalid dsse: not json", + builderID: common.GenericDelegatorBuilderID, envelope: &dsse.Envelope{ PayloadType: intoto.PayloadType, // NOTE: Not valid JSON. @@ -77,7 +83,8 @@ func Test_ProvenanceFromEnvelope(t *testing.T) { err: serrors.ErrorInvalidDssePayload, }, { - name: "invalid dsse: not in-toto", + name: "invalid dsse: not in-toto", + builderID: common.GenericDelegatorBuilderID, envelope: &dsse.Envelope{ // NOTE: Not an in-toto attestation payload type, PayloadType: "http://github.com/other/payload/type", @@ -101,7 +108,7 @@ func Test_ProvenanceFromEnvelope(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - _, err := ProvenanceFromEnvelope(tt.envelope) + _, err := ProvenanceFromEnvelope(tt.builderID, tt.envelope) if diff := cmp.Diff(tt.err, err, cmpopts.EquateErrors()); diff != "" { t.Errorf("unexpected error (-want +got):\n%s", diff) } diff --git a/verifiers/internal/gha/slsaprovenance/v0.2/base.go b/verifiers/internal/gha/slsaprovenance/v0.2/base.go index afbcdfb97..320306e68 100644 --- a/verifiers/internal/gha/slsaprovenance/v0.2/base.go +++ b/verifiers/internal/gha/slsaprovenance/v0.2/base.go @@ -9,8 +9,16 @@ import ( serrors "github.com/slsa-framework/slsa-verifier/v2/errors" "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/iface" ) +func newLegacyBuilderProvenance(a *Attestation) iface.Provenance { + return &provenanceV02{ + upperEnv: false, + prov: a, + } +} + // provenanceV02 implements basic logic for SLSA v0.2 provenance. type provenanceV02 struct { // upperEnv specifies if environment fields are in uppercase. @@ -28,6 +36,11 @@ func (p *provenanceV02) BuilderID() (string, error) { return p.prov.Predicate.Builder.ID, nil } +// BuildType implements Provenance.BuildType. +func (p *provenanceV02) BuildType() (string, error) { + return p.prov.Predicate.BuildType, nil +} + // SourceURI implements Provenance.SourceURI. func (p *provenanceV02) SourceURI() (string, error) { if len(p.prov.Predicate.Materials) == 0 { diff --git a/verifiers/internal/gha/slsaprovenance/v0.2/byob.go b/verifiers/internal/gha/slsaprovenance/v0.2/byob.go new file mode 100644 index 000000000..93f1943f9 --- /dev/null +++ b/verifiers/internal/gha/slsaprovenance/v0.2/byob.go @@ -0,0 +1,91 @@ +package v02 + +import ( + "fmt" + + serrors "github.com/slsa-framework/slsa-verifier/v2/errors" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/iface" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/utils" +) + +// byobProvenance is SLSA v0.2 provenance created by a BYOB builder. +type byobProvenance struct { + *provenanceV02 +} + +func newBYOBProvenance(att *Attestation) iface.Provenance { + return &byobProvenance{ + provenanceV02: &provenanceV02{ + prov: att, + upperEnv: true, + }, + } +} + +// GetBranch implements Provenance.GetBranch. +func (p *byobProvenance) GetBranch() (string, error) { + sourceURI, err := p.SourceURI() + if err != nil { + return "", fmt.Errorf("reading source uri: %w", err) + } + + // Returns the branch from the source URI if available. + _, ref, err := utils.ParseGitURIAndRef(sourceURI) + if err != nil { + return "", fmt.Errorf("parsing source uri: %w", err) + } + + if ref == "" { + return "", fmt.Errorf("%w: unable to get ref for source %q", + serrors.ErrorInvalidDssePayload, sourceURI) + } + + refType, _ := utils.ParseGitRef(ref) + switch refType { + case "heads": // branch. + // NOTE: We return the full git ref. + return ref, nil + case "tags": + // NOTE: If the ref type is a tag we want to try to parse out the branch from the tag. + sysParams, ok := p.prov.Predicate.Invocation.Environment.(map[string]interface{}) + if !ok { + return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type") + } + return common.GetBranch(sysParams, true) + default: + return "", fmt.Errorf("%w: unknown ref type %q for ref %q", + serrors.ErrorInvalidDssePayload, refType, ref) + } +} + +// GetTag implements Provenance.GetTag. +func (p *byobProvenance) GetTag() (string, error) { + sourceURI, err := p.SourceURI() + if err != nil { + return "", fmt.Errorf("reading source uri: %w", err) + } + + // Returns the branch from the source URI if available. + _, ref, err := utils.ParseGitURIAndRef(sourceURI) + if err != nil { + return "", fmt.Errorf("parsing source uri: %w", err) + } + + if ref == "" { + return "", fmt.Errorf("%w: unable to get ref for source %q", + serrors.ErrorInvalidDssePayload, sourceURI) + } + + refType, _ := utils.ParseGitRef(ref) + switch refType { + case "heads": // branch. + return "", nil + case "tags": + // NOTE: We return the full git ref. + return ref, nil + default: + return "", fmt.Errorf("%w: unknown ref type %q for ref %q", + serrors.ErrorInvalidDssePayload, refType, ref) + } +} diff --git a/verifiers/internal/gha/slsaprovenance/v0.2/byob_test.go b/verifiers/internal/gha/slsaprovenance/v0.2/byob_test.go new file mode 100644 index 000000000..012e2f05d --- /dev/null +++ b/verifiers/internal/gha/slsaprovenance/v0.2/byob_test.go @@ -0,0 +1,280 @@ +package v02 + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + intoto "github.com/in-toto/in-toto-golang/in_toto" + slsacommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" + slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" + + serrors "github.com/slsa-framework/slsa-verifier/v2/errors" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/iface" +) + +func Test_byobProvenance_GetBranch(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + prov iface.Provenance + branch string + err error + }{ + { + name: "empty provenance", + prov: newBYOBProvenance( + &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa02.ProvenancePredicate{}, + }, + ), + err: serrors.ErrorInvalidDssePayload, + }, + { + name: "materials uri @ refs/heads/main", + prov: newBYOBProvenance( + &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa02.ProvenancePredicate{ + Materials: []slsacommon.ProvenanceMaterial{ + { + URI: "git+https://github.com/kubernetes/kubernetes@refs/heads/main", + }, + }, + }, + }, + ), + branch: "refs/heads/main", + }, + { + name: "environment GITHUB_REF @ refs/heads/main", + prov: newBYOBProvenance( + &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa02.ProvenancePredicate{ + Invocation: slsa02.ProvenanceInvocation{ + Environment: map[string]interface{}{ + "GITHUB_REF_TYPE": "branch", + "GITHUB_REF": "refs/heads/main", + }, + }, + }, + }, + ), + err: serrors.ErrorInvalidDssePayload, + }, + { + name: "materials uri @ refs/tags/v1.0.0", + prov: newBYOBProvenance( + &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa02.ProvenancePredicate{ + Invocation: slsa02.ProvenanceInvocation{ + Environment: map[string]interface{}{ + "GITHUB_BASE_REF": "", + "GITHUB_REF_TYPE": "tag", + "GITHUB_REF": "refs/tags/v1.0.0", + "GITHUB_EVENT_NAME": "push", + "GITHUB_EVENT_PAYLOAD": map[string]any{ + "base_ref": nil, + }, + }, + }, + Materials: []slsacommon.ProvenanceMaterial{ + { + URI: "git+https://github.com/kubernetes/kubernetes@refs/tags/v1.0.0", + }, + }, + }, + }, + ), + branch: "", + }, + { + name: "environment GITHUB_REF @ ref/tags/v1.0.0", + prov: newBYOBProvenance( + &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa02.ProvenancePredicate{ + Invocation: slsa02.ProvenanceInvocation{ + Environment: map[string]interface{}{ + "GITHUB_BASE_REF": "", + "GITHUB_REF_TYPE": "tag", + "GITHUB_REF": "refs/tags/v1.0.0", + "GITHUB_EVENT_NAME": "push", + "GITHUB_EVENT_PAYLOAD": map[string]any{ + "base_ref": nil, + }, + }, + }, + }, + }, + ), + err: serrors.ErrorInvalidDssePayload, + }, + { + name: "materials uri no ref", + prov: newBYOBProvenance( + &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa02.ProvenancePredicate{ + Invocation: slsa02.ProvenanceInvocation{ + Environment: map[string]interface{}{ + "GITHUB_REF_TYPE": "branch", + "GITHUB_REF": "refs/heads/main", + }, + }, + Materials: []slsacommon.ProvenanceMaterial{ + { + URI: "git+https://github.com/kubernetes/kubernetes", + }, + }, + }, + }, + ), + err: serrors.ErrorInvalidDssePayload, + }, + } + + for i := range testCases { + tt := testCases[i] + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + branch, err := tt.prov.GetBranch() + if diff := cmp.Diff(tt.err, err, cmpopts.EquateErrors()); diff != "" { + t.Fatalf("unexpected error: %v", err) + } + if got, want := branch, tt.branch; got != want { + t.Fatalf("unexpected branch, got: %q, want: %q", got, want) + } + }) + } +} + +func Test_byobProvenance_GetTag(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + prov iface.Provenance + tag string + err error + }{ + { + name: "empty provenance", + prov: newBYOBProvenance( + &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa02.ProvenancePredicate{}, + }, + ), + err: serrors.ErrorInvalidDssePayload, + }, + { + name: "materials uri @ refs/heads/main", + prov: newBYOBProvenance( + &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa02.ProvenancePredicate{ + Materials: []slsacommon.ProvenanceMaterial{ + { + URI: "git+https://github.com/kubernetes/kubernetes@refs/heads/main", + }, + }, + }, + }, + ), + tag: "", + }, + { + name: "environment GITHUB_REF @ refs/heads/main", + prov: newBYOBProvenance( + &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa02.ProvenancePredicate{ + Invocation: slsa02.ProvenanceInvocation{ + Environment: map[string]interface{}{ + "GITHUB_REF_TYPE": "branch", + "GITHUB_REF": "refs/heads/main", + }, + }, + }, + }, + ), + err: serrors.ErrorInvalidDssePayload, + }, + { + name: "materials uri @ refs/tags/v1.0.0", + prov: newBYOBProvenance( + &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa02.ProvenancePredicate{ + Materials: []slsacommon.ProvenanceMaterial{ + { + URI: "git+https://github.com/kubernetes/kubernetes@refs/tags/v1.0.0", + }, + }, + }, + }, + ), + tag: "refs/tags/v1.0.0", + }, + { + name: "environment GITHUB_REF @ ref/tags/v1.0.0", + prov: newBYOBProvenance( + &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa02.ProvenancePredicate{ + Invocation: slsa02.ProvenanceInvocation{ + Environment: map[string]interface{}{ + "GITHUB_REF_TYPE": "tag", + "GITHUB_REF": "refs/tags/v1.0.0", + }, + }, + }, + }, + ), + err: serrors.ErrorInvalidDssePayload, + }, + { + name: "materials uri no ref", + prov: newBYOBProvenance( + &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa02.ProvenancePredicate{ + Invocation: slsa02.ProvenanceInvocation{ + Environment: map[string]interface{}{ + "GITHUB_REF_TYPE": "tag", + "GITHUB_REF": "refs/tags/v1.0.0", + }, + }, + Materials: []slsacommon.ProvenanceMaterial{ + { + URI: "git+https://github.com/kubernetes/kubernetes", + }, + }, + }, + }, + ), + err: serrors.ErrorInvalidDssePayload, + }, + } + + for i := range testCases { + tt := testCases[i] + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + tag, err := tt.prov.GetTag() + if diff := cmp.Diff(tt.err, err, cmpopts.EquateErrors()); diff != "" { + t.Fatalf("unexpected error: %v", err) + } + if got, want := tag, tt.tag; got != want { + t.Fatalf("unexpected tag, got: %q, want: %q", got, want) + } + }) + } +} diff --git a/verifiers/internal/gha/slsaprovenance/v0.2/provenance.go b/verifiers/internal/gha/slsaprovenance/v0.2/provenance.go index 42643460c..c854ade1c 100644 --- a/verifiers/internal/gha/slsaprovenance/v0.2/provenance.go +++ b/verifiers/internal/gha/slsaprovenance/v0.2/provenance.go @@ -7,26 +7,12 @@ import ( intoto "github.com/in-toto/in-toto-golang/in_toto" slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" - serrors "github.com/slsa-framework/slsa-verifier/v2/errors" + serrors "github.com/slsa-framework/slsa-verifier/v2/errors" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/iface" ) -var ( - goBuilderBuildType = "https://github.com/slsa-framework/slsa-github-generator/go@v1" - - genericGeneratorBuildType = "https://github.com/slsa-framework/slsa-github-generator/generic@v1" - containerGeneratorBuildType = "https://github.com/slsa-framework/slsa-github-generator/container@v1" - npmCLIBuildType = "https://github.com/npm/cli/gha@v1" - - // Legacy build types. - legacyGoBuilderBuildType = "https://github.com/slsa-framework/slsa-github-generator-go@v1" - legacyBuilderBuildType = "https://github.com/slsa-framework/slsa-github-generator@v1" - - // byobBuildType is the base build type for BYOB delegated builders. - byobDelegatorBuildType = "https://github.com/slsa-framework/slsa-github-generator/delegator-generic@v0" -) - // Attestation is a SLSA v0.2 in-toto attestation statement. type Attestation struct { intoto.StatementHeader @@ -38,8 +24,29 @@ type ProvenanceV02 interface { Predicate() slsa02.ProvenancePredicate } +type provFunc func(*Attestation) iface.Provenance + +// buildTypeMap is a map of builder IDs to supported buildTypes. +var buildTypeMap = map[string]map[string]provFunc{ + common.GenericDelegatorBuilderID: {common.BYOBBuildTypeV0: newBYOBProvenance}, + common.GenericLowPermsDelegatorBuilderID: {common.BYOBBuildTypeV0: newBYOBProvenance}, + + common.GoBuilderID: { + common.GoBuilderBuildTypeV1: newLegacyBuilderProvenance, + common.LegacyBuilderBuildTypeV1: newLegacyBuilderProvenance, + common.LegacyGoBuilderBuildTypeV1: newLegacyBuilderProvenance, + }, + + common.GenericGeneratorBuilderID: {common.GenericGeneratorBuildTypeV1: newLegacyBuilderProvenance}, + common.ContainerGeneratorBuilderID: {common.ContainerGeneratorBuildTypeV1: newLegacyBuilderProvenance}, + + common.NpmCLILegacyBuilderID: {common.NpmCLIBuildTypeV1: newLegacyBuilderProvenance}, + common.NpmCLIHostedBuilderID: {common.NpmCLIBuildTypeV1: newLegacyBuilderProvenance}, + // NOTE: we don't support Npm CLI on self-hosted. +} + // New returns a new Provenance for the given json payload. -func New(payload []byte) (iface.Provenance, error) { +func New(builderID string, payload []byte) (iface.Provenance, error) { // Strict unmarshal. // NOTE: this supports extensions because they are // only used as part of interface{}-defined fields. @@ -51,23 +58,15 @@ func New(payload []byte) (iface.Provenance, error) { return nil, err } - switch { - case a.Predicate.BuildType == byobDelegatorBuildType: - return &provenanceV02{ - upperEnv: true, - prov: a, - }, nil - case a.Predicate.BuildType == goBuilderBuildType || - a.Predicate.BuildType == genericGeneratorBuildType || - a.Predicate.BuildType == containerGeneratorBuildType || - a.Predicate.BuildType == npmCLIBuildType || - a.Predicate.BuildType == legacyBuilderBuildType || - a.Predicate.BuildType == legacyGoBuilderBuildType: - return &provenanceV02{ - upperEnv: false, - prov: a, - }, nil - default: - return nil, fmt.Errorf("%w: unknown buildType: %q", serrors.ErrorInvalidDssePayload, a.Predicate.BuildType) + btMap, ok := buildTypeMap[builderID] + if !ok { + return nil, fmt.Errorf("%w: %q", serrors.ErrorInvalidBuilderID, builderID) + } + + pFunc, ok := btMap[a.Predicate.BuildType] + if !ok { + return nil, fmt.Errorf("%w: %q for builder ID %q", serrors.ErrorInvalidBuildType, a.Predicate.BuildType, builderID) } + + return pFunc(a), nil } diff --git a/verifiers/internal/gha/slsaprovenance/v1.0/base.go b/verifiers/internal/gha/slsaprovenance/v1.0/base.go new file mode 100644 index 000000000..81f0e493c --- /dev/null +++ b/verifiers/internal/gha/slsaprovenance/v1.0/base.go @@ -0,0 +1,188 @@ +package v1 + +import ( + "fmt" + "strings" + "time" + + intoto "github.com/in-toto/in-toto-golang/in_toto" + slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + serrors "github.com/slsa-framework/slsa-verifier/v2/errors" + + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" +) + +// provenanceV1 is a base implementation for SLSA v1.0 provenance. +type provenanceV1 struct { + prov *Attestation +} + +// Predicate implements ProvenanceV02.Predicate. +func (p *provenanceV1) Predicate() slsa1.ProvenancePredicate { + return p.prov.Predicate +} + +// BuilderID implements Provenance.BuilderID. +func (p *provenanceV1) BuilderID() (string, error) { + return p.prov.Predicate.RunDetails.Builder.ID, nil +} + +// BuildType implements Provenance.BuildType. +func (p *provenanceV1) BuildType() (string, error) { + return p.prov.Predicate.BuildDefinition.BuildType, nil +} + +// SourceURI implements Provenance.SourceURI. +func (p *provenanceV1) SourceURI() (string, error) { + // Use resolvedDependencies. + if len(p.prov.Predicate.BuildDefinition.ResolvedDependencies) == 0 { + return "", fmt.Errorf("%w: empty resovedDependencies", serrors.ErrorInvalidDssePayload) + } + // For now, we use the first resolvedDependency relying on a GHA builder-verifier contract. + uri := p.prov.Predicate.BuildDefinition.ResolvedDependencies[0].URI + if uri == "" { + return "", fmt.Errorf("%w: empty uri", serrors.ErrorMalformedURI) + } + return uri, nil +} + +func (p *provenanceV1) builderTriggerInfo() (string, string, string, error) { + sysParams, ok := p.prov.Predicate.BuildDefinition.InternalParameters.(map[string]interface{}) + if !ok { + return "", "", "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "internal parameters type") + } + + if _, exists := sysParams["GITHUB_WORKFLOW_REF"]; !exists { + return "", "", "", fmt.Errorf("%w: GITHUB_WORKFLOW_REF", serrors.ErrorNotPresent) + } + + workflowRef, err := common.GetAsString(sysParams, "GITHUB_WORKFLOW_REF") + if err != nil { + return "", "", "", err + } + + parts := strings.Split(workflowRef, "@") + if len(parts) != 2 { + return "", "", "", fmt.Errorf("%w: ref: %s", serrors.ErrorInvalidFormat, workflowRef) + } + repoAndPath := parts[0] + ref := parts[1] + + parts = strings.Split(repoAndPath, "/") + if len(parts) < 2 { + return "", "", "", fmt.Errorf("%w: rep and path: %s", serrors.ErrorInvalidFormat, repoAndPath) + } + + repo := strings.Join(parts[:2], "/") + path := strings.Join(parts[2:], "/") + return fmt.Sprintf("git+https://github.com/%s", repo), ref, path, nil +} + +func (p *provenanceV1) triggerInfo() (string, string, string, error) { + return p.builderTriggerInfo() +} + +// TriggerURI implements Provenance.TriggerURI. +func (p *provenanceV1) TriggerURI() (string, error) { + repository, ref, _, err := p.triggerInfo() + if err != nil { + return "", err + } + if repository == "" || ref == "" { + return "", fmt.Errorf("%w: repository or ref is empty", serrors.ErrorMalformedURI) + } + return fmt.Sprintf("%s@%s", repository, ref), nil +} + +// Subjects implements Provenance.Subjects. +func (p *provenanceV1) Subjects() ([]intoto.Subject, error) { + subj := p.prov.Subject + if len(subj) == 0 { + return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "no subjects") + } + return subj, nil +} + +// GetBranch implements Provenance.GetBranch. +func (p *provenanceV1) GetBranch() (string, error) { + sysParams, ok := p.prov.Predicate.BuildDefinition.InternalParameters.(map[string]interface{}) + if !ok { + return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "internal parameters type") + } + + return common.GetBranch(sysParams, true) +} + +// GetTag implements Provenance.GetTag. +func (p *provenanceV1) GetTag() (string, error) { + sysParams, ok := p.prov.Predicate.BuildDefinition.InternalParameters.(map[string]interface{}) + if !ok { + return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "system parameters type") + } + + return common.GetTag(sysParams, true) +} + +// GetWorkflowInputs implements Provenance.GetWorkflowInputs. +func (p *provenanceV1) GetWorkflowInputs() (map[string]interface{}, error) { + sysParams, ok := p.prov.Predicate.BuildDefinition.InternalParameters.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "system parameters type") + } + return common.GetWorkflowInputs(sysParams, true) +} + +// GetBuildTriggerPath implements Provenance.GetBuildTriggerPath. +func (p *provenanceV1) GetBuildTriggerPath() (string, error) { + // TODO(#566): verify the ref and repo as well. + sysParams, ok := p.prov.Predicate.BuildDefinition.ExternalParameters.(map[string]interface{}) + if !ok { + return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "system parameters type") + } + + w, ok := sysParams["workflow"] + if !ok { + return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "workflow parameters type") + } + + wMap, ok := w.(map[string]string) + if !ok { + return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "workflow not a map") + } + + v, ok := wMap["path"] + if !ok { + return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "no path entry on workflow") + } + return v, nil +} + +// GetBuildInvocationID implements Provenance.GetBuildInvocationID. +func (p *provenanceV1) GetBuildInvocationID() (string, error) { + return p.prov.Predicate.RunDetails.BuildMetadata.InvocationID, nil +} + +// GetBuildStartTime implements Provenance.GetBuildStartTime. +func (p *provenanceV1) GetBuildStartTime() (*time.Time, error) { + return p.prov.Predicate.RunDetails.BuildMetadata.StartedOn, nil +} + +// GetBuildFinishTime implements Provenance.GetBuildFinishTime. +func (p *provenanceV1) GetBuildFinishTime() (*time.Time, error) { + return p.prov.Predicate.RunDetails.BuildMetadata.FinishedOn, nil +} + +// GetNumberResolvedDependencies implements Provenance.GetNumberResolvedDependencies. +func (p *provenanceV1) GetNumberResolvedDependencies() (int, error) { + return len(p.prov.Predicate.BuildDefinition.ResolvedDependencies), nil +} + +// GetSystemParameters implements Provenance.GetSystemParameters. +func (p *provenanceV1) GetSystemParameters() (map[string]any, error) { + sysParams, ok := p.prov.Predicate.BuildDefinition.InternalParameters.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "system parameters type") + } + + return sysParams, nil +} diff --git a/verifiers/internal/gha/slsaprovenance/v1.0/byob.go b/verifiers/internal/gha/slsaprovenance/v1.0/byob.go index 6216440d6..d6dba5320 100644 --- a/verifiers/internal/gha/slsaprovenance/v1.0/byob.go +++ b/verifiers/internal/gha/slsaprovenance/v1.0/byob.go @@ -2,14 +2,11 @@ package v1 import ( "fmt" - "strings" - "time" - intoto "github.com/in-toto/in-toto-golang/in_toto" - slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" serrors "github.com/slsa-framework/slsa-verifier/v2/errors" "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/utils" ) // BYOBBuildType is the base build type for BYOB delegated builders. @@ -17,224 +14,72 @@ var BYOBBuildType = "https://github.com/slsa-framework/slsa-github-generator/del // BYOBProvenance is SLSA v1.0 provenance for the slsa-github-generator BYOB build type. type BYOBProvenance struct { - prov *Attestation + *provenanceV1 } -// Predicate implements ProvenanceV02.Predicate. -func (p *BYOBProvenance) Predicate() slsa1.ProvenancePredicate { - return p.prov.Predicate -} - -// BuilderID implements Provenance.BuilderID. -func (p *BYOBProvenance) BuilderID() (string, error) { - return p.prov.Predicate.RunDetails.Builder.ID, nil -} - -// SourceURI implements Provenance.SourceURI. -func (p *BYOBProvenance) SourceURI() (string, error) { - // Use resolvedDependencies. - if len(p.prov.Predicate.BuildDefinition.ResolvedDependencies) == 0 { - return "", fmt.Errorf("%w: empty resovedDependencies", serrors.ErrorInvalidDssePayload) - } - // For now, we use the first resolvedDependency relying on a GHA builder-verifier contract. - uri := p.prov.Predicate.BuildDefinition.ResolvedDependencies[0].URI - if uri == "" { - return "", fmt.Errorf("%w: empty uri", serrors.ErrorMalformedURI) - } - return uri, nil -} - -// TODO(#613): Support for generators. -// -//nolint:unused -func getValidateKey(m map[string]interface{}, key string) (string, error) { - v, ok := m[key] - if !ok { - return "", fmt.Errorf("%w: no %v found", serrors.ErrorInvalidFormat, key) - } - vv, ok := v.(string) - if !ok { - return "", fmt.Errorf("%w: not a string %v", serrors.ErrorInvalidFormat, v) - } - if vv == "" { - return "", fmt.Errorf("%w: empty %v", serrors.ErrorInvalidFormat, key) - } - return vv, nil -} - -// TODO(#613): Support for generators. -// -//nolint:unused -func (p *BYOBProvenance) generatorTriggerInfo() (string, string, string, error) { - // See https://github.com/slsa-framework/github-actions-buildtypes/blob/main/workflow/v1/example.json#L16-L19. - extParams, ok := p.prov.Predicate.BuildDefinition.ExternalParameters.(map[string]interface{}) - if !ok { - return "", "", "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "external parameters type") - } - workflow, ok := extParams["workflow"] - if !ok { - return "", "", "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "external parameters workflow") - } - workflowMap, ok := workflow.(map[string]interface{}) - if !ok { - return "", "", "", fmt.Errorf("%w: %s, type %T", serrors.ErrorInvalidDssePayload, "not a map of interface{}", workflow) - } - ref, err := getValidateKey(workflowMap, "ref") - if err != nil { - return "", "", "", fmt.Errorf("%w: %v", serrors.ErrorMalformedURI, err) - } - repository, err := getValidateKey(workflowMap, "repository") - if err != nil { - return "", "", "", fmt.Errorf("%w: %v", serrors.ErrorMalformedURI, err) - } - path, err := getValidateKey(workflowMap, "path") +// GetBranch implements Provenance.GetBranch. +func (p *BYOBProvenance) GetBranch() (string, error) { + sourceURI, err := p.SourceURI() if err != nil { - return "", "", "", err - } - return repository, ref, path, nil -} - -func (p *BYOBProvenance) builderTriggerInfo() (string, string, string, error) { - sysParams, ok := p.prov.Predicate.BuildDefinition.InternalParameters.(map[string]interface{}) - if !ok { - return "", "", "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "internal parameters type") + return "", fmt.Errorf("reading source uri: %w", err) } - if _, exists := sysParams["GITHUB_WORKFLOW_REF"]; !exists { - return "", "", "", fmt.Errorf("%w: GITHUB_WORKFLOW_REF", serrors.ErrorNotPresent) - } - - workflowRef, err := common.GetAsString(sysParams, "GITHUB_WORKFLOW_REF") + // Returns the branch from the source URI if available. + _, ref, err := utils.ParseGitURIAndRef(sourceURI) if err != nil { - return "", "", "", err + return "", fmt.Errorf("parsing source uri: %w", err) } - parts := strings.Split(workflowRef, "@") - if len(parts) != 2 { - return "", "", "", fmt.Errorf("%w: ref: %s", serrors.ErrorInvalidFormat, workflowRef) + if ref == "" { + return "", fmt.Errorf("%w: unable to get ref for source %q", + serrors.ErrorInvalidDssePayload, sourceURI) } - repoAndPath := parts[0] - ref := parts[1] - parts = strings.Split(repoAndPath, "/") - if len(parts) < 2 { - return "", "", "", fmt.Errorf("%w: rep and path: %s", serrors.ErrorInvalidFormat, repoAndPath) + refType, _ := utils.ParseGitRef(ref) + switch refType { + case "heads": // branch. + // NOTE: We return the full git ref. + return ref, nil + case "tags": + // NOTE: If the ref type is a tag we want to try to parse out the branch from the tag. + sysParams, ok := p.prov.Predicate.BuildDefinition.InternalParameters.(map[string]interface{}) + if !ok { + return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "internal parameters type") + } + return common.GetBranch(sysParams, true) + default: + return "", fmt.Errorf("%w: unknown ref type %q for ref %q", + serrors.ErrorInvalidDssePayload, refType, ref) } - - repo := strings.Join(parts[:2], "/") - path := strings.Join(parts[2:], "/") - return fmt.Sprintf("git+https://github.com/%s", repo), ref, path, nil -} - -func (p *BYOBProvenance) triggerInfo() (string, string, string, error) { - // TODO(#613): Support for generators. - return p.builderTriggerInfo() -} - -// TriggerURI implements Provenance.TriggerURI. -func (p *BYOBProvenance) TriggerURI() (string, error) { - repository, ref, _, err := p.triggerInfo() - if err != nil { - return "", err - } - if repository == "" || ref == "" { - return "", fmt.Errorf("%w: repository or ref is empty", serrors.ErrorMalformedURI) - } - return fmt.Sprintf("%s@%s", repository, ref), nil -} - -// Subjects implements Provenance.Subjects. -func (p *BYOBProvenance) Subjects() ([]intoto.Subject, error) { - subj := p.prov.Subject - if len(subj) == 0 { - return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "no subjects") - } - return subj, nil -} - -// GetBranch implements Provenance.GetBranch. -func (p *BYOBProvenance) GetBranch() (string, error) { - // TODO(https://github.com/slsa-framework/slsa-verifier/issues/472): Add GetBranch() support. - sysParams, ok := p.prov.Predicate.BuildDefinition.InternalParameters.(map[string]interface{}) - if !ok { - return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "internal parameters type") - } - - return common.GetBranch(sysParams, true) } // GetTag implements Provenance.GetTag. func (p *BYOBProvenance) GetTag() (string, error) { - // Get the value from the internalParameters if there is no source URI. - sysParams, ok := p.prov.Predicate.BuildDefinition.InternalParameters.(map[string]interface{}) - if !ok { - return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "system parameters type") - } - - return common.GetTag(sysParams, true) -} - -// GetWorkflowInputs implements Provenance.GetWorkflowInputs. -func (p *BYOBProvenance) GetWorkflowInputs() (map[string]interface{}, error) { - sysParams, ok := p.prov.Predicate.BuildDefinition.InternalParameters.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "system parameters type") - } - return common.GetWorkflowInputs(sysParams, true) -} - -// GetBuildTriggerPath implements Provenance.GetBuildTriggerPath. -func (p *BYOBProvenance) GetBuildTriggerPath() (string, error) { - // TODO(https://github.com/slsa-framework/slsa-verifier/issues/566): - // verify the ref and repo as well. - sysParams, ok := p.prov.Predicate.BuildDefinition.ExternalParameters.(map[string]interface{}) - if !ok { - return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "system parameters type") - } - - w, ok := sysParams["workflow"] - if !ok { - return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "workflow parameters type") + sourceURI, err := p.SourceURI() + if err != nil { + return "", fmt.Errorf("reading source uri: %w", err) } - wMap, ok := w.(map[string]string) - if !ok { - return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "workflow not a map") + // Returns the branch from the source URI if available. + _, ref, err := utils.ParseGitURIAndRef(sourceURI) + if err != nil { + return "", fmt.Errorf("parsing source uri: %w", err) } - v, ok := wMap["path"] - if !ok { - return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "no path entry on workflow") + if ref == "" { + return "", fmt.Errorf("%w: unable to get ref for source %q", + serrors.ErrorInvalidDssePayload, sourceURI) } - return v, nil -} - -// GetBuildInvocationID implements Provenance.GetBuildInvocationID. -func (p *BYOBProvenance) GetBuildInvocationID() (string, error) { - return p.prov.Predicate.RunDetails.BuildMetadata.InvocationID, nil -} - -// GetBuildStartTime implements Provenance.GetBuildStartTime. -func (p *BYOBProvenance) GetBuildStartTime() (*time.Time, error) { - return p.prov.Predicate.RunDetails.BuildMetadata.StartedOn, nil -} - -// GetBuildFinishTime implements Provenance.GetBuildFinishTime. -func (p *BYOBProvenance) GetBuildFinishTime() (*time.Time, error) { - return p.prov.Predicate.RunDetails.BuildMetadata.FinishedOn, nil -} - -// GetNumberResolvedDependencies implements Provenance.GetNumberResolvedDependencies. -func (p *BYOBProvenance) GetNumberResolvedDependencies() (int, error) { - return len(p.prov.Predicate.BuildDefinition.ResolvedDependencies), nil -} -// GetSystemParameters implements Provenance.GetSystemParameters. -func (p *BYOBProvenance) GetSystemParameters() (map[string]any, error) { - sysParams, ok := p.prov.Predicate.BuildDefinition.InternalParameters.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "system parameters type") + refType, _ := utils.ParseGitRef(ref) + switch refType { + case "heads": // branch. + return "", nil + case "tags": + // NOTE: We return the full git ref. + return ref, nil + default: + return "", fmt.Errorf("%w: unknown ref type %q for ref %q", + serrors.ErrorInvalidDssePayload, refType, ref) } - - return sysParams, nil } diff --git a/verifiers/internal/gha/slsaprovenance/v1.0/byob_test.go b/verifiers/internal/gha/slsaprovenance/v1.0/byob_test.go new file mode 100644 index 000000000..bb593f9cd --- /dev/null +++ b/verifiers/internal/gha/slsaprovenance/v1.0/byob_test.go @@ -0,0 +1,279 @@ +package v1 + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + intoto "github.com/in-toto/in-toto-golang/in_toto" + slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + serrors "github.com/slsa-framework/slsa-verifier/v2/errors" +) + +func Test_BYOBProvenance_GetBranch(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + prov BYOBProvenance + branch string + err error + }{ + { + name: "empty provenance", + prov: BYOBProvenance{ + provenanceV1: &provenanceV1{ + prov: &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa1.ProvenancePredicate{}, + }, + }, + }, + err: serrors.ErrorInvalidDssePayload, + }, + { + name: "resolved dependency uri @ refs/heads/main", + prov: BYOBProvenance{ + provenanceV1: &provenanceV1{ + prov: &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa1.ProvenancePredicate{ + BuildDefinition: slsa1.ProvenanceBuildDefinition{ + ResolvedDependencies: []slsa1.ResourceDescriptor{ + { + URI: "git+https://github.com/kubernetes/kubernetes@refs/heads/main", + }, + }, + }, + }, + }, + }, + }, + branch: "refs/heads/main", + }, + { + name: "internalParameters GITHUB_REF @ refs/heads/main", + prov: BYOBProvenance{ + provenanceV1: &provenanceV1{ + prov: &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa1.ProvenancePredicate{ + BuildDefinition: slsa1.ProvenanceBuildDefinition{ + InternalParameters: map[string]interface{}{ + "GITHUB_REF_TYPE": "branch", + "GITHUB_REF": "refs/heads/main", + }, + }, + }, + }, + }, + }, + err: serrors.ErrorInvalidDssePayload, + }, + { + name: "resolved dependency uri @ refs/tags/v1.0.0", + prov: BYOBProvenance{ + provenanceV1: &provenanceV1{ + prov: &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa1.ProvenancePredicate{ + BuildDefinition: slsa1.ProvenanceBuildDefinition{ + InternalParameters: map[string]interface{}{ + "GITHUB_BASE_REF": "", + "GITHUB_REF_TYPE": "tag", + "GITHUB_REF": "refs/tags/v1.0.0", + "GITHUB_EVENT_NAME": "push", + "GITHUB_EVENT_PAYLOAD": map[string]any{ + "base_ref": nil, + }, + }, + ResolvedDependencies: []slsa1.ResourceDescriptor{ + { + URI: "git+https://github.com/kubernetes/kubernetes@refs/tags/v1.0.0", + }, + }, + }, + }, + }, + }, + }, + branch: "", + }, + { + name: "resolved dependency uri @ refs/heads/main no ref", + prov: BYOBProvenance{ + provenanceV1: &provenanceV1{ + prov: &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa1.ProvenancePredicate{ + BuildDefinition: slsa1.ProvenanceBuildDefinition{ + InternalParameters: map[string]interface{}{ + "GITHUB_REF_TYPE": "branch", + "GITHUB_REF": "refs/heads/main", + }, + ResolvedDependencies: []slsa1.ResourceDescriptor{ + { + URI: "git+https://github.com/kubernetes/kubernetes", + }, + }, + }, + }, + }, + }, + }, + err: serrors.ErrorInvalidDssePayload, + }, + } + + for i := range testCases { + tt := testCases[i] + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + branch, err := tt.prov.GetBranch() + if diff := cmp.Diff(tt.err, err, cmpopts.EquateErrors()); diff != "" { + t.Fatalf("unexpected error (-want +got): \n%s", diff) + } + if got, want := branch, tt.branch; got != want { + t.Fatalf("unexpected branch, got: %q, want: %q", got, want) + } + }) + } +} + +func Test_BYOBProvenance_GetTag(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + prov BYOBProvenance + tag string + err error + }{ + { + name: "empty provenance", + prov: BYOBProvenance{ + provenanceV1: &provenanceV1{ + prov: &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa1.ProvenancePredicate{}, + }, + }, + }, + err: serrors.ErrorInvalidDssePayload, + }, + { + name: "resolved dependency uri @ refs/heads/main", + prov: BYOBProvenance{ + provenanceV1: &provenanceV1{ + prov: &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa1.ProvenancePredicate{ + BuildDefinition: slsa1.ProvenanceBuildDefinition{ + ResolvedDependencies: []slsa1.ResourceDescriptor{ + { + URI: "git+https://github.com/kubernetes/kubernetes@refs/heads/main", + }, + }, + }, + }, + }, + }, + }, + tag: "", + }, + { + name: "internalParameters GITHUB_REF @ refs/heads/main", + prov: BYOBProvenance{ + provenanceV1: &provenanceV1{ + prov: &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa1.ProvenancePredicate{ + BuildDefinition: slsa1.ProvenanceBuildDefinition{ + InternalParameters: map[string]interface{}{ + "GITHUB_REF_TYPE": "branch", + "GITHUB_REF": "refs/heads/main", + }, + }, + }, + }, + }, + }, + err: serrors.ErrorInvalidDssePayload, + }, + { + name: "resolved dependency uri @ refs/tags/v1.0.0", + prov: BYOBProvenance{ + provenanceV1: &provenanceV1{ + prov: &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa1.ProvenancePredicate{ + BuildDefinition: slsa1.ProvenanceBuildDefinition{ + ResolvedDependencies: []slsa1.ResourceDescriptor{ + { + URI: "git+https://github.com/kubernetes/kubernetes@refs/tags/v1.0.0", + }, + }, + }, + }, + }, + }, + }, + tag: "refs/tags/v1.0.0", + }, + { + name: "internalParameters GITHUB_REF @ ref/tags/v1.0.0", + prov: BYOBProvenance{ + provenanceV1: &provenanceV1{ + prov: &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa1.ProvenancePredicate{ + BuildDefinition: slsa1.ProvenanceBuildDefinition{ + InternalParameters: map[string]interface{}{ + "GITHUB_REF_TYPE": "tag", + "GITHUB_REF": "refs/tags/v1.0.0", + }, + }, + }, + }, + }, + }, + err: serrors.ErrorInvalidDssePayload, + }, + { + name: "resolved dependency uri @ refs/tags/v1.0.0 no ref", + prov: BYOBProvenance{ + provenanceV1: &provenanceV1{ + prov: &Attestation{ + StatementHeader: intoto.StatementHeader{}, + Predicate: slsa1.ProvenancePredicate{ + BuildDefinition: slsa1.ProvenanceBuildDefinition{ + ResolvedDependencies: []slsa1.ResourceDescriptor{ + { + URI: "git+https://github.com/kubernetes/kubernetes", + }, + }, + }, + }, + }, + }, + }, + err: serrors.ErrorInvalidDssePayload, + }, + } + + for i := range testCases { + tt := testCases[i] + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + tag, err := tt.prov.GetTag() + if diff := cmp.Diff(tt.err, err, cmpopts.EquateErrors()); diff != "" { + t.Fatalf("unexpected error: %v", err) + } + if got, want := tag, tt.tag; got != want { + t.Fatalf("unexpected tag, got: %q, want: %q", got, want) + } + }) + } +} diff --git a/verifiers/internal/gha/slsaprovenance/v1.0/container_based.go b/verifiers/internal/gha/slsaprovenance/v1.0/container_based.go index 0767eb200..aa146aaab 100644 --- a/verifiers/internal/gha/slsaprovenance/v1.0/container_based.go +++ b/verifiers/internal/gha/slsaprovenance/v1.0/container_based.go @@ -1,11 +1,9 @@ package v1 -// ContainerBasedBuildType is the build type for the container-based builder and is based on BYOB. +// ContainerBasedBuildType is the build type for the container-based builder. var ContainerBasedBuildType = "https://slsa.dev/container-based-build/v0.1?draft" // ContainerBasedProvenance is provenance generated by the container-based builder. type ContainerBasedProvenance struct { - // NOTE: The Container-based builder is not based on BYOB framework but the - // provenanece is identical and can be treated the same (for now). - *BYOBProvenance + *provenanceV1 } diff --git a/verifiers/internal/gha/slsaprovenance/v1.0/provenance.go b/verifiers/internal/gha/slsaprovenance/v1.0/provenance.go index 741c77f66..6f44d9b9c 100644 --- a/verifiers/internal/gha/slsaprovenance/v1.0/provenance.go +++ b/verifiers/internal/gha/slsaprovenance/v1.0/provenance.go @@ -7,8 +7,9 @@ import ( intoto "github.com/in-toto/in-toto-golang/in_toto" slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" - serrors "github.com/slsa-framework/slsa-verifier/v2/errors" + serrors "github.com/slsa-framework/slsa-verifier/v2/errors" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/iface" ) @@ -23,8 +24,33 @@ type ProvenanceV1 interface { Predicate() slsa1.ProvenancePredicate } +type provFunc func(*Attestation) iface.Provenance + +func newBYOB(a *Attestation) iface.Provenance { + return &BYOBProvenance{ + provenanceV1: &provenanceV1{ + prov: a, + }, + } +} + +func newContainerBased(a *Attestation) iface.Provenance { + return &ContainerBasedProvenance{ + provenanceV1: &provenanceV1{ + prov: a, + }, + } +} + +// buildTypeMap is a map of builder IDs to supported buildTypes. +var buildTypeMap = map[string]map[string]provFunc{ + common.GenericDelegatorBuilderID: {common.BYOBBuildTypeV0: newBYOB}, + common.GenericLowPermsDelegatorBuilderID: {common.BYOBBuildTypeV0: newBYOB}, + common.ContainerBasedBuilderID: {common.ContainerBasedBuildTypeV01Draft: newContainerBased}, +} + // New returns a new Provenance object based on the payload. -func New(payload []byte) (iface.Provenance, error) { +func New(builderID string, payload []byte) (iface.Provenance, error) { // Strict unmarshal. // NOTE: this supports extensions because they are // only used as part of interface{}-defined fields. @@ -33,21 +59,18 @@ func New(payload []byte) (iface.Provenance, error) { a := &Attestation{} if err := dec.Decode(a); err != nil { - return nil, err + return nil, fmt.Errorf("%w: %w", serrors.ErrorInvalidDssePayload, err) } - switch a.Predicate.BuildDefinition.BuildType { - case BYOBBuildType: - return &BYOBProvenance{ - prov: a, - }, nil - case ContainerBasedBuildType: - return &ContainerBasedProvenance{ - BYOBProvenance: &BYOBProvenance{ - prov: a, - }, - }, nil - default: - return nil, fmt.Errorf("%w: unknown buildType: %q", serrors.ErrorInvalidDssePayload, a.Predicate.BuildDefinition.BuildType) + btMap, ok := buildTypeMap[builderID] + if !ok { + return nil, fmt.Errorf("%w: %q", serrors.ErrorInvalidBuilderID, builderID) + } + + provFunc, ok := btMap[a.Predicate.BuildDefinition.BuildType] + if !ok { + return nil, fmt.Errorf("%w: %q for builder ID %q", serrors.ErrorInvalidBuildType, a.Predicate.BuildDefinition.BuildType, builderID) } + + return provFunc(a), nil } diff --git a/verifiers/internal/gha/slsaprovenance/v1.0/provenance_test.go b/verifiers/internal/gha/slsaprovenance/v1.0/provenance_test.go new file mode 100644 index 000000000..3b5d5157f --- /dev/null +++ b/verifiers/internal/gha/slsaprovenance/v1.0/provenance_test.go @@ -0,0 +1,118 @@ +package v1 + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + + serrors "github.com/slsa-framework/slsa-verifier/v2/errors" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/iface" +) + +func Test_New(t *testing.T) { + testCases := []struct { + name string + builderID string + payload string + prov iface.Provenance + err error + }{ + { + name: "BYOB build type", + builderID: common.GenericDelegatorBuilderID, + payload: fmt.Sprintf(`{ + "predicate": { + "buildDefinition": { + "buildType": %q + } + } + }`, common.BYOBBuildTypeV0), + prov: &BYOBProvenance{ + provenanceV1: &provenanceV1{ + prov: &Attestation{ + Predicate: slsa1.ProvenancePredicate{ + BuildDefinition: slsa1.ProvenanceBuildDefinition{ + BuildType: common.BYOBBuildTypeV0, + }, + }, + }, + }, + }, + }, + { + name: "Container-based build type", + builderID: common.ContainerBasedBuilderID, + payload: fmt.Sprintf(`{ + "predicate": { + "buildDefinition": { + "buildType": %q + } + } + }`, common.ContainerBasedBuildTypeV01Draft), + prov: &ContainerBasedProvenance{ + provenanceV1: &provenanceV1{ + prov: &Attestation{ + Predicate: slsa1.ProvenancePredicate{ + BuildDefinition: slsa1.ProvenanceBuildDefinition{ + BuildType: common.ContainerBasedBuildTypeV01Draft, + }, + }, + }, + }, + }, + }, + { + name: "Unknown fields", + payload: `{ + "predicate": { + "unknown": "field", + "buildDefinition": { + "buildType": "foo" + } + } + }`, + err: serrors.ErrorInvalidDssePayload, + }, + { + name: "Unknown builder ID", + builderID: "unknown", + payload: `{ + "predicate": { + "buildDefinition": { + "buildType": "foo" + } + } + }`, + err: serrors.ErrorInvalidBuilderID, + }, + { + name: "Unknown buildType", + builderID: common.GenericDelegatorBuilderID, + payload: `{ + "predicate": { + "buildDefinition": { + "buildType": "foo" + } + } + }`, + err: serrors.ErrorInvalidBuildType, + }, + } + + for i := range testCases { + tt := testCases[i] + t.Run(tt.name, func(t *testing.T) { + p, err := New(tt.builderID, []byte(tt.payload)) + if diff := cmp.Diff(tt.err, err, cmpopts.EquateErrors()); diff != "" { + t.Fatalf("unexpected error (-want +got): \n%s", diff) + } + if diff := cmp.Diff(tt.prov, p, cmp.AllowUnexported(provenanceV1{}, BYOBProvenance{}, ContainerBasedProvenance{})); diff != "" { + t.Fatalf("unexpected result (-want +got): \n%s", diff) + } + }) + } +} diff --git a/verifiers/internal/gha/verifier.go b/verifiers/internal/gha/verifier.go index 139fd3698..d49090fc5 100644 --- a/verifiers/internal/gha/verifier.go +++ b/verifiers/internal/gha/verifier.go @@ -55,7 +55,7 @@ func verifyEnvAndCert(env *dsse.Envelope, } // Verify the builder identity. - builderID, byob, err := VerifyBuilderIdentity(workflowInfo, builderOpts, defaultBuilders) + verifiedBuilderID, byob, err := VerifyBuilderIdentity(workflowInfo, builderOpts, defaultBuilders) if err != nil { return nil, nil, err } @@ -67,7 +67,7 @@ func verifyEnvAndCert(env *dsse.Envelope, // Verify properties of the SLSA provenance. // Unpack and verify info in the provenance, including the subject Digest. - provenanceOpts.ExpectedBuilderID = builderID.String() + provenanceOpts.ExpectedBuilderID = verifiedBuilderID.String() // There is a corner-case to handle: if the verified builder ID from the cert // is a delegator builder, the user MUST provide an expected builder ID // and we MUST match it against the content of the provenance. @@ -79,7 +79,7 @@ func verifyEnvAndCert(env *dsse.Envelope, } provenanceOpts.ExpectedBuilderID = *builderOpts.ExpectedID } - if err := VerifyProvenance(env, provenanceOpts, byob); err != nil { + if err := VerifyProvenance(env, provenanceOpts, verifiedBuilderID, byob); err != nil { return nil, nil, err } @@ -92,7 +92,7 @@ func verifyEnvAndCert(env *dsse.Envelope, return nil, nil, err } - return r, builderID, nil + return r, verifiedBuilderID, nil } func verifyNpmEnvAndCert(env *dsse.Envelope, @@ -178,7 +178,7 @@ func verifyNpmEnvAndCert(env *dsse.Envelope, return nil, fmt.Errorf("%w: re-usable workflow is GitHub-hosted", serrors.ErrorMismatchBuilderID) } default: - return nil, fmt.Errorf("%w: builder %q. Expected one of %q, %q", serrors.ErrorNotSupported, *builderOpts.ExpectedID, + return nil, fmt.Errorf("%w: builder %v. Expected one of %v, %v", serrors.ErrorNotSupported, *builderOpts.ExpectedID, common.NpmCLISelfHostedBuilderID, common.NpmCLIHostedBuilderID) } @@ -193,7 +193,7 @@ func verifyNpmEnvAndCert(env *dsse.Envelope, // Verify properties of the SLSA provenance. // Unpack and verify info in the provenance, including the Subject Digest. - if err := VerifyNpmPackageProvenance(env, workflowInfo, provenanceOpts, isTrustedBuilder); err != nil { + if err := VerifyNpmPackageProvenance(env, workflowInfo, provenanceOpts, trustedBuilderID, isTrustedBuilder); err != nil { return nil, err } @@ -330,6 +330,14 @@ func (v *GHAVerifier) VerifyNpmPackage(ctx context.Context, return nil, nil, err } + // Verify builder information. + builder, err := npm.verifyBuilderID( + provenanceOpts, builderOpts, + defaultBYOBReusableWorkflows) + if err != nil { + return nil, nil, err + } + // Verify attestation headers. if err := npm.verifyIntotoHeaders(); err != nil { return nil, nil, err @@ -346,15 +354,6 @@ func (v *GHAVerifier) VerifyNpmPackage(ctx context.Context, } } - // Verify certificate information. - builder, err := verifyNpmEnvAndCert(npm.ProvenanceEnvelope(), - npm.ProvenanceLeafCertificate(), - provenanceOpts, builderOpts, - defaultBYOBReusableWorkflows) - if err != nil { - return nil, nil, err - } - prov, err := npm.verifiedProvenanceBytes() if err != nil { return nil, nil, err