diff --git a/cli/slsa-verifier/main_regression_test.go b/cli/slsa-verifier/main_regression_test.go index ad93e9e..514651f 100644 --- a/cli/slsa-verifier/main_regression_test.go +++ b/cli/slsa-verifier/main_regression_test.go @@ -1795,6 +1795,8 @@ func Test_runVerifyNpmPackage(t *testing.T) { } } +// Test_runVerifyVSA tests the CLI inputes of verify-vsa. More extensive tests are in +// slsa-verifier/verifiers/internal/vsa/verifier_test.go func Test_runVerifyVSA(t *testing.T) { t.Parallel() @@ -1855,7 +1857,6 @@ func Test_runVerifyVSA(t *testing.T) { publicKeyHashAlgo: PointerTo("SHA256"), err: serrors.ErrorNoValidSignature, }, - // TODO: Add more tests for different scenarios. } for _, tt := range tests { diff --git a/errors/errors.go b/errors/errors.go index 79c2a57..4da797d 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -45,9 +45,10 @@ var ( ErrorNotPresent = errors.New("not present") ErrorInvalidPublicKey = errors.New("invalid public key") ErrorInvalidHashAlgo = errors.New("unsupported hash algorithm") - ErrorMismatchSLSAResult = errors.New("SLSA result does not match") + ErrorInvalidVerificationResult = errors.New("verificationResult is not PASSED") ErrorMismatchVerifiedLevels = errors.New("verified levels do not match") ErrorMissingSubjectDigest = errors.New("missing subject digest") + ErrorEmptyRequiredField = errors.New("empty value in required field") ErrorMismatchResourceURI = errors.New("resource URI does not match") ErrorMismatchVerifierID = errors.New("verifier ID does not match") ) diff --git a/verifiers/internal/vsa/testdata/gce/v1/gke-gce-pre.bcid-vsa.jsonl b/verifiers/internal/vsa/testdata/gce/v1/gke-gce-pre.bcid-vsa.jsonl deleted file mode 100644 index 35976a9..0000000 --- a/verifiers/internal/vsa/testdata/gce/v1/gke-gce-pre.bcid-vsa.jsonl +++ /dev/null @@ -1,10 +0,0 @@ -{ - "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9zbHNhLmRldi92ZXJpZmljYXRpb25fc3VtbWFyeS92MSIsInByZWRpY2F0ZSI6eyJ0aW1lVmVyaWZpZWQiOiIyMDI0LTA2LTEyVDA3OjI0OjM0LjM1MTYwOFoiLCJ2ZXJpZmllciI6eyJpZCI6Imh0dHBzOi8vYmNpZC5jb3JwLmdvb2dsZS5jb20vdmVyaWZpZXIvYmNpZF9wYWNrYWdlX2VuZm9yY2VyL3YwLjEifSwidmVyaWZpY2F0aW9uUmVzdWx0IjoiUEFTU0VEIiwidmVyaWZpZWRMZXZlbHMiOlsiQkNJRF9MMSIsIlNMU0FfQlVJTERfTEVWRUxfMiJdLCJyZXNvdXJjZVVyaSI6ImdjZV9pbWFnZTovL2drZS1ub2RlLWltYWdlczpna2UtMTI2MTUtZ2tlMTQxODAwMC1jb3MtMTAxLTE3MTYyLTQ2My0yOS1jLWNncHYxLXByZSIsInBvbGljeSI6eyJ1cmkiOiJnb29nbGVmaWxlOi9nb29nbGVfc3JjL2ZpbGVzLzY0MjUxMzE5Mi9kZXBvdC9nb29nbGUzL3Byb2R1Y3Rpb24vc2VjdXJpdHkvYmNpZC9zb2Z0d2FyZS9nY2VfaW1hZ2UvZ2tlL3ZtX2ltYWdlcy5zd19wb2xpY3kudGV4dHByb3RvIn19LCJzdWJqZWN0IjpbeyJuYW1lIjoiXyIsImRpZ2VzdCI6eyJnY2VfaW1hZ2VfaWQiOiI4OTcwMDk1MDA1MzA2MDAwMDUzIn19XX0=", - "payloadType": "application/vnd.in-toto+json", - "signatures": [ - { - "sig": "bmIy2gfnQt6oYpd0WbpQMtZcMRtmntDmyki+Be+2Z9qkboMVbi2RQAD1b5AWbBs7iAP8NZVJOI4R/4jOVYB/FA==", - "keyid": "keystore://76574:prod:vsa_signing_public_key" - } - ] -} \ No newline at end of file diff --git a/verifiers/internal/vsa/testdata/gce/v1/vsa_signing_public_key.pem b/verifiers/internal/vsa/testdata/gce/v1/vsa_signing_public_key.pem deleted file mode 100644 index 27bda33..0000000 --- a/verifiers/internal/vsa/testdata/gce/v1/vsa_signing_public_key.pem +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeGa6ZCZn0q6WpaUwJrSk+PPYEsca -3Xkk3UrxvbQtoZzTmq0zIYq+4QQl0YBedSyy+XcwAMaUWTouTrB05WhYtg== ------END PUBLIC KEY----- \ No newline at end of file diff --git a/verifiers/internal/vsa/v1.0/vsa.go b/verifiers/internal/vsa/v1.0/vsa.go index a04b75f..bbc4bd2 100644 --- a/verifiers/internal/vsa/v1.0/vsa.go +++ b/verifiers/internal/vsa/v1.0/vsa.go @@ -10,7 +10,7 @@ import ( serrors "github.com/slsa-framework/slsa-verifier/v2/errors" ) -const vsaPredicateType = "https://slsa.dev/verification_summary/v1" +const PredicateType = "https://slsa.dev/verification_summary/v1" // VSA is a struct that represents a VSA statement. // spec: https://slsa.dev/spec/v1.0/verification_summary. @@ -43,8 +43,8 @@ type Verifier struct { // VSAFromStatement creates a VSA from a statement. func VSAFromStatement(statement *intotoGolang.Statement) (*VSA, error) { - if statement.PredicateType != vsaPredicateType { - return nil, fmt.Errorf("%w: expected predicate type %q, got %q", serrors.ErrorInvalidDssePayload, vsaPredicateType, statement.PredicateType) + if statement.PredicateType != PredicateType { + return nil, fmt.Errorf("%w: expected predicate type %q, got %q", serrors.ErrorInvalidDssePayload, PredicateType, statement.PredicateType) } vsaBytes, err := json.Marshal(statement) if err != nil { diff --git a/verifiers/internal/vsa/verifier.go b/verifiers/internal/vsa/verifier.go index b1d61f0..9103d15 100644 --- a/verifiers/internal/vsa/verifier.go +++ b/verifiers/internal/vsa/verifier.go @@ -21,7 +21,6 @@ func VerifyVSA(ctx context.Context, verificationOpts *options.VerificationOpts, ) ([]byte, *utils.TrustedAttesterID, error) { // following steps in https://slsa.dev/spec/v1.1/verification_summary#how-to-verify - envelope, err := utils.EnvelopeFromBytes(attestation) if err != nil { return nil, nil, err @@ -29,17 +28,8 @@ func VerifyVSA(ctx context.Context, // 1. verify the envelope signature, // 4. match the verfier with the public key: implicit because we accept a user-provided public key. - err = verifyEnvelopeSignature(ctx, envelope, verificationOpts) - if err != nil { - return nil, nil, err - } - - statement, err := utils.StatementFromEnvelope(envelope) - if err != nil { - return nil, nil, err - } // 3. parse the VSA, verifying the predicateType. - vsa, err := vsa10.VSAFromStatement(statement) + vsa, err := extractSignedVSA(ctx, envelope, verificationOpts) if err != nil { return nil, nil, err } @@ -65,11 +55,31 @@ func VerifyVSA(ctx context.Context, return vsaBytes, trustedAttesterID, nil } +// extractSignedVSA verifies the envelope signature and type and extracts the VSA from the envelope. +func extractSignedVSA(ctx context.Context, envelope *dsse.Envelope, verificationOpts *options.VerificationOpts) (*vsa10.VSA, error) { + // 1. verify the envelope signature, + // 4. match the verfier with the public key: implicit because we accept a user-provided public key. + err := verifyEnvelopeSignature(ctx, envelope, verificationOpts) + if err != nil { + return nil, err + } + statement, err := utils.StatementFromEnvelope(envelope) + if err != nil { + return nil, err + } + // 3. parse the VSA, verifying the predicateType. + vsa, err := vsa10.VSAFromStatement(statement) + if err != nil { + return nil, err + } + return vsa, nil +} + // verifyEnvelopeSignature verifies the signature of the envelope. func verifyEnvelopeSignature(ctx context.Context, envelope *dsse.Envelope, verificationOpts *options.VerificationOpts) error { signatureVerifier, err := sigstoreSignature.LoadVerifier(verificationOpts.PublicKey, verificationOpts.PublicKeyHashAlgo) if err != nil { - return fmt.Errorf("%w: loading sigstore DSSE envolope verifier %w", serrors.ErrorInvalidPublicKey, err) + return fmt.Errorf("%w: loading sigstore DSSE envolope verifier: %w", serrors.ErrorInvalidPublicKey, err) } envelopeVerifier, err := dsse.NewEnvelopeVerifier(&sigstoreDSSE.VerifierAdapter{ SignatureVerifier: signatureVerifier, @@ -77,11 +87,11 @@ func verifyEnvelopeSignature(ctx context.Context, envelope *dsse.Envelope, verif PubKeyID: *verificationOpts.PublicKeyID, }) if err != nil { - return fmt.Errorf("%w: creating sigstore DSSE envelope verifier %w", serrors.ErrorInvalidPublicKey, err) + return fmt.Errorf("%w: creating sigstore DSSE envelope verifier: %w", serrors.ErrorInvalidPublicKey, err) } _, err = envelopeVerifier.Verify(ctx, envelope) if err != nil { - return fmt.Errorf("%w: verifying envelope %w", serrors.ErrorNoValidSignature, err) + return fmt.Errorf("%w: verifying envelope: %w", serrors.ErrorNoValidSignature, err) } return nil } @@ -114,10 +124,7 @@ func matchExpectedValues(vsa *vsa10.VSA, vsaOpts *options.VSAOpts) error { // matchExepectedSubjectDigests checks if the expected subject digests are present in the VSA. func matchExepectedSubjectDigests(vsa *vsa10.VSA, vsaOpts *options.VSAOpts) error { if len(*vsaOpts.ExpectedDigests) == 0 { - return fmt.Errorf("%w: no subject digests provided", serrors.ErrorInvalidSubject) - } - if len(vsa.Subject) == 0 { - return fmt.Errorf("%w: no subject digests found in the VSA", serrors.ErrorInvalidDssePayload) + return fmt.Errorf("%w: no subject digests provided", serrors.ErrorEmptyRequiredField) } // collect all digests from the VSA, so we can efficiently search, e.g.: // { @@ -139,6 +146,9 @@ func matchExepectedSubjectDigests(vsa *vsa10.VSA, vsaOpts *options.VSAOpts) erro allVSASubjectDigests[digestType][digestValue] = true } } + if len(allVSASubjectDigests) == 0 { + return fmt.Errorf("%w: no subject digests found in the VSA", serrors.ErrorInvalidDssePayload) + } // search for the expected digests in the VSA for _, expectedDigest := range *vsaOpts.ExpectedDigests { parts := strings.SplitN(expectedDigest, ":", 2) @@ -159,6 +169,9 @@ func matchExepectedSubjectDigests(vsa *vsa10.VSA, vsaOpts *options.VSAOpts) erro // matchVerifierID checks if the verifier ID in the VSA matches the expected value. func matchVerifierID(vsa *vsa10.VSA, vsaOpts *options.VSAOpts) error { + if vsa.Predicate.Verifier.ID == "" { + return fmt.Errorf("%w: no verifierID found in the VSA", serrors.ErrorEmptyRequiredField) + } if *vsaOpts.ExpectedVerifierID != vsa.Predicate.Verifier.ID { return fmt.Errorf("%w: verifier ID mismatch: wanted %s, got %s", serrors.ErrorMismatchVerifierID, *vsaOpts.ExpectedVerifierID, vsa.Predicate.Verifier.ID) } @@ -167,6 +180,9 @@ func matchVerifierID(vsa *vsa10.VSA, vsaOpts *options.VSAOpts) error { // matchResourceURI checks if the resource URI in the VSA matches the expected value. func matchResourceURI(vsa *vsa10.VSA, vsaOpts *options.VSAOpts) error { + if vsa.Predicate.ResourceURI == "" { + return fmt.Errorf("%w: no resourceURI provided", serrors.ErrorEmptyRequiredField) + } if *vsaOpts.ExpectedResourceURI != vsa.Predicate.ResourceURI { return fmt.Errorf("%w: resource URI mismatch: wanted %s, got %s", serrors.ErrorMismatchResourceURI, *vsaOpts.ExpectedResourceURI, vsa.Predicate.ResourceURI) } @@ -176,7 +192,7 @@ func matchResourceURI(vsa *vsa10.VSA, vsaOpts *options.VSAOpts) error { // confirmVerificationResult checks that the policy verification result is "PASSED". func confirmVerificationResult(vsa *vsa10.VSA) error { if normalizeString(vsa.Predicate.VerificationResult) != "PASSED" { - return fmt.Errorf("%w: verification result is not Passed: %s", serrors.ErrorMismatchSLSAResult, vsa.Predicate.VerificationResult) + return fmt.Errorf("%w: verification result is not Passed: %s", serrors.ErrorInvalidVerificationResult, vsa.Predicate.VerificationResult) } return nil } diff --git a/verifiers/internal/vsa/verifier_test.go b/verifiers/internal/vsa/verifier_test.go new file mode 100644 index 0000000..68cf787 --- /dev/null +++ b/verifiers/internal/vsa/verifier_test.go @@ -0,0 +1,425 @@ +package vsa + +import ( + "bytes" + "context" + "crypto" + "encoding/base64" + "encoding/json" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + intotoAttestattions "github.com/in-toto/attestation/go/v1" + intotoGolang "github.com/in-toto/in-toto-golang/in_toto" + intotoCommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/sigstore/pkg/cryptoutils" + + serrors "github.com/slsa-framework/slsa-verifier/v2/errors" + "github.com/slsa-framework/slsa-verifier/v2/options" + vsa10 "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/vsa/v1.0" +) + +func Test_extractSignedVSA(t *testing.T) { + ctx := context.Background() + + t.Parallel() + + // goodPayload := base64.StdEncoding.EncodeToString() + goodAttestationString := ` + { + "_type": "https://in-toto.io/Statement/v1", + "predicateType": "https://slsa.dev/verification_summary/v1", + "predicate": { + "timeVerified": "2024-06-12T07:24:34.351608Z", + "verifier": { + "id": "https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1" + }, + "verificationResult": "PASSED", + "verifiedLevels": [ + "BCID_L1", + "SLSA_BUILD_LEVEL_2" + ], + "resourceUri": "gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre", + "policy": { + "uri": "googlefile:/google_src/files/642513192/depot/google3/production/security/bcid/software/gce_image/gke/vm_images.sw_policy.textproto" + } + }, + "subject": [ + { + "name": "_", + "digest": { + "gce_image_id": "8970095005306000053" + } + } + ] + } + ` + goodEnvelope := &dsse.Envelope{ + PayloadType: intotoGolang.PayloadType, + Payload: mustEncodeAttestationString(goodAttestationString), + Signatures: []dsse.Signature{ + { + KeyID: "keystore://76574:prod:vsa_signing_public_key", + Sig: "bmIy2gfnQt6oYpd0WbpQMtZcMRtmntDmyki+Be+2Z9qkboMVbi2RQAD1b5AWbBs7iAP8NZVJOI4R/4jOVYB/FA==", + }, + }, + } + goodVSAOpts := &options.VerificationOpts{ + PublicKey: mustPublicKeyFromBytes([]byte(`-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeGa6ZCZn0q6WpaUwJrSk+PPYEsca +3Xkk3UrxvbQtoZzTmq0zIYq+4QQl0YBedSyy+XcwAMaUWTouTrB05WhYtg== +-----END PUBLIC KEY-----`)), + PublicKeyID: pointerTo("keystore://76574:prod:vsa_signing_public_key"), + PublicKeyHashAlgo: crypto.SHA256, + } + goodVSA := &vsa10.VSA{ + StatementHeader: intotoGolang.StatementHeader{ + Type: intotoAttestattions.StatementTypeUri, + PredicateType: vsa10.PredicateType, + Subject: []intotoGolang.Subject{ + { + Name: "_", + Digest: map[string]string{ + "gce_image_id": "8970095005306000053", + }, + }, + }, + }, + Predicate: vsa10.Predicate{ + TimeVerified: time.Date(2024, 6, 12, 7, 24, 34, 351608000, time.UTC), + Verifier: vsa10.Verifier{ + ID: "https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1", + }, + ResourceURI: "gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre", + Policy: intotoCommon.ProvenanceMaterial{ + URI: "googlefile:/google_src/files/642513192/depot/google3/production/security/bcid/software/gce_image/gke/vm_images.sw_policy.textproto", + }, + VerificationResult: "PASSED", + VerifiedLevels: []string{"BCID_L1", "SLSA_BUILD_LEVEL_2"}, + }, + } + + tests := []struct { + name string + envelope *dsse.Envelope + opts *options.VerificationOpts + expectedVSA *vsa10.VSA + err error + }{ + { + name: "success", + envelope: goodEnvelope, + opts: goodVSAOpts, + expectedVSA: goodVSA, + }, + { + name: "failure: empty signatures", + envelope: &dsse.Envelope{ + PayloadType: goodEnvelope.PayloadType, + Payload: goodEnvelope.Payload, + Signatures: []dsse.Signature{}, + }, + opts: goodVSAOpts, + expectedVSA: nil, + err: dsse.ErrNoSignature, + }, + { + name: "failure: mismatch signature", + envelope: &dsse.Envelope{ + PayloadType: goodEnvelope.PayloadType, + Payload: mustEncodeAttestationString("{}"), + Signatures: goodEnvelope.Signatures, + }, + opts: goodVSAOpts, + expectedVSA: nil, + err: serrors.ErrorNoValidSignature, + }, + { + name: "failure: misatch keyID", + envelope: goodEnvelope, + opts: &options.VerificationOpts{ + PublicKey: goodVSAOpts.PublicKey, + PublicKeyID: pointerTo("keystore://76574:prod:another_key_id"), + PublicKeyHashAlgo: crypto.SHA256, + }, + expectedVSA: nil, + err: serrors.ErrorNoValidSignature, + }, + { + name: "failure: missing needed keyID", + envelope: goodEnvelope, + opts: &options.VerificationOpts{ + PublicKey: goodVSAOpts.PublicKey, + PublicKeyID: pointerTo(""), + PublicKeyHashAlgo: crypto.SHA256, + }, + expectedVSA: nil, + err: serrors.ErrorNoValidSignature, + }, + { + name: "failure: incorrect algorithm", + envelope: goodEnvelope, + opts: &options.VerificationOpts{ + PublicKey: goodVSAOpts.PublicKey, + PublicKeyID: pointerTo(""), + PublicKeyHashAlgo: crypto.SHA512, + }, + expectedVSA: nil, + err: serrors.ErrorNoValidSignature, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + vsa, err := extractSignedVSA(ctx, tc.envelope, tc.opts) + + if diff := cmp.Diff(tc.expectedVSA, vsa, cmpopts.EquateComparable()); diff != "" { + t.Errorf("unexpected VSA (-want +got): \n%s", diff) + } + + if diff := cmp.Diff(tc.err, err, cmpopts.EquateErrors()); diff != "" { + t.Errorf("unexpected error (-want +got): \n%s", diff) + } + }) + } +} + +func Test_matchExpectedValues(t *testing.T) { + t.Parallel() + + goodVSA := &vsa10.VSA{ + StatementHeader: intotoGolang.StatementHeader{ + PredicateType: vsa10.PredicateType, + Subject: []intotoGolang.Subject{ + { + Digest: map[string]string{ + "gce_image_id": "8970095005306000053", + "sha256": "abc", + }, + }, + }, + }, + Predicate: vsa10.Predicate{ + Verifier: vsa10.Verifier{ + ID: "https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1", + }, + ResourceURI: "gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre", + VerificationResult: "PASSED", + VerifiedLevels: []string{"BCID_L1", "SLSA_BUILD_LEVEL_2"}, + }, + } + goodVSAOpts := &options.VSAOpts{ + ExpectedDigests: &[]string{"gce_image_id:8970095005306000053"}, + ExpectedVerifierID: pointerTo("https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1"), + ExpectedResourceURI: pointerTo("gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre"), + ExpectedVerifiedLevels: &[]string{"BCID_L1", "SLSA_BUILD_LEVEL_2"}, + } + + tests := []struct { + name string + vsa *vsa10.VSA + opts *options.VSAOpts + err error + }{ + // success cases + { + name: "success", + vsa: goodVSA, + opts: goodVSAOpts, + }, + { + name: "success: empty verifiedLevels", + vsa: &vsa10.VSA{ + StatementHeader: goodVSA.StatementHeader, + Predicate: vsa10.Predicate{ + Verifier: goodVSA.Predicate.Verifier, + ResourceURI: goodVSA.Predicate.ResourceURI, + VerificationResult: goodVSA.Predicate.VerificationResult, + VerifiedLevels: []string{}, + }, + }, + opts: &options.VSAOpts{ + ExpectedDigests: goodVSAOpts.ExpectedDigests, + ExpectedResourceURI: goodVSAOpts.ExpectedResourceURI, + ExpectedVerifiedLevels: &[]string{}, + ExpectedVerifierID: goodVSAOpts.ExpectedVerifierID, + }, + }, + { + name: "success: unspecified verifiedLevels", + vsa: goodVSA, + opts: &options.VSAOpts{ + ExpectedDigests: goodVSAOpts.ExpectedDigests, + ExpectedResourceURI: goodVSAOpts.ExpectedResourceURI, + ExpectedVerifiedLevels: &[]string{}, + ExpectedVerifierID: goodVSAOpts.ExpectedVerifierID, + }, + }, + // failure cases + { + name: "failure empty digests", + vsa: &vsa10.VSA{ + StatementHeader: intotoGolang.StatementHeader{ + PredicateType: vsa10.PredicateType, + Subject: []intotoGolang.Subject{ + { + Digest: map[string]string{}, + }, + }, + }, + Predicate: goodVSA.Predicate, + }, + opts: goodVSAOpts, + err: serrors.ErrorInvalidDssePayload, + }, + { + name: "failure: no supplied digests", + vsa: goodVSA, + opts: &options.VSAOpts{ + ExpectedDigests: &[]string{}, + ExpectedResourceURI: goodVSAOpts.ExpectedResourceURI, + ExpectedVerifiedLevels: goodVSAOpts.ExpectedVerifiedLevels, + ExpectedVerifierID: goodVSAOpts.ExpectedVerifierID, + }, + err: serrors.ErrorEmptyRequiredField, + }, + { + name: "failure: missing digest", + vsa: goodVSA, + opts: &options.VSAOpts{ + ExpectedDigests: &[]string{"zeit:geist"}, + ExpectedResourceURI: goodVSAOpts.ExpectedResourceURI, + ExpectedVerifiedLevels: goodVSAOpts.ExpectedVerifiedLevels, + ExpectedVerifierID: goodVSAOpts.ExpectedVerifierID, + }, + err: serrors.ErrorMissingSubjectDigest, + }, + { + name: "failure: empty verifierID", + vsa: &vsa10.VSA{ + StatementHeader: goodVSA.StatementHeader, + Predicate: vsa10.Predicate{ + Verifier: vsa10.Verifier{}, + ResourceURI: goodVSA.Predicate.ResourceURI, + VerificationResult: goodVSA.Predicate.VerificationResult, + VerifiedLevels: goodVSA.Predicate.VerifiedLevels, + }, + }, + opts: goodVSAOpts, + err: serrors.ErrorEmptyRequiredField, + }, + { + name: "failure: mismatch verifierID", + vsa: goodVSA, + opts: &options.VSAOpts{ + ExpectedDigests: goodVSAOpts.ExpectedDigests, + ExpectedResourceURI: goodVSAOpts.ExpectedResourceURI, + ExpectedVerifiedLevels: goodVSAOpts.ExpectedVerifiedLevels, + ExpectedVerifierID: pointerTo("https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.2"), + }, + err: serrors.ErrorMismatchVerifierID, + }, + { + name: "failure: empty resourceURI", + vsa: &vsa10.VSA{ + StatementHeader: goodVSA.StatementHeader, + Predicate: vsa10.Predicate{ + Verifier: goodVSA.Predicate.Verifier, + ResourceURI: "", + VerificationResult: goodVSA.Predicate.VerificationResult, + VerifiedLevels: goodVSA.Predicate.VerifiedLevels, + }, + }, + opts: goodVSAOpts, + err: serrors.ErrorEmptyRequiredField, + }, + { + name: "failure: mismatch resourceURI", + vsa: goodVSA, + opts: &options.VSAOpts{ + ExpectedDigests: goodVSAOpts.ExpectedDigests, + ExpectedResourceURI: pointerTo("gce_image://gke-node-images:gke-126GGG"), + ExpectedVerifiedLevels: goodVSAOpts.ExpectedVerifiedLevels, + ExpectedVerifierID: goodVSAOpts.ExpectedVerifierID, + }, + err: serrors.ErrorMismatchResourceURI, + }, + { + name: "failure: empty verificationResult", + vsa: &vsa10.VSA{ + StatementHeader: goodVSA.StatementHeader, + Predicate: vsa10.Predicate{ + Verifier: goodVSA.Predicate.Verifier, + ResourceURI: goodVSA.Predicate.ResourceURI, + VerificationResult: "", + VerifiedLevels: goodVSA.Predicate.VerifiedLevels, + }, + }, + opts: goodVSAOpts, + err: serrors.ErrorInvalidVerificationResult, + }, + { + name: "failure: wrong verificationResult", + vsa: &vsa10.VSA{ + StatementHeader: goodVSA.StatementHeader, + Predicate: vsa10.Predicate{ + Verifier: goodVSA.Predicate.Verifier, + ResourceURI: goodVSA.Predicate.ResourceURI, + VerificationResult: "FAILED", + VerifiedLevels: goodVSA.Predicate.VerifiedLevels, + }, + }, + opts: goodVSAOpts, + err: serrors.ErrorInvalidVerificationResult, + }, + { + name: "failure: missing verifiedLevels", + vsa: goodVSA, + opts: &options.VSAOpts{ + ExpectedDigests: goodVSAOpts.ExpectedDigests, + ExpectedResourceURI: goodVSAOpts.ExpectedResourceURI, + ExpectedVerifiedLevels: &[]string{"SLSA_BUILD_LEVEL_3"}, + ExpectedVerifierID: goodVSAOpts.ExpectedVerifierID, + }, + err: serrors.ErrorMismatchVerifiedLevels, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + err := matchExpectedValues(tc.vsa, tc.opts) + if diff := cmp.Diff(tc.err, err, cmpopts.EquateErrors()); diff != "" { + t.Errorf("unexpected error (-want +got): \n%s", diff) + } + }) + } +} + +func mustEncodeAttestationString(attestationString string) string { + dst := &bytes.Buffer{} + if err := json.Compact(dst, []byte(attestationString)); err != nil { + panic(err) + } + return base64.StdEncoding.EncodeToString(dst.Bytes()) +} + +func mustPublicKeyFromBytes(pubKeyBytes []byte) crypto.PublicKey { + pubKey, err := cryptoutils.UnmarshalPEMToPublicKey(pubKeyBytes) + if err != nil { + panic(err) + } + return pubKey +} + +func pointerTo[K any](object K) *K { + return &object +} diff --git a/verifiers/internal/vsa/vsa_test.go b/verifiers/internal/vsa/vsa_test.go deleted file mode 100644 index d503ab2..0000000 --- a/verifiers/internal/vsa/vsa_test.go +++ /dev/null @@ -1,187 +0,0 @@ -package vsa - -import ( - "context" - "crypto" - "os" - "path/filepath" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" // Add this import - "github.com/sigstore/sigstore/pkg/cryptoutils" - - serrors "github.com/slsa-framework/slsa-verifier/v2/errors" - "github.com/slsa-framework/slsa-verifier/v2/options" -) - -const testDir = "./testdata" - -func Test_VerifyVSA(t *testing.T) { - t.Parallel() - - ctx := context.Background() - - tests := []struct { - name string - attestationPath string - vsaOpts *options.VSAOpts - verificationOpts *options.VerificationOpts - err error - }{ - { - "success", - "gce/v1/gke-gce-pre.bcid-vsa.jsonl", - &options.VSAOpts{ - ExpectedDigests: &[]string{"gce_image_id:8970095005306000053"}, - ExpectedVerifierID: pointerTo("https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1"), - ExpectedResourceURI: pointerTo("gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre"), - ExpectedVerifiedLevels: &[]string{"BCID_L1", "SLSA_BUILD_LEVEL_2"}, - }, - &options.VerificationOpts{ - PublicKey: mustPublicKey(filepath.Clean(filepath.Join(testDir, "gce/v1/vsa_signing_public_key.pem"))), - PublicKeyID: pointerTo("keystore://76574:prod:vsa_signing_public_key"), - PublicKeyHashAlgo: crypto.SHA256, - }, - nil, - }, - { - "success: unspecified levels", - "gce/v1/gke-gce-pre.bcid-vsa.jsonl", - &options.VSAOpts{ - ExpectedDigests: &[]string{"gce_image_id:8970095005306000053"}, - ExpectedVerifierID: pointerTo("https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1"), - ExpectedResourceURI: pointerTo("gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre"), - ExpectedVerifiedLevels: &[]string{}, - }, - &options.VerificationOpts{ - PublicKey: mustPublicKey(filepath.Clean(filepath.Join(testDir, "gce/v1/vsa_signing_public_key.pem"))), - PublicKeyID: pointerTo("keystore://76574:prod:vsa_signing_public_key"), - PublicKeyHashAlgo: crypto.SHA256, - }, - nil, - }, - { - "failure: missing levels", - "gce/v1/gke-gce-pre.bcid-vsa.jsonl", - &options.VSAOpts{ - ExpectedDigests: &[]string{"gce_image_id:8970095005306000053"}, - ExpectedVerifierID: pointerTo("https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1"), - ExpectedResourceURI: pointerTo("gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre"), - ExpectedVerifiedLevels: &[]string{"SLSA_BUILD_LEVEL_3"}, - }, - &options.VerificationOpts{ - PublicKey: mustPublicKey(filepath.Clean(filepath.Join(testDir, "gce/v1/vsa_signing_public_key.pem"))), - PublicKeyID: pointerTo("keystore://76574:prod:vsa_signing_public_key"), - PublicKeyHashAlgo: crypto.SHA256, - }, - serrors.ErrorMismatchVerifiedLevels, - }, - { - "failure: unspecified subject digests", - "gce/v1/gke-gce-pre.bcid-vsa.jsonl", - &options.VSAOpts{ - ExpectedDigests: &[]string{}, - ExpectedVerifierID: pointerTo("https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1"), - ExpectedResourceURI: pointerTo("gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre"), - ExpectedVerifiedLevels: &[]string{}, - }, - &options.VerificationOpts{ - PublicKey: mustPublicKey(filepath.Clean(filepath.Join(testDir, "gce/v1/vsa_signing_public_key.pem"))), - PublicKeyID: pointerTo("keystore://76574:prod:vsa_signing_public_key"), - PublicKeyHashAlgo: crypto.SHA256, - }, - serrors.ErrorInvalidSubject, - }, - { - "failure: mismatch subject digests", - "gce/v1/gke-gce-pre.bcid-vsa.jsonl", - &options.VSAOpts{ - ExpectedDigests: &[]string{"my-giest:123"}, - ExpectedVerifierID: pointerTo("https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1"), - ExpectedResourceURI: pointerTo("gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre"), - ExpectedVerifiedLevels: &[]string{}, - }, - &options.VerificationOpts{ - PublicKey: mustPublicKey(filepath.Clean(filepath.Join(testDir, "gce/v1/vsa_signing_public_key.pem"))), - PublicKeyID: pointerTo("keystore://76574:prod:vsa_signing_public_key"), - PublicKeyHashAlgo: crypto.SHA256, - }, - serrors.ErrorMissingSubjectDigest, - }, - { - "failure: mismatch resource URI", - "gce/v1/gke-gce-pre.bcid-vsa.jsonl", - &options.VSAOpts{ - ExpectedDigests: &[]string{"gce_image_id:8970095005306000053"}, - ExpectedVerifierID: pointerTo("https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1"), - ExpectedResourceURI: pointerTo("my-uri://my/path"), - ExpectedVerifiedLevels: &[]string{}, - }, - &options.VerificationOpts{ - PublicKey: mustPublicKey(filepath.Clean(filepath.Join(testDir, "gce/v1/vsa_signing_public_key.pem"))), - PublicKeyID: pointerTo("keystore://76574:prod:vsa_signing_public_key"), - PublicKeyHashAlgo: crypto.SHA256, - }, - serrors.ErrorMismatchResourceURI, - }, - { - "failure: msimatch verifier id", - "gce/v1/gke-gce-pre.bcid-vsa.jsonl", - &options.VSAOpts{ - ExpectedDigests: &[]string{"gce_image_id:8970095005306000053"}, - ExpectedVerifierID: pointerTo("https://celestial-being.gn/gundam"), - ExpectedResourceURI: pointerTo("gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre"), - ExpectedVerifiedLevels: &[]string{}, - }, - &options.VerificationOpts{ - PublicKey: mustPublicKey(filepath.Clean(filepath.Join(testDir, "gce/v1/vsa_signing_public_key.pem"))), - PublicKeyID: pointerTo("keystore://76574:prod:vsa_signing_public_key"), - PublicKeyHashAlgo: crypto.SHA256, - }, - serrors.ErrorMismatchVerifierID, - }, - // TODO: Add more test cases - } - - for _, tt := range tests { - // t.Parallel() - - attestationPath := filepath.Clean(filepath.Join(testDir, tt.attestationPath)) - attestation, err := os.ReadFile(attestationPath) - if err != nil { - t.Errorf("failed to read attestations file: %v", err) - } - - _, trustedAttesterID, err := VerifyVSA(ctx, attestation, tt.vsaOpts, tt.verificationOpts) - if err != nil && trustedAttesterID != nil { - t.Errorf("unexpected trustedAttesterID to be nil: %v", trustedAttesterID) - } - - if err == nil { - if diff := cmp.Diff(*tt.vsaOpts.ExpectedVerifierID, trustedAttesterID.Name()); diff != "" { - t.Errorf("unexpected trustedAttesterID (-want +got): \n%s", diff) - } - } - - if diff := cmp.Diff(tt.err, err, cmpopts.EquateErrors()); diff != "" { - t.Errorf("unexpected error (-want +got): \n%s", diff) - } - } -} - -func mustPublicKey(path string) crypto.PublicKey { - pubKeyBytes, err := os.ReadFile(path) - if err != nil { - panic(err) - } - pubKey, err := cryptoutils.UnmarshalPEMToPublicKey(pubKeyBytes) - if err != nil { - panic(err) - } - return pubKey -} - -func pointerTo[K any](object K) *K { - return &object -}