diff --git a/verifiers/internal/gha/npm.go b/verifiers/internal/gha/npm.go index 2ac0f53..5c8e3e5 100644 --- a/verifiers/internal/gha/npm.go +++ b/verifiers/internal/gha/npm.go @@ -16,6 +16,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" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" "github.com/slsa-framework/slsa-verifier/v2/verifiers/utils" ) @@ -114,7 +115,7 @@ func extractAttestations(attestations []attestation) (*attestation, *attestation for i := range attestations { att := attestations[i] // Provenance type verification. - if att.PredicateType == slsaprovenance.ProvenanceV02Type { + if att.PredicateType == common.ProvenanceV02Type { provenanceAttestation = &att } // Publish type verification. @@ -196,7 +197,7 @@ func (n *Npm) verifyPublishAttesttationSignature() error { func (n *Npm) verifyIntotoHeaders() error { if err := verifyIntotoTypes(n.verifiedProvenanceAtt, - slsaprovenance.ProvenanceV02Type, intoto.PayloadType, false); err != nil { + common.ProvenanceV02Type, intoto.PayloadType, false); err != nil { return err } if err := verifyIntotoTypes(n.verifiedPublishAtt, diff --git a/verifiers/internal/gha/npm_test.go b/verifiers/internal/gha/npm_test.go index fee31d9..14788b3 100644 --- a/verifiers/internal/gha/npm_test.go +++ b/verifiers/internal/gha/npm_test.go @@ -12,7 +12,7 @@ import ( 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" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" ) func Test_verifyName(t *testing.T) { @@ -671,7 +671,7 @@ func Test_verifyIntotoTypes(t *testing.T) { }{ { name: "prov correct", - predicateType: slsaprovenance.ProvenanceV02Type, + predicateType: common.ProvenanceV02Type, payloadType: intoto.PayloadType, att: &SignedAttestation{ Envelope: &dsselib.Envelope{ @@ -682,7 +682,7 @@ func Test_verifyIntotoTypes(t *testing.T) { }, { name: "prov mismatch payload type", - predicateType: slsaprovenance.ProvenanceV02Type, + predicateType: common.ProvenanceV02Type, payloadType: intoto.PayloadType, att: &SignedAttestation{ Envelope: &dsselib.Envelope{ @@ -694,7 +694,7 @@ func Test_verifyIntotoTypes(t *testing.T) { }, { name: "prov mismatch predicate type", - predicateType: slsaprovenance.ProvenanceV02Type + "a", + predicateType: common.ProvenanceV02Type + "a", payloadType: intoto.PayloadType, att: &SignedAttestation{ Envelope: &dsselib.Envelope{ diff --git a/verifiers/internal/gha/provenance.go b/verifiers/internal/gha/provenance.go index 28a8dc3..731fb8f 100644 --- a/verifiers/internal/gha/provenance.go +++ b/verifiers/internal/gha/provenance.go @@ -16,11 +16,9 @@ import ( 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/internal/gha/slsaprovenance/iface" "github.com/slsa-framework/slsa-verifier/v2/verifiers/utils" - - // Load provenance types. - - _ "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/v1.0" ) // SignedAttestation contains a signed DSSE envelope @@ -34,6 +32,7 @@ type SignedAttestation struct { RekorEntry *models.LogEntryAnon } +// EnvelopeFromBytes reads a DSSE envelope from the given payload. func EnvelopeFromBytes(payload []byte) (env *dsselib.Envelope, err error) { env = &dsselib.Envelope{} err = json.Unmarshal(payload, env) @@ -43,7 +42,7 @@ func EnvelopeFromBytes(payload []byte) (env *dsselib.Envelope, err error) { // Verify Builder ID in provenance statement. // This function does an exact comparison, and expects expectedBuilderID to be the full // `name@refs/tags/`. -func verifyBuilderIDExactMatch(prov slsaprovenance.Provenance, expectedBuilderID string) error { +func verifyBuilderIDExactMatch(prov iface.Provenance, expectedBuilderID string) error { id, err := prov.BuilderID() if err != nil { return err @@ -62,7 +61,7 @@ func verifyBuilderIDExactMatch(prov slsaprovenance.Provenance, expectedBuilderID // Verify Builder ID in provenance statement. // This function verifies the names match. If the expected builder ID contains a version, // it also verifies the versions match. -func verifyBuilderIDLooseMatch(prov slsaprovenance.Provenance, expectedBuilderID string) error { +func verifyBuilderIDLooseMatch(prov iface.Provenance, expectedBuilderID string) error { id, err := prov.BuilderID() if err != nil { return err @@ -91,7 +90,7 @@ func asURI(s string) string { } // Verify source URI in provenance statement. -func verifySourceURI(prov slsaprovenance.Provenance, expectedSourceURI string, allowNoMaterialRef bool) error { +func verifySourceURI(prov iface.Provenance, expectedSourceURI string, allowNoMaterialRef bool) error { source := asURI(expectedSourceURI) // We expect github.com URIs only. @@ -172,7 +171,7 @@ func sourceFromURI(uri string, allowNoRef bool) (string, error) { } // Verify Subject Digest from the provenance statement. -func verifyDigest(prov slsaprovenance.Provenance, expectedHash string) error { +func verifyDigest(prov iface.Provenance, expectedHash string) error { subjects, err := prov.Subjects() if err != nil { return err @@ -221,6 +220,7 @@ func VerifyProvenanceSignature(ctx context.Context, trustedRoot *TrustedRoot, provenance, rClient, trustedRoot) } +// VerifyNpmPackageProvenance verifies provenance for an npm package. func VerifyNpmPackageProvenance(env *dsselib.Envelope, workflow *WorkflowIdentity, provenanceOpts *options.ProvenanceOpts, isTrustedBuilder bool, ) error { @@ -282,7 +282,7 @@ func VerifyNpmPackageProvenance(env *dsselib.Envelope, workflow *WorkflowIdentit return nil } -func isValidDelegatorBuilderID(prov slsaprovenance.Provenance) error { +func isValidDelegatorBuilderID(prov iface.Provenance) error { // Verify the TRW was referenced at a proper tag by the user. id, err := prov.BuilderID() if err != nil { @@ -295,6 +295,7 @@ func isValidDelegatorBuilderID(prov slsaprovenance.Provenance) error { return utils.IsValidBuilderTag(parts[1], false) } +// 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) @@ -322,7 +323,8 @@ func VerifyProvenance(env *dsselib.Envelope, provenanceOpts *options.ProvenanceO return VerifyProvenanceCommonOptions(prov, provenanceOpts, false) } -func VerifyProvenanceCommonOptions(prov slsaprovenance.Provenance, provenanceOpts *options.ProvenanceOpts, +// VerifyProvenanceCommonOptions verifies the given provenance. +func VerifyProvenanceCommonOptions(prov iface.Provenance, provenanceOpts *options.ProvenanceOpts, allowNoMaterialRef bool, ) error { // Verify source. @@ -366,7 +368,9 @@ func VerifyProvenanceCommonOptions(prov slsaprovenance.Provenance, provenanceOpt return nil } -func VerifyWorkflowInputs(prov slsaprovenance.Provenance, inputs map[string]string) error { +// VerifyWorkflowInputs verifies that the workflow inputs in the provenance +// match the expected values. +func VerifyWorkflowInputs(prov iface.Provenance, inputs map[string]string) error { pyldInputs, err := prov.GetWorkflowInputs() if err != nil { return err @@ -374,7 +378,7 @@ func VerifyWorkflowInputs(prov slsaprovenance.Provenance, inputs map[string]stri // Verify all inputs. for k, v := range inputs { - value, err := slsaprovenance.GetAsString(pyldInputs, k) + value, err := common.GetAsString(pyldInputs, k) if err != nil { return fmt.Errorf("%w: cannot retrieve value of '%s'", serrors.ErrorMismatchWorkflowInputs, k) } @@ -390,7 +394,7 @@ func VerifyWorkflowInputs(prov slsaprovenance.Provenance, inputs map[string]stri // VerifyBranch verifies that the source branch in the provenance matches the // expected value. -func VerifyBranch(prov slsaprovenance.Provenance, expectedBranch string) error { +func VerifyBranch(prov iface.Provenance, expectedBranch string) error { ref, err := prov.GetBranch() if err != nil { return err @@ -410,7 +414,7 @@ func VerifyBranch(prov slsaprovenance.Provenance, expectedBranch string) error { // VerifyTag verifies that the source tag in the provenance matches the // expected value. -func VerifyTag(prov slsaprovenance.Provenance, expectedTag string) error { +func VerifyTag(prov iface.Provenance, expectedTag string) error { ref, err := prov.GetTag() if err != nil { return err @@ -430,7 +434,7 @@ func VerifyTag(prov slsaprovenance.Provenance, expectedTag string) error { // VerifyVersionedTag verifies that the source tag in the provenance matches the // expected semver value. -func VerifyVersionedTag(prov slsaprovenance.Provenance, expectedTag string) error { +func VerifyVersionedTag(prov iface.Provenance, expectedTag string) error { // Retrieve, validate and canonicalize the provenance tag. // Note: prerelease is validated as part of patch validation // and must be equal. Build is discarded as per https://semver.org/: diff --git a/verifiers/internal/gha/provenance_forgeable.go b/verifiers/internal/gha/provenance_forgeable.go index c0273a8..d673e9f 100644 --- a/verifiers/internal/gha/provenance_forgeable.go +++ b/verifiers/internal/gha/provenance_forgeable.go @@ -6,14 +6,12 @@ import ( "strings" serrors "github.com/slsa-framework/slsa-verifier/v2/errors" - "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance" - - // Load provenance types. + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/iface" slsav02 "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/v0.2" - _ "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/v1.0" ) -func verifyProvenanceMatchesCertificate(prov slsaprovenance.Provenance, workflow *WorkflowIdentity) error { +func verifyProvenanceMatchesCertificate(prov iface.Provenance, workflow *WorkflowIdentity) error { // See the generation at https://github.com/npm/cli/blob/latest/workspaces/libnpmpublish/lib/provenance.js. // Verify systemParameters. if err := verifySystemParameters(prov, workflow); err != nil { @@ -65,7 +63,7 @@ func verifyProvenanceMatchesCertificate(prov slsaprovenance.Provenance, workflow return nil } -func verifySubjectDigestName(prov slsaprovenance.Provenance, digestName string) error { +func verifySubjectDigestName(prov iface.Provenance, digestName string) error { subjects, err := prov.Subjects() if err != nil { return err @@ -84,7 +82,7 @@ func verifySubjectDigestName(prov slsaprovenance.Provenance, digestName string) return nil } -func verifyBuildConfig(prov slsaprovenance.Provenance, workflow *WorkflowIdentity) error { +func verifyBuildConfig(prov iface.Provenance, workflow *WorkflowIdentity) error { triggerPath, err := prov.GetBuildTriggerPath() if err != nil { // If the field is not available in the provenance, @@ -98,7 +96,7 @@ func verifyBuildConfig(prov slsaprovenance.Provenance, workflow *WorkflowIdentit return equalCertificateValue(workflow.BuildConfigPath, triggerPath, "trigger workflow") } -func verifyResolvedDependencies(prov slsaprovenance.Provenance) error { +func verifyResolvedDependencies(prov iface.Provenance) error { n, err := prov.GetNumberResolvedDependencies() if err != nil { return err @@ -110,7 +108,7 @@ func verifyResolvedDependencies(prov slsaprovenance.Provenance) error { return nil } -func verifyMetadata(prov slsaprovenance.Provenance, workflow *WorkflowIdentity) error { +func verifyMetadata(prov iface.Provenance, workflow *WorkflowIdentity) error { if err := verifyCommonMetadata(prov, workflow); err != nil { return err } @@ -125,7 +123,7 @@ func verifyMetadata(prov slsaprovenance.Provenance, workflow *WorkflowIdentity) return nil } -func verifyCommonMetadata(prov slsaprovenance.Provenance, workflow *WorkflowIdentity) error { +func verifyCommonMetadata(prov iface.Provenance, workflow *WorkflowIdentity) error { // Verify build invocation ID. invocationID, err := prov.GetBuildInvocationID() if err != nil { @@ -169,7 +167,7 @@ func verifyCommonMetadata(prov slsaprovenance.Provenance, workflow *WorkflowIden return nil } -func verifyV02Metadata(prov slsaprovenance.Provenance) error { +func verifyV02Metadata(prov iface.Provenance) error { // https://github.com/in-toto/in-toto-golang/blob/master/in_toto/slsa_provenance/v0.2/provenance.go /* v0.2: @@ -181,21 +179,23 @@ func verifyV02Metadata(prov slsaprovenance.Provenance) error { }, "reproducible": false */ - prov02, ok := prov.(*slsav02.ProvenanceV02) + prov02, ok := prov.(slsav02.ProvenanceV02) if !ok { return nil } - if prov02.Predicate.Metadata == nil { + predicate := prov02.Predicate() + + if predicate.Metadata == nil { return nil } - if prov02.Predicate.Metadata.Reproducible { + if predicate.Metadata.Reproducible { return fmt.Errorf("%w: reproducible: %v", serrors.ErrorNonVerifiableClaim, - prov02.Predicate.Metadata.Reproducible) + predicate.Metadata.Reproducible) } - completeness := prov02.Predicate.Metadata.Completeness + completeness := predicate.Metadata.Completeness if completeness.Parameters || completeness.Materials || completeness.Environment { return fmt.Errorf("%w: completeness: %v", @@ -205,44 +205,47 @@ func verifyV02Metadata(prov slsaprovenance.Provenance) error { return nil } -func verifyV02Parameters(prov slsaprovenance.Provenance) error { +func verifyV02Parameters(prov iface.Provenance) error { // https://github.com/in-toto/in-toto-golang/blob/master/in_toto/slsa_provenance/v0.2/provenance.go - prov02, ok := prov.(*slsav02.ProvenanceV02) + prov02, ok := prov.(slsav02.ProvenanceV02) if !ok { return nil } - if prov02.Predicate.Invocation.Parameters == nil { + predicate := prov02.Predicate() + + if predicate.Invocation.Parameters == nil { return nil } - m, ok := prov02.Predicate.Invocation.Parameters.(map[string]any) + m, ok := predicate.Invocation.Parameters.(map[string]any) if !ok || len(m) > 0 { return fmt.Errorf("%w: parameters: %v", - serrors.ErrorNonVerifiableClaim, prov02.Predicate.Invocation.Parameters) + serrors.ErrorNonVerifiableClaim, predicate.Invocation.Parameters) } return nil } -func verifyV02BuildConfig(prov slsaprovenance.Provenance) error { +func verifyV02BuildConfig(prov iface.Provenance) error { // https://github.com/in-toto/in-toto-golang/blob/master/in_toto/slsa_provenance/v0.2/provenance.go - prov02, ok := prov.(*slsav02.ProvenanceV02) + prov02, ok := prov.(slsav02.ProvenanceV02) if !ok { return nil } + predicate := prov02.Predicate() - if prov02.Predicate.BuildConfig == nil { + if predicate.BuildConfig == nil { return nil } - m, ok := prov02.Predicate.BuildConfig.(map[string]any) + m, ok := predicate.BuildConfig.(map[string]any) if !ok || len(m) > 0 { return fmt.Errorf("%w: buildConfig: %v", - serrors.ErrorNonVerifiableClaim, prov02.Predicate.BuildConfig) + serrors.ErrorNonVerifiableClaim, predicate.BuildConfig) } return nil } -func verifySystemParameters(prov slsaprovenance.Provenance, workflow *WorkflowIdentity) error { +func verifySystemParameters(prov iface.Provenance, workflow *WorkflowIdentity) error { /* "environment": { "GITHUB_EVENT_NAME": "workflow_dispatch", @@ -338,7 +341,7 @@ func getRunIDs(workflow *WorkflowIdentity) (string, string, error) { func verifySystemRun(params map[string]any, workflow *WorkflowIdentity) error { // Verify only if the values are provided in the provenance. - if !slsaprovenance.Exists(params, "GITHUB_RUN_ID") && !slsaprovenance.Exists(params, "GITHUB_RUN_ATTEMPT") { + if !common.Exists(params, "GITHUB_RUN_ID") && !common.Exists(params, "GITHUB_RUN_ATTEMPT") { return nil } // The certificate contains runID as '4757060009/attempts/1'. @@ -364,11 +367,11 @@ func verifySystemRun(params map[string]any, workflow *WorkflowIdentity) error { func verifySystemParameter(params map[string]any, name string, certValue *string) error { // If the provenance does not contain an env variable. - if !slsaprovenance.Exists(params, name) { + if !common.Exists(params, name) { return nil } // Provenance contains the field, we must verify it. - provValue, err := slsaprovenance.GetAsString(params, name) + provValue, err := common.GetAsString(params, name) if err != nil { return err } diff --git a/verifiers/internal/gha/provenance_forgeable_test.go b/verifiers/internal/gha/provenance_forgeable_test.go index 3562e82..94abb0d 100644 --- a/verifiers/internal/gha/provenance_forgeable_test.go +++ b/verifiers/internal/gha/provenance_forgeable_test.go @@ -1,7 +1,6 @@ package gha import ( - "fmt" "testing" "time" @@ -9,10 +8,7 @@ import ( intoto "github.com/in-toto/in-toto-golang/in_toto" intotocommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" intotov02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" - intotov1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" serrors "github.com/slsa-framework/slsa-verifier/v2/errors" - slsav02 "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/v0.2" - slsav10 "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/v1.0" ) func Test_verifySubjectDigestName(t *testing.T) { @@ -48,7 +44,7 @@ func Test_verifySubjectDigestName(t *testing.T) { { name: "invalid no subjects", digestName: "sha256", - err: serrors.ErrorInvalidDssePayload, + err: serrors.ErrorNonVerifiableClaim, }, { name: "wrong digest", @@ -66,25 +62,10 @@ func Test_verifySubjectDigestName(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - prov02 := &slsav02.ProvenanceV02{ - &intoto.ProvenanceStatement{ - StatementHeader: intoto.StatementHeader{ - Subject: tt.subject, - }, - }, + prov := &testProvenance{ + subjects: tt.subject, } - err := verifySubjectDigestName(prov02, tt.digestName) - if !errCmp(err, tt.err) { - t.Errorf(cmp.Diff(err, tt.err)) - } - - prov1 := &slsav10.ProvenanceV1{ - StatementHeader: intoto.StatementHeader{ - Subject: tt.subject, - }, - } - err = verifySubjectDigestName(prov1, tt.digestName) - if !errCmp(err, tt.err) { + if err := verifySubjectDigestName(prov, tt.digestName); !errCmp(err, tt.err) { t.Errorf(cmp.Diff(err, tt.err)) } }) @@ -125,33 +106,10 @@ func Test_verifyBuildConfig(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - prov02 := &slsav02.ProvenanceV02{ - &intoto.ProvenanceStatement{ - Predicate: intotov02.ProvenancePredicate{ - Invocation: intotov02.ProvenanceInvocation{ - ConfigSource: intotov02.ConfigSource{ - EntryPoint: tt.path, - }, - }, - }, - }, + prov := &testProvenance{ + buildTriggerPath: tt.path, } - err := verifyBuildConfig(prov02, &tt.workflow) - if !errCmp(err, tt.err) { - t.Errorf(cmp.Diff(err, tt.err)) - } - - prov1 := &slsav10.ProvenanceV1{ - Predicate: intotov1.ProvenancePredicate{ - BuildDefinition: intotov1.ProvenanceBuildDefinition{ - InternalParameters: map[string]interface{}{ - "GITHUB_WORKFLOW_REF": fmt.Sprintf("some/repo/%s@some-ref", tt.path), - }, - }, - }, - } - err = verifyBuildConfig(prov1, &tt.workflow) - if !errCmp(err, tt.err) { + if err := verifyBuildConfig(prov, &tt.workflow); !errCmp(err, tt.err) { t.Errorf(cmp.Diff(err, tt.err)) } }) @@ -186,29 +144,10 @@ func Test_verifyResolvedDependencies(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - prov02 := &slsav02.ProvenanceV02{ - &intoto.ProvenanceStatement{ - Predicate: intotov02.ProvenancePredicate{}, - }, + prov := &testProvenance{ + noResolvedDeps: tt.n, } - if tt.n > 0 { - prov02.Predicate.Materials = make([]intotocommon.ProvenanceMaterial, tt.n) - } - err := verifyResolvedDependencies(prov02) - if !errCmp(err, tt.err) { - t.Errorf(cmp.Diff(err, tt.err)) - } - - prov1 := &slsav10.ProvenanceV1{ - Predicate: intotov1.ProvenancePredicate{ - BuildDefinition: intotov1.ProvenanceBuildDefinition{}, - }, - } - if tt.n > 0 { - prov1.Predicate.BuildDefinition.ResolvedDependencies = make([]intotov1.ResourceDescriptor, tt.n) - } - err = verifyResolvedDependencies(prov1) - if !errCmp(err, tt.err) { + if err := verifyResolvedDependencies(prov); !errCmp(err, tt.err) { t.Errorf(cmp.Diff(err, tt.err)) } }) @@ -296,45 +235,19 @@ func Test_verifyCommonMetadata(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() - metadata := tt.metadata || tt.invocationID != nil || tt.startTime != nil || - tt.endTime != nil - prov02 := &slsav02.ProvenanceV02{ - &intoto.ProvenanceStatement{ - Predicate: intotov02.ProvenancePredicate{}, - }, - } - if metadata { - prov02.Predicate.Metadata = &intotov02.ProvenanceMetadata{} - } + + prov := &testProvenance{} if tt.invocationID != nil { - prov02.Predicate.Metadata.BuildInvocationID = *tt.invocationID + prov.buildInvocationID = *tt.invocationID } if tt.startTime != nil { - prov02.Predicate.Metadata.BuildStartedOn = tt.startTime + prov.buildStartTime = tt.startTime } if tt.endTime != nil { - prov02.Predicate.Metadata.BuildFinishedOn = tt.endTime + prov.buildFinishTime = tt.endTime } - err := verifyCommonMetadata(prov02, &tt.workflow) - if !errCmp(err, tt.err) { - t.Errorf(cmp.Diff(err, tt.err)) - } - - prov1 := &slsav10.ProvenanceV1{} - - if tt.invocationID != nil { - prov1.Predicate.RunDetails.BuildMetadata.InvocationID = *tt.invocationID - } - if tt.startTime != nil { - prov1.Predicate.RunDetails.BuildMetadata.StartedOn = tt.startTime - } - if tt.endTime != nil { - prov1.Predicate.RunDetails.BuildMetadata.FinishedOn = tt.endTime - } - - err = verifyCommonMetadata(prov1, &tt.workflow) - if !errCmp(err, tt.err) { + if err := verifyCommonMetadata(prov, &tt.workflow); !errCmp(err, tt.err) { t.Errorf(cmp.Diff(err, tt.err)) } }) @@ -382,15 +295,12 @@ func Test_verifyV02Metadata(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() - metadata := tt.metadata || tt.reproducible || tt.parameters || tt.environment || tt.materials - prov02 := &slsav02.ProvenanceV02{ - &intoto.ProvenanceStatement{}, - } + prov02 := &testProvenanceV02{} if metadata { - prov02.Predicate.Metadata = &intotov02.ProvenanceMetadata{ + prov02.predicate.Metadata = &intotov02.ProvenanceMetadata{ Completeness: intotov02.ProvenanceComplete{ Parameters: tt.parameters, Materials: tt.materials, @@ -399,8 +309,7 @@ func Test_verifyV02Metadata(t *testing.T) { Reproducible: tt.reproducible, } } - err := verifyV02Metadata(prov02) - if !errCmp(err, tt.err) { + if err := verifyV02Metadata(prov02); !errCmp(err, tt.err) { t.Errorf(cmp.Diff(err, tt.err)) } }) @@ -441,11 +350,9 @@ func Test_verifyV02Parameters(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - prov02 := &slsav02.ProvenanceV02{ - &intoto.ProvenanceStatement{}, - } + prov02 := &testProvenanceV02{} if tt.present || len(tt.value) > 0 { - prov02.Predicate.Invocation.Parameters = tt.value + prov02.predicate.Invocation.Parameters = tt.value } err := verifyV02Parameters(prov02) if !errCmp(err, tt.err) { @@ -489,11 +396,9 @@ func Test_verifyV02BuildConfig(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - prov02 := &slsav02.ProvenanceV02{ - &intoto.ProvenanceStatement{}, - } + prov02 := &testProvenanceV02{} if tt.present || len(tt.value) > 0 { - prov02.Predicate.BuildConfig = tt.value + prov02.predicate.BuildConfig = tt.value } err := verifyV02BuildConfig(prov02) if !errCmp(err, tt.err) { @@ -616,52 +521,43 @@ func Test_verifyMetadata(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - metadata := tt.metadata || tt.invocationID != nil || tt.startTime != nil || - tt.endTime != nil || tt.reproducible || tt.parameters || - tt.environment || tt.materials - - prov02 := &slsav02.ProvenanceV02{ - &intoto.ProvenanceStatement{}, - } - if metadata { - prov02.Predicate.Metadata = &intotov02.ProvenanceMetadata{ - Completeness: intotov02.ProvenanceComplete{ - Parameters: tt.parameters, - Materials: tt.materials, - Environment: tt.environment, - }, - Reproducible: tt.reproducible, - } - if tt.invocationID != nil { - prov02.Predicate.Metadata.BuildInvocationID = *tt.invocationID - } - if tt.startTime != nil { - prov02.Predicate.Metadata.BuildStartedOn = tt.startTime - } - if tt.endTime != nil { - prov02.Predicate.Metadata.BuildFinishedOn = tt.endTime - } - } - errV02 := verifyMetadata(prov02, &tt.workflow) - if !errCmp(errV02, tt.errV02) { - t.Errorf(cmp.Diff(errV02, tt.errV02)) - } - - prov1 := &slsav10.ProvenanceV1{} - + prov02 := &testProvenanceV02{} if tt.invocationID != nil { - prov1.Predicate.RunDetails.BuildMetadata.InvocationID = *tt.invocationID + prov02.buildInvocationID = *tt.invocationID } if tt.startTime != nil { - prov1.Predicate.RunDetails.BuildMetadata.StartedOn = tt.startTime + prov02.buildStartTime = tt.startTime } if tt.endTime != nil { - prov1.Predicate.RunDetails.BuildMetadata.FinishedOn = tt.endTime + prov02.buildFinishTime = tt.endTime } - errV01 := verifyMetadata(prov1, &tt.workflow) - if !errCmp(errV01, tt.errV01) { - t.Errorf(cmp.Diff(errV01, tt.errV01)) + prov02.predicate.Metadata = &intotov02.ProvenanceMetadata{ + Completeness: intotov02.ProvenanceComplete{ + Parameters: tt.parameters, + Materials: tt.materials, + Environment: tt.environment, + }, + Reproducible: tt.reproducible, + } + + if err := verifyMetadata(prov02, &tt.workflow); !errCmp(err, tt.errV02) { + t.Errorf(cmp.Diff(err, tt.errV02)) + } + + prov1 := &testProvenanceV1{} + if tt.invocationID != nil { + prov1.buildInvocationID = *tt.invocationID + } + if tt.startTime != nil { + prov1.buildStartTime = tt.startTime + } + if tt.endTime != nil { + prov1.buildFinishTime = tt.endTime + } + + if err := verifyMetadata(prov1, &tt.workflow); !errCmp(err, tt.errV01) { + t.Errorf(cmp.Diff(err, tt.errV01)) } }) } @@ -1038,30 +934,12 @@ func Test_verifySystemParameters(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() - prov02 := &slsav02.ProvenanceV02{ - &intoto.ProvenanceStatement{ - Predicate: intotov02.ProvenancePredicate{ - Invocation: intotov02.ProvenanceInvocation{ - Environment: tt.environment, - }, - }, - }, + + prov := &testProvenance{ + systemParameters: tt.environment, } - err := verifySystemParameters(prov02, &tt.workflow) - if !errCmp(err, tt.err) { - t.Errorf(cmp.Diff(err, tt.err)) - } - - prov1 := &slsav10.ProvenanceV1{ - Predicate: intotov1.ProvenancePredicate{ - BuildDefinition: intotov1.ProvenanceBuildDefinition{ - InternalParameters: tt.environment, - }, - }, - } - err = verifySystemParameters(prov1, &tt.workflow) - if !errCmp(err, tt.err) { + if err := verifySystemParameters(prov, &tt.workflow); !errCmp(err, tt.err) { t.Errorf(cmp.Diff(err, tt.err)) } }) @@ -1127,6 +1005,11 @@ func Test_verifyProvenanceMatchesCertificate(t *testing.T) { }, { name: "unknown field", + subject: []intoto.Subject{ + { + Digest: intotocommon.DigestSet{"sha512": "abcd"}, + }, + }, environment: map[string]interface{}{ "SOMETHING": "workflow_dispatch", }, @@ -1201,48 +1084,15 @@ func Test_verifyProvenanceMatchesCertificate(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() - prov02 := &slsav02.ProvenanceV02{ - &intoto.ProvenanceStatement{ - StatementHeader: intoto.StatementHeader{ - Subject: tt.subject, - }, - Predicate: intotov02.ProvenancePredicate{ - Invocation: intotov02.ProvenanceInvocation{ - Environment: tt.environment, - ConfigSource: intotov02.ConfigSource{ - EntryPoint: tt.workflowTriggerPath, - }, - }, - }, - }, + + prov := &testProvenance{ + subjects: tt.subject, + noResolvedDeps: tt.numberResolvedDependencies, + buildTriggerPath: tt.workflowTriggerPath, + systemParameters: tt.environment, } - if tt.numberResolvedDependencies > 0 { - prov02.Predicate.Materials = make([]intotocommon.ProvenanceMaterial, tt.numberResolvedDependencies) - } - - err := verifyProvenanceMatchesCertificate(prov02, &tt.certificateIdentity) - if !errCmp(err, tt.err) { - t.Errorf(cmp.Diff(err, tt.err)) - } - - prov1 := &slsav10.ProvenanceV1{ - StatementHeader: intoto.StatementHeader{ - Subject: tt.subject, - }, - Predicate: intotov1.ProvenancePredicate{ - BuildDefinition: intotov1.ProvenanceBuildDefinition{ - InternalParameters: tt.environment, - // TODO(#566): verify fields for v1.0 provenance. - }, - }, - } - - if tt.numberResolvedDependencies > 0 { - prov1.Predicate.BuildDefinition.ResolvedDependencies = make([]intotov1.ResourceDescriptor, tt.numberResolvedDependencies) - } - err = verifyProvenanceMatchesCertificate(prov1, &tt.certificateIdentity) - if !errCmp(err, tt.err) { + if err := verifyProvenanceMatchesCertificate(prov, &tt.certificateIdentity); !errCmp(err, tt.err) { t.Errorf(cmp.Diff(err, tt.err)) } }) diff --git a/verifiers/internal/gha/provenance_test.go b/verifiers/internal/gha/provenance_test.go index de4b57b..a19f782 100644 --- a/verifiers/internal/gha/provenance_test.go +++ b/verifiers/internal/gha/provenance_test.go @@ -8,9 +8,12 @@ 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" + 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" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/iface" ) type testProvenance struct { @@ -47,7 +50,21 @@ func (p *testProvenance) GetWorkflowInputs() (map[string]interface{}, error) { return p.workflowInputs, nil } -func provenanceFromBytes(payload []byte) (slsaprovenance.Provenance, error) { +type testProvenanceV02 struct { + testProvenance + predicate slsa02.ProvenancePredicate +} + +func (p *testProvenanceV02) Predicate() slsa02.ProvenancePredicate { return p.predicate } + +type testProvenanceV1 struct { + testProvenance + predicate slsa1.ProvenancePredicate +} + +func (p *testProvenanceV1) Predicate() slsa1.ProvenancePredicate { return p.predicate } + +func provenanceFromBytes(payload []byte) (iface.Provenance, error) { env, err := EnvelopeFromBytes(payload) if err != nil { return nil, err @@ -95,7 +112,7 @@ func Test_VerifyDigest(t *testing.T) { t.Parallel() tests := []struct { name string - prov slsaprovenance.Provenance + prov iface.Provenance artifactHash string expected error }{ @@ -507,7 +524,7 @@ func Test_VerifyBranch(t *testing.T) { t.Parallel() tests := []struct { name string - prov slsaprovenance.Provenance + prov iface.Provenance branch string expected error }{ @@ -574,7 +591,7 @@ func Test_VerifyWorkflowInputs(t *testing.T) { t.Parallel() tests := []struct { name string - prov slsaprovenance.Provenance + prov iface.Provenance inputs map[string]string expected error }{ @@ -696,7 +713,7 @@ func Test_VerifyTag(t *testing.T) { t.Parallel() tests := []struct { name string - prov slsaprovenance.Provenance + prov iface.Provenance tag string expected error }{ @@ -763,7 +780,7 @@ func Test_VerifyVersionedTag(t *testing.T) { t.Parallel() tests := []struct { name string - prov slsaprovenance.Provenance + prov iface.Provenance tag string expected error }{ diff --git a/verifiers/internal/gha/slsaprovenance/common.go b/verifiers/internal/gha/slsaprovenance/common/common.go similarity index 76% rename from verifiers/internal/gha/slsaprovenance/common.go rename to verifiers/internal/gha/slsaprovenance/common/common.go index d9be54f..e2ebf67 100644 --- a/verifiers/internal/gha/slsaprovenance/common.go +++ b/verifiers/internal/gha/slsaprovenance/common/common.go @@ -1,18 +1,22 @@ -package slsaprovenance +package common import ( "fmt" "strings" - slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" serrors "github.com/slsa-framework/slsa-verifier/v2/errors" ) +const ( + // ProvenanceV02Type is the SLSA v0.2 predicate type. + ProvenanceV02Type = "https://slsa.dev/provenance/v0.2" +) + // GetWorkflowInputs gets the workflow inputs from the GitHub environment map // and converts the keys to the necessary casing depending on predicate type. -func GetWorkflowInputs(environment map[string]any, predicateType string) (map[string]any, error) { +func GetWorkflowInputs(environment map[string]any, upperEnv bool) (map[string]any, error) { // Verify it's a workflow_dispatch trigger. - eventKey, err := convertKey("github_event_name", predicateType) + eventKey, err := convertKey("github_event_name", upperEnv) if err != nil { return nil, fmt.Errorf("%w: %s", serrors.ErrorMismatchWorkflowInputs, err) @@ -26,7 +30,7 @@ func GetWorkflowInputs(environment map[string]any, predicateType string) (map[st serrors.ErrorMismatchWorkflowInputs, triggerName) } - payload, err := GetEventPayload(environment, predicateType) + payload, err := GetEventPayload(environment, upperEnv) if err != nil { return nil, err } @@ -45,8 +49,8 @@ func GetWorkflowInputs(environment map[string]any, predicateType string) (map[st // GetEventPayload retrieves the GitHub event payload from the environment map // that contains the GitHub context payload. -func GetEventPayload(environment map[string]any, predicateType string) (map[string]any, error) { - eventPayloadKey, err := convertKey("github_event_payload", predicateType) +func GetEventPayload(environment map[string]any, upperEnv bool) (map[string]any, error) { + eventPayloadKey, err := convertKey("github_event_payload", upperEnv) if err != nil { return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, err) } @@ -63,15 +67,11 @@ func GetEventPayload(environment map[string]any, predicateType string) (map[stri return payload, nil } -func convertKey(key, predicateType string) (string, error) { - switch predicateType { - case slsa1.PredicateSLSAProvenance: +func convertKey(key string, upperEnv bool) (string, error) { + if upperEnv { return strings.ToUpper(key), nil - case ProvenanceV02Type: - return key, nil - default: - return "", fmt.Errorf("unrecognized predicate type %s", predicateType) } + return key, nil } func getAsAny(environment map[string]any, field string) (any, error) { @@ -83,8 +83,8 @@ func getAsAny(environment map[string]any, field string) (any, error) { return value, nil } -func getBranchForTag(environment map[string]any, predicateType string) (string, error) { - baseRefKey, err := convertKey("github_base_ref", predicateType) +func getBranchForTag(environment map[string]any, upperEnv bool) (string, error) { + baseRefKey, err := convertKey("github_base_ref", upperEnv) if err != nil { return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, err) } @@ -100,7 +100,7 @@ func getBranchForTag(environment map[string]any, predicateType string) (string, } // Look at the event payload instead. - environmentKey, err := convertKey("github_event_name", predicateType) + environmentKey, err := convertKey("github_event_name", upperEnv) if err != nil { return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, err) } @@ -109,7 +109,7 @@ func getBranchForTag(environment map[string]any, predicateType string) (string, return "", err } - payload, err := GetEventPayload(environment, predicateType) + payload, err := GetEventPayload(environment, upperEnv) if err != nil { return "", err } @@ -154,8 +154,9 @@ func getBranchForTag(environment map[string]any, predicateType string) (string, } } -func GetTag(environment map[string]any, predicateType string) (string, error) { - refTypeKey, err := convertKey("github_ref_type", predicateType) +// GetTag returns the triggering event's tag based on the given environment. +func GetTag(environment map[string]any, upperEnv bool) (string, error) { + refTypeKey, err := convertKey("github_ref_type", upperEnv) if err != nil { return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, err) } @@ -169,7 +170,7 @@ func GetTag(environment map[string]any, predicateType string) (string, error) { case "branch": return "", nil case "tag": - refKey, err := convertKey("github_ref", predicateType) + refKey, err := convertKey("github_ref", upperEnv) if err != nil { return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, err) } @@ -180,8 +181,9 @@ func GetTag(environment map[string]any, predicateType string) (string, error) { } } -func GetBranch(environment map[string]any, predicateType string) (string, error) { - refTypeKey, err := convertKey("github_ref_type", predicateType) +// GetBranch returns the triggering event's branch based on the given environment. +func GetBranch(environment map[string]any, upperEnv bool) (string, error) { + refTypeKey, err := convertKey("github_ref_type", upperEnv) if err != nil { return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, err) } @@ -193,24 +195,26 @@ func GetBranch(environment map[string]any, predicateType string) (string, error) switch refType { case "branch": - refKey, err := convertKey("github_ref", predicateType) + refKey, err := convertKey("github_ref", upperEnv) if err != nil { return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, err) } return GetAsString(environment, refKey) case "tag": - return getBranchForTag(environment, predicateType) + return getBranchForTag(environment, upperEnv) default: return "", fmt.Errorf("%w: %s %s", serrors.ErrorInvalidDssePayload, "unknown ref type", refType) } } +// Exists returns true if the given key exists in the environment. func Exists(environment map[string]any, field string) bool { _, ok := environment[field] return ok } +// GetAsString returns the value in the given environment as a string. func GetAsString(environment map[string]any, field string) (string, error) { value, ok := environment[field] if !ok { diff --git a/verifiers/internal/gha/slsaprovenance/iface/provenance.go b/verifiers/internal/gha/slsaprovenance/iface/provenance.go new file mode 100644 index 0000000..cc7dcca --- /dev/null +++ b/verifiers/internal/gha/slsaprovenance/iface/provenance.go @@ -0,0 +1,50 @@ +package iface + +import ( + "time" + + intoto "github.com/in-toto/in-toto-golang/in_toto" +) + +// Provenance represents provenance for a predicate type and build type. +type Provenance interface { + // BuilderID returns the builder id in the predicate. + BuilderID() (string, error) + + // SourceURI is the full URI (including tag) of the source material. + SourceURI() (string, error) + + // TriggerURI is the full URI (including tag) of the configuration / trigger. + TriggerURI() (string, error) + + // Subject is the list of intoto subjects in the provenance. + Subjects() ([]intoto.Subject, error) + + // GetBranch retrieves the branch name of the source from the provenance. + GetBranch() (string, error) + + // GetTag retrieves the tag of the source from the provenance. + GetTag() (string, error) + + // Get workflow trigger path. + GetBuildTriggerPath() (string, error) + + // Get system pararmeters. + GetSystemParameters() (map[string]any, error) + + // Get build invocation ID. + GetBuildInvocationID() (string, error) + + // Get build start time. + GetBuildStartTime() (*time.Time, error) + + // Get build finish time. + GetBuildFinishTime() (*time.Time, error) + + // Get number of resolved dependencies. + GetNumberResolvedDependencies() (int, error) + + // GetWorkflowInputs retrieves the inputs from the provenance. Only succeeds for event + // relevant event types (workflow_inputs). + GetWorkflowInputs() (map[string]interface{}, error) +} diff --git a/verifiers/internal/gha/slsaprovenance/slsaprovenance.go b/verifiers/internal/gha/slsaprovenance/slsaprovenance.go index dc8b5fc..dc5da20 100644 --- a/verifiers/internal/gha/slsaprovenance/slsaprovenance.go +++ b/verifiers/internal/gha/slsaprovenance/slsaprovenance.go @@ -1,69 +1,32 @@ package slsaprovenance import ( - "bytes" "encoding/base64" "encoding/json" "fmt" - "sync" - "time" - 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" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/iface" + slsav02 "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/v0.2" + slsav1 "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/v1.0" ) -const ( - ProvenanceV02Type = "https://slsa.dev/provenance/v0.2" -) +// provenanceConstructor creates a new Provenance instance for the given payload as a json Decoder. +type provenanceConstructor func(payload []byte) (iface.Provenance, error) -type Provenance interface { - // BuilderID returns the builder id in the predicate. - BuilderID() (string, error) - - // SourceURI is the full URI (including tag) of the source material. - SourceURI() (string, error) - - // TriggerURI is the full URI (including tag) of the configuration / trigger. - TriggerURI() (string, error) - - // Subject is the list of intoto subjects in the provenance. - Subjects() ([]intoto.Subject, error) - - // GetBranch retrieves the branch name of the source from the provenance. - GetBranch() (string, error) - - // GetTag retrieves the tag of the source from the provenance. - GetTag() (string, error) - - // Get workflow trigger path. - GetBuildTriggerPath() (string, error) - - // Get system pararmeters. - GetSystemParameters() (map[string]any, error) - - // Get build invocation ID. - GetBuildInvocationID() (string, error) - - // Get build start time. - GetBuildStartTime() (*time.Time, error) - - // Get build finish time. - GetBuildFinishTime() (*time.Time, error) - - // Get number of resolved dependencies. - GetNumberResolvedDependencies() (int, error) - - // GetWorkflowInputs retrieves the inputs from the provenance. Only succeeds for event - // relevant event types (workflow_inputs). - GetWorkflowInputs() (map[string]interface{}, 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, } -// ProvenanceMap stores the different provenance version types. -var ProvenanceMap sync.Map - -// Provenance interface that each type may implement. -func ProvenanceFromEnvelope(env *dsselib.Envelope) (Provenance, error) { +// ProvenanceFromEnvelope returns a Provenance instance for the given DSSE Envelope. +func ProvenanceFromEnvelope(env *dsselib.Envelope) (iface.Provenance, error) { if env.PayloadType != "application/vnd.in-toto+json" { return nil, fmt.Errorf("%w: expected payload type 'application/vnd.in-toto+json', got '%s'", serrors.ErrorInvalidDssePayload, env.PayloadType) @@ -82,19 +45,14 @@ func ProvenanceFromEnvelope(env *dsselib.Envelope) (Provenance, error) { } // Load the appropriate structure and unmarshal. - ptype, ok := ProvenanceMap.Load(pred.PredicateType) + newProv, ok := predicateTypeMap[pred.PredicateType] if !ok { return nil, fmt.Errorf("%w: unexpected predicate type '%s'", serrors.ErrorInvalidDssePayload, pred.PredicateType) } - prov := ptype.(func() Provenance)() - - // Strict unmarshal. - // NOTE: this supports extensions because they are - // only used as part of interface{}-defined fields. - dec := json.NewDecoder(bytes.NewReader(pyld)) - dec.DisallowUnknownFields() - if err := dec.Decode(prov); err != nil { + prov, err := newProv(pyld) + if err != nil { return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, err.Error()) } + return prov, nil } diff --git a/verifiers/internal/gha/slsaprovenance/v0.2/base.go b/verifiers/internal/gha/slsaprovenance/v0.2/base.go new file mode 100644 index 0000000..9bbd3f0 --- /dev/null +++ b/verifiers/internal/gha/slsaprovenance/v0.2/base.go @@ -0,0 +1,135 @@ +package v02 + +import ( + "fmt" + "time" + + 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" + + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common" +) + +// provenanceV02 implements basic logic for SLSA v0.2 provenance. +type provenanceV02 struct { + // upperEnv specifies if environment fields are in uppercase. + upperEnv bool + prov *intotoAttestation +} + +// Predicate implements provenanceV02.Predicate. +func (p *provenanceV02) Predicate() slsa02.ProvenancePredicate { + return p.prov.Predicate +} + +// BuilderID implements Provenance.BuilderID. +func (p *provenanceV02) BuilderID() (string, error) { + return p.prov.Predicate.Builder.ID, nil +} + +// SourceURI implements Provenance.SourceURI. +func (p *provenanceV02) SourceURI() (string, error) { + if len(p.prov.Predicate.Materials) == 0 { + return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "no material") + } + uri := p.prov.Predicate.Materials[0].URI + if uri == "" { + return "", fmt.Errorf("%w: empty uri", serrors.ErrorMalformedURI) + } + + return uri, nil +} + +// TriggerURI implements Provenance.TriggerURI. +func (p *provenanceV02) TriggerURI() (string, error) { + uri := p.prov.Predicate.Invocation.ConfigSource.URI + if uri == "" { + return "", fmt.Errorf("%w: empty uri", serrors.ErrorMalformedURI) + } + return uri, nil +} + +// Subjects implements Provenance.Subjects. +func (p *provenanceV02) 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 *provenanceV02) GetBranch() (string, error) { + // GetBranch gets the branch from the invocation parameters. + environment, ok := p.prov.Predicate.Invocation.Environment.(map[string]interface{}) + if !ok { + return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type") + } + + return common.GetBranch(environment, p.upperEnv) +} + +// GetTag implements Provenance.GetTag. +func (p *provenanceV02) GetTag() (string, error) { + environment, ok := p.prov.Predicate.Invocation.Environment.(map[string]interface{}) + if !ok { + return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type") + } + + return common.GetTag(environment, p.upperEnv) +} + +// GetWorkflowInputs implements Provenance.GetWorkflowInputs. +func (p *provenanceV02) GetWorkflowInputs() (map[string]interface{}, error) { + // Verify it's a workflow_dispatch trigger. + environment, ok := p.prov.Predicate.Invocation.Environment.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type") + } + + return common.GetWorkflowInputs(environment, p.upperEnv) +} + +// GetBuildTriggerPath implements Provenance.GetBuildTriggerPath. +func (p *provenanceV02) GetBuildTriggerPath() (string, error) { + return p.prov.Predicate.Invocation.ConfigSource.EntryPoint, nil +} + +// GetBuildInvocationID implements Provenance.GetBuildInvocationID. +func (p *provenanceV02) GetBuildInvocationID() (string, error) { + if p.prov.Predicate.Metadata == nil { + return "", nil + } + return p.prov.Predicate.Metadata.BuildInvocationID, nil +} + +// GetBuildStartTime implements Provenance.GetBuildStartTime. +func (p *provenanceV02) GetBuildStartTime() (*time.Time, error) { + if p.prov.Predicate.Metadata == nil { + return nil, nil + } + return p.prov.Predicate.Metadata.BuildStartedOn, nil +} + +// GetBuildFinishTime implements Provenance.GetBuildFinishTime. +func (p *provenanceV02) GetBuildFinishTime() (*time.Time, error) { + if p.prov.Predicate.Metadata == nil { + return nil, nil + } + return p.prov.Predicate.Metadata.BuildFinishedOn, nil +} + +// GetNumberResolvedDependencies implements Provenance.GetNumberResolvedDependencies. +func (p *provenanceV02) GetNumberResolvedDependencies() (int, error) { + return len(p.prov.Predicate.Materials), nil +} + +// GetSystemParameters implements Provenance.GetSystemParameters. +func (p *provenanceV02) GetSystemParameters() (map[string]any, error) { + environment, ok := p.prov.Predicate.Invocation.Environment.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type") + } + return environment, nil +} diff --git a/verifiers/internal/gha/slsaprovenance/v0.2/provenance.go b/verifiers/internal/gha/slsaprovenance/v0.2/provenance.go index 64b6543..5ca8b4a 100644 --- a/verifiers/internal/gha/slsaprovenance/v0.2/provenance.go +++ b/verifiers/internal/gha/slsaprovenance/v0.2/provenance.go @@ -1,126 +1,73 @@ package v02 import ( + "bytes" + "encoding/json" "fmt" - "time" 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" - "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance" + + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/iface" ) -// TODO(https://github.com/slsa-framework/slsa-verifier/issues/473): Use a static mapping. -// -//nolint:gochecknoinits -func init() { - slsaprovenance.ProvenanceMap.Store( - slsaprovenance.ProvenanceV02Type, - New) +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" +) + +// intotoAttestation is a SLSA v0.2 in-toto attestation statement. +type intotoAttestation struct { + intoto.StatementHeader + Predicate slsa02.ProvenancePredicate `json:"predicate"` } -type ProvenanceV02 struct { - *intoto.ProvenanceStatement +// ProvenanceV02 represents v0.2 provenance. +type ProvenanceV02 interface { + Predicate() slsa02.ProvenancePredicate } -// This returns a new, empty instance of the v0.2 provenance. -func New() slsaprovenance.Provenance { - return &ProvenanceV02{} -} +// New returns a new Provenance for the given json payload. +func New(payload []byte) (iface.Provenance, error) { + // Strict unmarshal. + // NOTE: this supports extensions because they are + // only used as part of interface{}-defined fields. + dec := json.NewDecoder(bytes.NewReader(payload)) + dec.DisallowUnknownFields() -func (prov *ProvenanceV02) BuilderID() (string, error) { - return prov.Predicate.Builder.ID, nil -} - -func (prov *ProvenanceV02) SourceURI() (string, error) { - if len(prov.Predicate.Materials) == 0 { - return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "no material") - } - uri := prov.Predicate.Materials[0].URI - if uri == "" { - return "", fmt.Errorf("%w: empty uri", serrors.ErrorMalformedURI) + a := &intotoAttestation{} + if err := dec.Decode(a); err != nil { + return nil, err } - return uri, nil -} - -func (prov *ProvenanceV02) TriggerURI() (string, error) { - uri := prov.Predicate.Invocation.ConfigSource.URI - if uri == "" { - return "", fmt.Errorf("%w: empty uri", serrors.ErrorMalformedURI) + 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) } - return uri, nil -} - -func (prov *ProvenanceV02) Subjects() ([]intoto.Subject, error) { - subj := prov.Subject - if len(subj) == 0 { - return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "no subjects") - } - return subj, nil -} - -func (prov *ProvenanceV02) GetBranch() (string, error) { - // GetBranch gets the branch from the invocation parameters. - environment, ok := prov.Predicate.Invocation.Environment.(map[string]interface{}) - if !ok { - return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type") - } - - return slsaprovenance.GetBranch(environment, prov.PredicateType) -} - -func (prov *ProvenanceV02) GetTag() (string, error) { - environment, ok := prov.Predicate.Invocation.Environment.(map[string]interface{}) - if !ok { - return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type") - } - - return slsaprovenance.GetTag(environment, prov.PredicateType) -} - -func (prov *ProvenanceV02) GetWorkflowInputs() (map[string]interface{}, error) { - // Verify it's a workflow_dispatch trigger. - environment, ok := prov.Predicate.Invocation.Environment.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type") - } - - return slsaprovenance.GetWorkflowInputs(environment, prov.PredicateType) -} - -func (prov *ProvenanceV02) GetBuildTriggerPath() (string, error) { - return prov.Predicate.Invocation.ConfigSource.EntryPoint, nil -} - -func (prov *ProvenanceV02) GetBuildInvocationID() (string, error) { - if prov.Predicate.Metadata == nil { - return "", nil - } - return prov.Predicate.Metadata.BuildInvocationID, nil -} - -func (prov *ProvenanceV02) GetBuildStartTime() (*time.Time, error) { - if prov.Predicate.Metadata == nil { - return nil, nil - } - return prov.Predicate.Metadata.BuildStartedOn, nil -} - -func (prov *ProvenanceV02) GetBuildFinishTime() (*time.Time, error) { - if prov.Predicate.Metadata == nil { - return nil, nil - } - return prov.Predicate.Metadata.BuildFinishedOn, nil -} - -func (prov *ProvenanceV02) GetNumberResolvedDependencies() (int, error) { - return len(prov.Predicate.Materials), nil -} - -func (prov *ProvenanceV02) GetSystemParameters() (map[string]any, error) { - environment, ok := prov.Predicate.Invocation.Environment.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type") - } - return environment, nil } diff --git a/verifiers/internal/gha/slsaprovenance/v1.0/byob.go b/verifiers/internal/gha/slsaprovenance/v1.0/byob.go new file mode 100644 index 0000000..0717e3b --- /dev/null +++ b/verifiers/internal/gha/slsaprovenance/v1.0/byob.go @@ -0,0 +1,240 @@ +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" +) + +// byobBuildType is the base build type for BYOB delegated builders. +var byobBuildType = "https://github.com/slsa-framework/slsa-github-generator/delegator-generic@v0" + +// BYOBProvenance is SLSA v1.0 provenance for the slsa-github-generator BYOB build type. +type BYOBProvenance struct { + prov *intotoAttestation +} + +// 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") + 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") + } + + 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 *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") + } + + 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 *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") + } + + return sysParams, nil +} diff --git a/verifiers/internal/gha/slsaprovenance/v1.0/container_based.go b/verifiers/internal/gha/slsaprovenance/v1.0/container_based.go new file mode 100644 index 0000000..98a45ea --- /dev/null +++ b/verifiers/internal/gha/slsaprovenance/v1.0/container_based.go @@ -0,0 +1,11 @@ +package v1 + +// containerBasedBuildType is the build type for the container-based builder and is based on BYOB. +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 +} diff --git a/verifiers/internal/gha/slsaprovenance/v1.0/provenance.go b/verifiers/internal/gha/slsaprovenance/v1.0/provenance.go index be6fbd3..3ab5398 100644 --- a/verifiers/internal/gha/slsaprovenance/v1.0/provenance.go +++ b/verifiers/internal/gha/slsaprovenance/v1.0/provenance.go @@ -1,217 +1,52 @@ package v1 import ( + "bytes" + "encoding/json" "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" + + "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/iface" ) -// TODO(https://github.com/slsa-framework/slsa-verifier/issues/473): Use a static mapping. -// -//nolint:gochecknoinits -func init() { - slsaprovenance.ProvenanceMap.Store( - slsa1.PredicateSLSAProvenance, - New) -} - -type ProvenanceV1 struct { +type intotoAttestation struct { intoto.StatementHeader - Predicate slsa1.ProvenancePredicate `json:"predicate"` - predicateType string + Predicate slsa1.ProvenancePredicate `json:"predicate"` } -// This returns a new, empty instance of the v0.2 provenance. -func New() slsaprovenance.Provenance { - return &ProvenanceV1{ - predicateType: slsa1.PredicateSLSAProvenance, - } +// ProvenanceV1 represents v1.0 provenance. +type ProvenanceV1 interface { + Predicate() slsa1.ProvenancePredicate } -func (prov *ProvenanceV1) BuilderID() (string, error) { - return prov.Predicate.RunDetails.Builder.ID, nil -} - -func (prov *ProvenanceV1) SourceURI() (string, error) { - // Use resolvedDependencies. - if len(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 := 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 (prov *ProvenanceV1) generatorTriggerInfo() (string, string, string, error) { - // See https://github.com/slsa-framework/github-actions-buildtypes/blob/main/workflow/v1/example.json#L16-L19. - extParams, ok := 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) +// New returns a new Provenance object based on the payload. +func New(payload []byte) (iface.Provenance, error) { + // Strict unmarshal. + // NOTE: this supports extensions because they are + // only used as part of interface{}-defined fields. + dec := json.NewDecoder(bytes.NewReader(payload)) + dec.DisallowUnknownFields() + + a := &intotoAttestation{} + if err := dec.Decode(a); err != nil { + return nil, 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) } - path, err := getValidateKey(workflowMap, "path") - if err != nil { - return "", "", "", err - } - return repository, ref, path, nil -} - -func (prov *ProvenanceV1) builderTriggerInfo() (string, string, string, error) { - sysParams, ok := 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 := slsaprovenance.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 (prov *ProvenanceV1) triggerInfo() (string, string, string, error) { - // TODO(#613): Support for generators. - return prov.builderTriggerInfo() -} - -func (prov *ProvenanceV1) TriggerURI() (string, error) { - repository, ref, _, err := prov.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 -} - -func (prov *ProvenanceV1) Subjects() ([]intoto.Subject, error) { - subj := prov.Subject - if len(subj) == 0 { - return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "no subjects") - } - return subj, nil -} - -func (prov *ProvenanceV1) GetBranch() (string, error) { - // TODO(https://github.com/slsa-framework/slsa-verifier/issues/472): Add GetBranch() support. - sysParams, ok := prov.Predicate.BuildDefinition.InternalParameters.(map[string]interface{}) - if !ok { - return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "internal parameters type") - } - - return slsaprovenance.GetBranch(sysParams, prov.predicateType) -} - -func (prov *ProvenanceV1) GetTag() (string, error) { - sysParams, ok := prov.Predicate.BuildDefinition.InternalParameters.(map[string]interface{}) - if !ok { - return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "system parameters type") - } - return slsaprovenance.GetTag(sysParams, prov.predicateType) -} - -func (prov *ProvenanceV1) GetWorkflowInputs() (map[string]interface{}, error) { - sysParams, ok := prov.Predicate.BuildDefinition.InternalParameters.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "system parameters type") - } - return slsaprovenance.GetWorkflowInputs(sysParams, prov.predicateType) -} - -func (prov *ProvenanceV1) GetBuildTriggerPath() (string, error) { - _, _, path, err := prov.triggerInfo() - if err != nil { - return "", err - } - - return path, nil -} - -func (prov *ProvenanceV1) GetBuildInvocationID() (string, error) { - return prov.Predicate.RunDetails.BuildMetadata.InvocationID, nil -} - -func (prov *ProvenanceV1) GetBuildStartTime() (*time.Time, error) { - return prov.Predicate.RunDetails.BuildMetadata.StartedOn, nil -} - -func (prov *ProvenanceV1) GetBuildFinishTime() (*time.Time, error) { - return prov.Predicate.RunDetails.BuildMetadata.FinishedOn, nil -} - -func (prov *ProvenanceV1) GetNumberResolvedDependencies() (int, error) { - return len(prov.Predicate.BuildDefinition.ResolvedDependencies), nil -} - -func (prov *ProvenanceV1) GetSystemParameters() (map[string]any, error) { - sysParams, ok := prov.Predicate.BuildDefinition.InternalParameters.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "system parameters type") - } - - return sysParams, nil }