From 610ef6f1afa25cc246a027a3d8f207fd958a37f7 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave Date: Wed, 19 Jun 2024 00:30:15 +0000 Subject: [PATCH] verify reamining fields, print attestations Signed-off-by: Ramon Petgrave --- .../vsa/gce/v1/vsa_signing_public_key.pem | 4 + cli/slsa-verifier/verify.go | 3 + cli/slsa-verifier/verify/options.go | 14 ++ cli/slsa-verifier/verify/verify_vsa.go | 37 ++++- errors/errors.go | 1 + options/options.go | 13 ++ verifiers/internal/vsa/keys/static.go | 12 -- verifiers/internal/vsa/verifier.go | 130 ++++++++++++------ verifiers/utils/builder.go | 17 +++ .../utils/trusted_attestation_producer.go | 6 - verifiers/verifier.go | 3 +- 11 files changed, 181 insertions(+), 59 deletions(-) create mode 100644 cli/slsa-verifier/testdata/vsa/gce/v1/vsa_signing_public_key.pem delete mode 100644 verifiers/internal/vsa/keys/static.go delete mode 100644 verifiers/utils/trusted_attestation_producer.go diff --git a/cli/slsa-verifier/testdata/vsa/gce/v1/vsa_signing_public_key.pem b/cli/slsa-verifier/testdata/vsa/gce/v1/vsa_signing_public_key.pem new file mode 100644 index 0000000..27bda33 --- /dev/null +++ b/cli/slsa-verifier/testdata/vsa/gce/v1/vsa_signing_public_key.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeGa6ZCZn0q6WpaUwJrSk+PPYEsca +3Xkk3UrxvbQtoZzTmq0zIYq+4QQl0YBedSyy+XcwAMaUWTouTrB05WhYtg== +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/cli/slsa-verifier/verify.go b/cli/slsa-verifier/verify.go index 1f372b1..f68250b 100644 --- a/cli/slsa-verifier/verify.go +++ b/cli/slsa-verifier/verify.go @@ -200,6 +200,9 @@ func verifyVSACmd() *cobra.Command { ResourceUri: &o.ResourceUri, VerifiedLevels: &o.VerifiedLevels, PrintAttestations: &o.PrintAttestations, + PublicKeyPath: &o.PublicKeyPath, + PublicKeyID: &o.PublicKeyID, + SignatureHashAlgo: &o.SignatureHashAlgo, } if _, err := v.Exec(cmd.Context()); err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", FAILURE, err) diff --git a/cli/slsa-verifier/verify/options.go b/cli/slsa-verifier/verify/options.go index 1350fad..11679de 100644 --- a/cli/slsa-verifier/verify/options.go +++ b/cli/slsa-verifier/verify/options.go @@ -134,6 +134,9 @@ type VerifyVSAOptions struct { VerifierID string ResourceUri string VerifiedLevels []string + PublicKeyPath string + PublicKeyID string + SignatureHashAlgo string PrintAttestations bool } @@ -159,11 +162,22 @@ func (o *VerifyVSAOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVar(&o.PrintAttestations, "print-attestations", false, "[optional] print the verified attestations to stdout") + cmd.Flags().StringVar(&o.PublicKeyPath, "public-key-path", "", + "path to a public key file") + + cmd.Flags().StringVar(&o.PublicKeyID, "public-key-id", "", + "the ID of the public key") + + cmd.Flags().StringVar(&o.SignatureHashAlgo, "public-key-hash-algo", "SHA256", + "the hash algorithm used to hash the public key, one of SHA256, SHA384, or SHA512") + cmd.MarkFlagRequired("subject-digests") cmd.MarkFlagRequired("attestations-path") cmd.MarkFlagRequired("verifier-id") cmd.MarkFlagRequired("resource-uri") cmd.MarkFlagRequired("verified-levels") + cmd.MarkFlagRequired("public-key-path") + // public-key-id" and "public-key-hash-algo" are optional since they have useful defaults } type workflowInputs struct { diff --git a/cli/slsa-verifier/verify/verify_vsa.go b/cli/slsa-verifier/verify/verify_vsa.go index 0d34cab..d5735fc 100644 --- a/cli/slsa-verifier/verify/verify_vsa.go +++ b/cli/slsa-verifier/verify/verify_vsa.go @@ -16,10 +16,13 @@ package verify import ( "context" + "crypto" "errors" "fmt" "os" + "github.com/sigstore/sigstore/pkg/cryptoutils" + 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" "github.com/slsa-framework/slsa-verifier/v2/verifiers/utils" @@ -33,6 +36,16 @@ type VerifyVSACommand struct { ResourceUri *string VerifiedLevels *[]string PrintAttestations *bool + PublicKeyPath *string + PublicKeyID *string + SignatureHashAlgo *string +} + +var hashAlgos = map[string]crypto.Hash{ + "": crypto.SHA256, // default to SHA256 + "SHA256": crypto.SHA256, + "SHA384": crypto.SHA384, + "SHA512": crypto.SHA512, } // Exec executes the verifiers.VerifyVSA @@ -48,12 +61,34 @@ func (c *VerifyVSACommand) Exec(ctx context.Context) (*utils.TrustedAttesterID, ExpectedResourceURI: *c.ResourceUri, ExpectedVerifiedLevels: *c.VerifiedLevels, } + pubKeyBytes, err := os.ReadFile(*c.PublicKeyPath) + if err != nil { + printFailed(err) + return nil, err + } + pubKey, err := cryptoutils.UnmarshalPEMToPublicKey(pubKeyBytes) + if err != nil { + err = fmt.Errorf("%w: %w", serrors.ErrorInvalidPublicKey, err) + printFailed(err) + return nil, err + } + hashHalgo, ok := hashAlgos[*c.SignatureHashAlgo] + if !ok { + err := fmt.Errorf("%w: %s", serrors.ErrorInvalidHashAlgo, *c.SignatureHashAlgo) + printFailed(err) + return nil, err + } + VerificationOpts := &options.VerificationOpts{ + PublicKey: pubKey, + PublicKeyID: *c.PublicKeyID, + SignatureHashAlgo: hashHalgo, + } attestations, err := os.ReadFile(*c.AttestationsPath) if err != nil { printFailed(err) return nil, err } - verifiedProvenance, outProducerID, err := verifiers.VerifyVSA(ctx, attestations, vsaOpts) + verifiedProvenance, outProducerID, err := verifiers.VerifyVSA(ctx, attestations, vsaOpts, VerificationOpts) if err != nil { printFailed(err) return nil, err diff --git a/errors/errors.go b/errors/errors.go index 9701262..26341c0 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -44,4 +44,5 @@ var ( ErrorInvalidHash = errors.New("invalid hash") ErrorNotPresent = errors.New("not present") ErrorInvalidPublicKey = errors.New("invalid public key") + ErrorInvalidHashAlgo = errors.New("unsupported hash algorithm") ) diff --git a/options/options.go b/options/options.go index 37c7b2f..c16bd73 100644 --- a/options/options.go +++ b/options/options.go @@ -1,5 +1,7 @@ package options +import "crypto" + // ProvenanceOpts are the options for checking provenance information. type ProvenanceOpts struct { // ExpectedBranch is the expected branch (github_ref or github_base_ref) in @@ -52,3 +54,14 @@ type VSAOpts struct { // ExpectedVerifiedLevels is the levels of verification that are passed from user and not verified ExpectedVerifiedLevels []string } + +type VerificationOpts struct { + // PublicKey is the public key used to verify the signature on the Envelope + PublicKey crypto.PublicKey + + // PublicKeyID is the ID of the public key + PublicKeyID string + + // SignatureHashAlgo is the hash algorithm used to hash the signature + SignatureHashAlgo crypto.Hash +} diff --git a/verifiers/internal/vsa/keys/static.go b/verifiers/internal/vsa/keys/static.go deleted file mode 100644 index dec3a4f..0000000 --- a/verifiers/internal/vsa/keys/static.go +++ /dev/null @@ -1,12 +0,0 @@ -package keys - -// GoogleVSASigningPublicKey is the public key used to verify Google VSA signatures. -const GoogleVSASigningPublicKey = `-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeGa6ZCZn0q6WpaUwJrSk+PPYEsca -3Xkk3UrxvbQtoZzTmq0zIYq+4QQl0YBedSyy+XcwAMaUWTouTrB05WhYtg== ------END PUBLIC KEY-----` - -// AttestorKeys is a map of Attestor IDs to their public keys. -var AttestorKeys = map[string]string{ - "keystore://76574:prod:vsa_signing_public_key": GoogleVSASigningPublicKey, -} diff --git a/verifiers/internal/vsa/verifier.go b/verifiers/internal/vsa/verifier.go index 4904ab8..3ebea6d 100644 --- a/verifiers/internal/vsa/verifier.go +++ b/verifiers/internal/vsa/verifier.go @@ -2,18 +2,15 @@ package vsa import ( "context" - "crypto" "fmt" "strings" "github.com/secure-systems-lab/go-securesystemslib/dsse" sigstoreBundle "github.com/sigstore/sigstore-go/pkg/bundle" - sigstoreCryptoUtils "github.com/sigstore/sigstore/pkg/cryptoutils" sigstoreSignature "github.com/sigstore/sigstore/pkg/signature" sigstoreDSSE "github.com/sigstore/sigstore/pkg/signature/dsse" serrors "github.com/slsa-framework/slsa-verifier/v2/errors" "github.com/slsa-framework/slsa-verifier/v2/options" - vsaKeys "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/vsa/keys" vsa10 "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/vsa/v1.0" "github.com/slsa-framework/slsa-verifier/v2/verifiers/utils" ) @@ -22,6 +19,7 @@ import ( func VerifyVSA(ctx context.Context, attestations []byte, vsaOpts *options.VSAOpts, + verificationOpts *options.VerificationOpts, ) ([]byte, *utils.TrustedAttesterID, error) { // parse the envelope envelope, err := utils.EnvelopeFromBytes(attestations) @@ -32,63 +30,58 @@ func VerifyVSA(ctx context.Context, Envelope: envelope, } - // verify the envelope. signature - err = verifyEnvelopeSignature(ctx, &sigstoreEnvelope) + // 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, &sigstoreEnvelope, verificationOpts) if err != nil { return nil, nil, err } - // TODO: - // verify the metadata statement, err := utils.StatementFromEnvelope(envelope) if err != nil { return nil, nil, err } + // 3. parse the VSA, verifying the predicateType. vsa, err := vsa10.VSAFromStatement(statement) if err != nil { return nil, nil, err } + + // 2. match the subject digests, + // 4. match the verifier ID, + // 5. match the expected valuesmatch resourceURI, + // 6. confirm the slsaResult is PASSED, + // 7. match the verifiedLevels, + // no other feields are checked. err = matchExpectedValues(vsa, vsaOpts) if err != nil { return nil, nil, err } - - // TODO: - // print the attestation - return nil, nil, nil + trustedAttesterID, err := utils.TrustedAttesterIDNew(vsa.Predicate.Verifier.ID, false) + if err != nil { + return nil, nil, err + } + vsaBytes, err := envelope.DecodeB64Payload() + if err != nil { + return nil, nil, fmt.Errorf("%w: %w", serrors.ErrorInvalidDssePayload, err) + } + return vsaBytes, trustedAttesterID, nil } -// verifyEnvelopeSignature verifies the signatures of the envelope, requiring at least one signature to be valid. -func verifyEnvelopeSignature(ctx context.Context, sigstoreEnvelope *sigstoreBundle.Envelope) error { - // assemble an "adapter" for each of the signatures and their KeyID - var verifierAdapters []dsse.Verifier - for _, signature := range sigstoreEnvelope.Envelope.Signatures { - keyID := signature.KeyID - pubKeyString, ok := vsaKeys.AttestorKeys[keyID] - if !ok { - continue - } - pubKey, err := sigstoreCryptoUtils.UnmarshalPEMToPublicKey([]byte(pubKeyString)) - if err != nil { - return fmt.Errorf("%w: %w", serrors.ErrorInvalidPublicKey, err) - } - signatureVerifier, err := sigstoreSignature.LoadVerifier(pubKey, crypto.SHA256) - if err != nil { - return fmt.Errorf("%w: loading sigstore DSSE envolope verifier %w", serrors.ErrorInvalidPublicKey, err) - } - verifierAdapter := &sigstoreDSSE.VerifierAdapter{ - SignatureVerifier: signatureVerifier, - Pub: pubKey, - PubKeyID: keyID, // "keystore://76574:prod:vsa_signing_public_key" - } - verifierAdapters = append(verifierAdapters, verifierAdapter) +// verifyEnvelopeSignature verifies the signature of the envelope. +func verifyEnvelopeSignature(ctx context.Context, sigstoreEnvelope *sigstoreBundle.Envelope, verificationOpts *options.VerificationOpts) error { + signatureVerifier, err := sigstoreSignature.LoadVerifier(verificationOpts.PublicKey, verificationOpts.SignatureHashAlgo) + if err != nil { + return fmt.Errorf("%w: loading sigstore DSSE envolope verifier %w", serrors.ErrorInvalidPublicKey, err) } - // create the envelope verifier with all adapters - envelopeVerifier, err := dsse.NewEnvelopeVerifier(verifierAdapters...) + envelopeVerifier, err := dsse.NewEnvelopeVerifier(&sigstoreDSSE.VerifierAdapter{ + SignatureVerifier: signatureVerifier, + Pub: verificationOpts.PublicKey, + PubKeyID: verificationOpts.PublicKeyID, + }) if err != nil { return fmt.Errorf("%w: creating sigstore DSSE envelope verifier %w", serrors.ErrorInvalidPublicKey, err) } - // verify the envelope _, err = envelopeVerifier.Verify(ctx, sigstoreEnvelope.Envelope) if err != nil { return fmt.Errorf("%w: verifying envelope %w", serrors.ErrorInvalidPublicKey, err) @@ -98,10 +91,26 @@ func verifyEnvelopeSignature(ctx context.Context, sigstoreEnvelope *sigstoreBund // matchExpectedValues checks if the expected values are present in the VSA. func matchExpectedValues(vsa *vsa10.VSA, vsaOpts *options.VSAOpts) error { + // 2. match the expected subject digests if err := matchExepectedSubjectDigests(vsa, vsaOpts); err != nil { return err } - // TODO: match other expected values + // 4. match the verifier ID + if err := matchVerifierID(vsa, vsaOpts); err != nil { + return err + } + // 5. match the expected resourceURI + if err := matchResourceURI(vsa, vsaOpts); err != nil { + return err + } + // 6. confirm the slsaResult is Passed + if err := conirmSLASResult(vsa); err != nil { + return err + } + // 7. match the verifiedLevels + if err := matchVerifiedLevels(vsa, vsaOpts); err != nil { + return err + } return nil } @@ -144,3 +153,46 @@ func matchExepectedSubjectDigests(vsa *vsa10.VSA, vsaOpts *options.VSAOpts) erro } return nil } + +// 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 != vsaOpts.ExpectedVerifierID { + return fmt.Errorf("%w: verifier ID mismatch: expected %s, got %s", serrors.ErrorInvalidDssePayload, vsa.Predicate.Verifier.ID, vsa.Predicate.Verifier.ID) + } + return nil +} + +// 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 != vsaOpts.ExpectedResourceURI { + return fmt.Errorf("%w: resource URI mismatch: expected %s, got %s", serrors.ErrorInvalidDssePayload, vsa.Predicate.ResourceURI, vsaOpts.ExpectedResourceURI) + } + return nil +} + +// confirmSLASResult confirms the VSA verification result is PASSED. +func conirmSLASResult(vsa *vsa10.VSA) error { + if normalizeString(vsa.Predicate.VerificationResult) != "PASSED" { + return fmt.Errorf("%w: verification result is not Passed: %s", serrors.ErrorInvalidDssePayload, vsa.Predicate.VerificationResult) + } + return nil +} + +// matchVerifiedLevels checks if the verified levels in the VSA match the expected values. +func matchVerifiedLevels(vsa *vsa10.VSA, vsaOpts *options.VSAOpts) error { + vsaLevels := make(map[string]bool) + for _, level := range vsa.Predicate.VerifiedLevels { + vsaLevels[level] = true + } + for _, expectedLevel := range vsaOpts.ExpectedVerifiedLevels { + if _, ok := vsaLevels[normalizeString(expectedLevel)]; !ok { + return fmt.Errorf("%w: expected verified level not found: %s", serrors.ErrorInvalidDssePayload, expectedLevel) + } + } + return nil +} + +// normalizeString normalizes a string by trimming whitespace and converting to uppercase. +func normalizeString(s string) string { + return strings.TrimSpace(strings.ToUpper(s)) +} diff --git a/verifiers/utils/builder.go b/verifiers/utils/builder.go index 9978d05..1184a74 100644 --- a/verifiers/utils/builder.go +++ b/verifiers/utils/builder.go @@ -9,6 +9,23 @@ import ( serrors "github.com/slsa-framework/slsa-verifier/v2/errors" ) +// TrustedAttesterID represents an identifer that has been explicitly trusted. +type TrustedAttesterID struct { + TrustedBuilderID +} + +// TrustedAttesterIDNew creates a new AttesterID structure. +func TrustedAttesterIDNew(attesterID string, needVersion bool) (*TrustedAttesterID, error) { + builderID, err := TrustedBuilderIDNew(attesterID, needVersion) + if err != nil { + return nil, err + } + trustedAttesterID := &TrustedAttesterID{ + TrustedBuilderID: *builderID, + } + return trustedAttesterID, nil +} + // TrustedBuilderID represents a builder ID that has been explicitly trusted. type TrustedBuilderID struct { name, version string diff --git a/verifiers/utils/trusted_attestation_producer.go b/verifiers/utils/trusted_attestation_producer.go deleted file mode 100644 index 8bd262d..0000000 --- a/verifiers/utils/trusted_attestation_producer.go +++ /dev/null @@ -1,6 +0,0 @@ -package utils - -// TrustedAttesterID represents an identifer that has been explicitly trusted. -type TrustedAttesterID struct { - name, version string -} diff --git a/verifiers/verifier.go b/verifiers/verifier.go index 7ad92fe..717bc07 100644 --- a/verifiers/verifier.go +++ b/verifiers/verifier.go @@ -80,6 +80,7 @@ func VerifyNpmPackage(ctx context.Context, func VerifyVSA(ctx context.Context, attestations []byte, vsaOpts *options.VSAOpts, + verificationOpts *options.VerificationOpts, ) ([]byte, *utils.TrustedAttesterID, error) { - return vsa.VerifyVSA(ctx, attestations, vsaOpts) + return vsa.VerifyVSA(ctx, attestations, vsaOpts, verificationOpts) }