Files
Ramon Petgrave 208ac12589 feat: vsa support (#777)
Fixes #542

Adds support for VSAs.

## Testing process

- added some unit an end-to-end tests
- manually invoking

    ```
    go run ./cli/slsa-verifier/ verify-vsa \
    --subject-digest gce_image_id:8970095005306000053 \
--attestation-path
./cli/slsa-verifier/testdata/vsa/gce/v1/gke-gce-pre.bcid-vsa.jsonl \
--verifier-id
https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1 \
--resource-uri
gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre
\
    --verified-level BCID_L1 \
    --verified-level SLSA_BUILD_LEVEL_2 \
--public-key-path
./cli/slsa-verifier/testdata/vsa/gce/v1/vsa_signing_public_key.pem \
    --public-key-id keystore://76574:prod:vsa_signing_public_key \
    --print-attestation



{"_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"}}]}
    Verifying VSA: PASSED
    
    PASSED: SLSA verification passed
    ```

TODOS:
- open issue on the in_toto attestations repo about the incorrect json
[fields](https://github.com/in-toto/attestation/blob/36c11295429a997d5bb520b4e80a1d0c16845f9c/go/predicates/vsa/v1/vsa.pb.go#L26-L40)
for vsa 1.0

---------

Signed-off-by: Ramon Petgrave <ramon.petgrave64@gmail.com>
2024-07-10 21:25:16 -04:00

277 lines
10 KiB
Go

package vsa
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/secure-systems-lab/go-securesystemslib/dsse"
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"
vsa10 "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/vsa/v1.0"
"github.com/slsa-framework/slsa-verifier/v2/verifiers/utils"
)
// VerifyVSA verifies the VSA attestation. It returns the attestation base64-decoded from the envelope, and the trusted attester ID.
// We don't return a TrustedBuilderID. Instead, the user can user can parse the builderID separately, perhaps with
// https://pkg.go.dev/golang.org/x/mod/semver.
func VerifyVSA(ctx context.Context,
attestation []byte,
vsaOpts *options.VSAOpts,
verificationOpts *options.VerificationOpts,
) ([]byte, 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, err
}
// 1. verify the envelope signature,
// 4. match the verfier with the public key: implicit because we accept a user-provided public key.
// 3. parse the VSA, verifying the predicateType.
vsa, err := extractSignedVSA(ctx, envelope, verificationOpts)
if err != nil {
return nil, err
}
// 2. match the subject digests,
// 4. match the verifier ID,
// 5. match the expected resourceURI,
// 6. confirm the slsaResult is PASSED,
// 7. match the verifiedLevels,
// no other fields are checked.
err = matchExpectedValues(vsa, vsaOpts)
if err != nil {
return nil, err
}
vsaBytes, err := envelope.DecodeB64Payload()
if err != nil {
return nil, fmt.Errorf("%w: %w", serrors.ErrorInvalidDssePayload, err)
}
return vsaBytes, 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)
}
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)
}
_, err = envelopeVerifier.Verify(ctx, envelope)
if err != nil {
return fmt.Errorf("%w: verifying envelope: %w", serrors.ErrorNoValidSignature, err)
}
return nil
}
// 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
}
// 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 verificationResult is Passed
if err := confirmVerificationResult(vsa); err != nil {
return err
}
// 7. match the verifiedLevels
if err := matchVerifiedLevels(vsa, vsaOpts); err != nil {
return err
}
return nil
}
// 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.ErrorEmptyRequiredField)
}
// collect all digests from the VSA, so we can efficiently search, e.g.:
// {
// "sha256": {
// "abc": true,
// "def": true,
// },
// "gce_image_id": {
// "123": true,
// "456": true,
// }
// }
allVSASubjectDigests := make(map[string]map[string]bool)
for _, subject := range vsa.Subject {
for digestType, digestValue := range subject.Digest {
if _, ok := allVSASubjectDigests[digestType]; !ok {
allVSASubjectDigests[digestType] = make(map[string]bool)
}
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)
if len(parts) != 2 {
return fmt.Errorf("%w: expected digest %s is not in the format <digest type>:<digest value>", serrors.ErrorInvalidDssePayload, expectedDigest)
}
digestType := parts[0]
digestValue := parts[1]
if _, ok := allVSASubjectDigests[digestType]; !ok {
return fmt.Errorf("%w: expected digest not found: %s", serrors.ErrorMissingSubjectDigest, expectedDigest)
}
if _, ok := allVSASubjectDigests[digestType][digestValue]; !ok {
return fmt.Errorf("%w: expected digest not found: %s", serrors.ErrorMissingSubjectDigest, expectedDigest)
}
}
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 == "" {
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)
}
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 == "" {
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)
}
return nil
}
// confirmVerificationResult checks that the policy verification result is "PASSED".
func confirmVerificationResult(vsa *vsa10.VSA) error {
if vsa.Predicate.VerificationResult != "PASSED" {
return fmt.Errorf("%w: verification result is not Passed: %s", serrors.ErrorInvalidVerificationResult, 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 {
// check for SLSA track levels
wantedSLSALevels, err := extractSLSALevels(vsaOpts.ExpectedVerifiedLevels)
if err != nil {
return err
}
gotSLSALevels, err := extractSLSALevels(&vsa.Predicate.VerifiedLevels)
if err != nil {
return err
}
for track, expectedMinLSLSALevel := range wantedSLSALevels {
if vsaLevel, exists := gotSLSALevels[track]; !exists {
return fmt.Errorf("%w: expected SLSA level not found: %s", serrors.ErrorMismatchVerifiedLevels, track)
} else if vsaLevel < expectedMinLSLSALevel {
return fmt.Errorf("%w: expected SLSA level %s to be at least %d, got %d", serrors.ErrorMismatchVerifiedLevels, track, expectedMinLSLSALevel, vsaLevel)
}
}
// check for non-SLSA track levels
nonSLSAVSALevels := make(map[string]bool)
for _, level := range vsa.Predicate.VerifiedLevels {
if isSLSATRACKLevel(level) {
continue
}
nonSLSAVSALevels[level] = true
}
for _, expectedLevel := range *vsaOpts.ExpectedVerifiedLevels {
if isSLSATRACKLevel(expectedLevel) {
continue
}
if _, ok := nonSLSAVSALevels[expectedLevel]; !ok {
return fmt.Errorf("%w: expected verified level not found: %s", serrors.ErrorMismatchVerifiedLevels, expectedLevel)
}
}
return nil
}
// isSLSATRACKLevel checks if the level is an SLSA track level.
// SLSA track levels are of the form SLSA_<track>_LEVEL_<level>, e.g., SLSA_BUILD_LEVEL_2.
func isSLSATRACKLevel(level string) bool {
return strings.HasPrefix(level, "SLSA_")
}
// extractSLSALevels extracts the SLSA levels from the verified levels.
// It returns a map of track to the highest level found, e.g.,
// SLSA_BUILD_LEVEL_2, SLSA_SOURCE_LEVEL_3 ->
//
// {
// "BUILD": 2,
// "SOURCE": 3,
// }
func extractSLSALevels(trackLevels *[]string) (map[string]int, error) {
vsaSLSATrackLadder := make(map[string]int)
for _, trackLevel := range *trackLevels {
if !strings.HasPrefix(trackLevel, "SLSA_") {
continue
}
parts := strings.SplitN(trackLevel, "_", 4)
if len(parts) != 4 {
return nil, fmt.Errorf("%w: invalid SLSA level: %s", serrors.ErrorInvalidSLSALevel, trackLevel)
}
if parts[2] != "LEVEL" {
return nil, fmt.Errorf("%w: invalid SLSA level: %s", serrors.ErrorInvalidSLSALevel, trackLevel)
}
track := parts[1]
level, err := strconv.Atoi(parts[3])
if err != nil {
return nil, fmt.Errorf("%w: invalid SLSA level: %s", serrors.ErrorInvalidSLSALevel, trackLevel)
}
if currentLevel, exists := vsaSLSATrackLadder[track]; exists {
vsaSLSATrackLadder[track] = max(currentLevel, level)
} else {
vsaSLSATrackLadder[track] = level
}
}
return vsaSLSATrackLadder, nil
}