mirror of
https://github.com/slsa-framework/slsa-verifier.git
synced 2026-05-16 21:46:40 +00:00
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeGa6ZCZn0q6WpaUwJrSk+PPYEsca
|
||||
3Xkk3UrxvbQtoZzTmq0zIYq+4QQl0YBedSyy+XcwAMaUWTouTrB05WhYtg==
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
425
verifiers/internal/vsa/verifier_test.go
Normal file
425
verifiers/internal/vsa/verifier_test.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user