verify reamining fields, print attestations

Signed-off-by: Ramon Petgrave <ramon.petgrave64@gmail.com>
This commit is contained in:
Ramon Petgrave
2024-06-19 00:30:15 +00:00
parent 13a74b5b4a
commit 610ef6f1af
11 changed files with 181 additions and 59 deletions

View File

@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeGa6ZCZn0q6WpaUwJrSk+PPYEsca
3Xkk3UrxvbQtoZzTmq0zIYq+4QQl0YBedSyy+XcwAMaUWTouTrB05WhYtg==
-----END PUBLIC KEY-----

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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

View File

@@ -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")
)

View File

@@ -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
}

View File

@@ -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,
}

View File

@@ -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))
}

View File

@@ -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

View File

@@ -1,6 +0,0 @@
package utils
// TrustedAttesterID represents an identifer that has been explicitly trusted.
type TrustedAttesterID struct {
name, version string
}

View File

@@ -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)
}