From 3de1b6d450ec00592b64efdd029e9304c216f73d Mon Sep 17 00:00:00 2001 From: Asra Ali Date: Wed, 7 Dec 2022 15:55:34 -0600 Subject: [PATCH] feat: support keyless verification for verify-blob-attestation Signed-off-by: Asra Ali fix Signed-off-by: Asra Ali fix failure Signed-off-by: Asra Ali fix windows Signed-off-by: Asra Ali update cli options Signed-off-by: Asra Ali docgen Signed-off-by: Asra Ali add close Signed-off-by: Asra Ali fix test Signed-off-by: Asra Ali --- cmd/cosign/cli/options/verify.go | 25 +- cmd/cosign/cli/verify.go | 28 +- cmd/cosign/cli/verify/verify_blob.go | 45 +-- .../cli/verify/verify_blob_attestation.go | 352 +++++++++++++----- .../verify/verify_blob_attestation_test.go | 51 +-- cmd/cosign/cli/verify/verify_blob_test.go | 101 +++-- doc/cosign_verify-blob-attestation.md | 30 +- doc/cosign_verify-blob.md | 52 ++- pkg/cosign/verify.go | 6 +- test/e2e_test.go | 11 +- 10 files changed, 451 insertions(+), 250 deletions(-) diff --git a/cmd/cosign/cli/options/verify.go b/cmd/cosign/cli/options/verify.go index 0d2fc47d876..80fd353234e 100644 --- a/cmd/cosign/cli/options/verify.go +++ b/cmd/cosign/cli/options/verify.go @@ -141,7 +141,6 @@ type VerifyBlobOptions struct { SecurityKey SecurityKeyOptions CertVerify CertVerifyOptions Rekor RekorOptions - Registry RegistryOptions CommonVerifyOptions CommonVerifyOptions RFC3161TimestampPath string @@ -154,7 +153,6 @@ func (o *VerifyBlobOptions) AddFlags(cmd *cobra.Command) { o.SecurityKey.AddFlags(cmd) o.Rekor.AddFlags(cmd) o.CertVerify.AddFlags(cmd) - o.Registry.AddFlags(cmd) o.CommonVerifyOptions.AddFlags(cmd) cmd.Flags().StringVar(&o.Key, "key", "", @@ -190,7 +188,17 @@ func (o *VerifyDockerfileOptions) AddFlags(cmd *cobra.Command) { type VerifyBlobAttestationOptions struct { Key string SignaturePath string + BundlePath string + PredicateOptions + CheckClaims bool + + SecurityKey SecurityKeyOptions + CertVerify CertVerifyOptions + Rekor RekorOptions + CommonVerifyOptions CommonVerifyOptions + + RFC3161TimestampPath string } var _ Interface = (*VerifyBlobOptions)(nil) @@ -198,10 +206,23 @@ var _ Interface = (*VerifyBlobOptions)(nil) // AddFlags implements Interface func (o *VerifyBlobAttestationOptions) AddFlags(cmd *cobra.Command) { o.PredicateOptions.AddFlags(cmd) + o.SecurityKey.AddFlags(cmd) + o.Rekor.AddFlags(cmd) + o.CertVerify.AddFlags(cmd) + o.CommonVerifyOptions.AddFlags(cmd) cmd.Flags().StringVar(&o.Key, "key", "", "path to the public key file, KMS URI or Kubernetes Secret") cmd.Flags().StringVar(&o.SignaturePath, "signature", "", "path to base64-encoded signature over attestation in DSSE format") + + cmd.Flags().StringVar(&o.BundlePath, "bundle", "", + "path to bundle FILE") + + cmd.Flags().BoolVar(&o.CheckClaims, "check-claims", true, + "whether to check the claims found") + + cmd.Flags().StringVar(&o.RFC3161TimestampPath, "rfc3161-timestamp", "", + "path to RFC3161 timestamp FILE") } diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index 84adf071b50..30b781bb265 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -329,10 +329,32 @@ The blob may be specified as a path to a file.`, Args: cobra.ExactArgs(1), PersistentPreRun: options.BindViper, RunE: func(cmd *cobra.Command, args []string) error { + ko := options.KeyOpts{ + KeyRef: o.Key, + Sk: o.SecurityKey.Use, + Slot: o.SecurityKey.Slot, + RekorURL: o.Rekor.URL, + BundlePath: o.BundlePath, + RFC3161TimestampPath: o.RFC3161TimestampPath, + TSACertChainPath: o.CommonVerifyOptions.TSACertChainPath, + } v := verify.VerifyBlobAttestationCommand{ - KeyRef: o.Key, - PredicateType: o.PredicateOptions.Type, - SignaturePath: o.SignaturePath, + KeyOpts: ko, + PredicateType: o.PredicateOptions.Type, + CheckClaims: o.CheckClaims, + SignaturePath: o.SignaturePath, + CertVerifyOptions: o.CertVerify, + CertRef: o.CertVerify.Cert, + CertChain: o.CertVerify.CertChain, + CertGithubWorkflowTrigger: o.CertVerify.CertGithubWorkflowTrigger, + CertGithubWorkflowSHA: o.CertVerify.CertGithubWorkflowSha, + CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName, + CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, + CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, + IgnoreSCT: o.CertVerify.IgnoreSCT, + SCTRef: o.CertVerify.SCT, + Offline: o.CommonVerifyOptions.Offline, + SkipTlogVerify: o.CommonVerifyOptions.SkipTlogVerify, } if len(args) != 1 { return fmt.Errorf("no path to blob passed in, run `cosign verify-blob-attestation -h` for more help") diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 8bdaf0669f1..b371dd45907 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -27,7 +27,6 @@ import ( "os" "path/filepath" - ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/sigstore/cosign/v2/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor" @@ -40,7 +39,6 @@ import ( "github.com/sigstore/cosign/v2/pkg/oci/static" sigs "github.com/sigstore/cosign/v2/pkg/signature" - ctypes "github.com/sigstore/cosign/v2/pkg/types" "github.com/sigstore/sigstore/pkg/cryptoutils" ) @@ -288,30 +286,12 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } } - // Use the DSSE verifier if the payload is a DSSE with the In-Toto format. - // TODO: This verifier only supports verification of a single signer/signature on - // the envelope. Either have the verifier validate that only one signature exists, - // or use a multi-signature verifier. - if isIntotoDSSE(blobBytes) { - // co.SigVerifier = dsse.WrapVerifier(co.SigVerifier) - signature, err := static.NewAttestation(blobBytes, opts...) - if err != nil { - return err - } - // We have no artifact the attestation is tied to, so we can't do any claim - // verification. - // TODO: Add an option to support this to populate the v1.Hash for a claim. - if _, err = cosign.VerifyBlobAttestation(ctx, signature, co); err != nil { - return err - } - } else { - signature, err := static.NewSignature(blobBytes, sig, opts...) - if err != nil { - return err - } - if _, err = cosign.VerifyBlobSignature(ctx, signature, co); err != nil { - return err - } + signature, err := static.NewSignature(blobBytes, sig, opts...) + if err != nil { + return err + } + if _, err = cosign.VerifyBlobSignature(ctx, signature, co); err != nil { + return err } fmt.Fprintln(os.Stderr, "Verified OK") @@ -361,16 +341,3 @@ func payloadBytes(blobRef string) ([]byte, error) { } return blobBytes, nil } - -// isIntotoDSSE checks whether a payload is a Dead Simple Signing Envelope with the In-Toto format. -func isIntotoDSSE(blobBytes []byte) bool { - DSSEenvelope := ssldsse.Envelope{} - if err := json.Unmarshal(blobBytes, &DSSEenvelope); err != nil { - return false - } - if DSSEenvelope.PayloadType != ctypes.IntotoPayloadType { - return false - } - - return true -} diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go index 07ea1a89ed7..1f0f7dff6c9 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -16,158 +16,328 @@ package verify import ( - "bytes" "context" "crypto" + "crypto/sha256" + "crypto/x509" "encoding/base64" "encoding/hex" "encoding/json" + "errors" "fmt" + "io" "os" "path/filepath" - "strings" - - "github.com/in-toto/in-toto-golang/in_toto" - ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor" + internal "github.com/sigstore/cosign/v2/internal/pkg/cosign" + "github.com/sigstore/cosign/v2/internal/pkg/cosign/tsa" + "github.com/sigstore/cosign/v2/pkg/blob" "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/cosign/v2/pkg/cosign/bundle" + "github.com/sigstore/cosign/v2/pkg/cosign/pivkey" "github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key" + "github.com/sigstore/cosign/v2/pkg/oci/static" + "github.com/sigstore/cosign/v2/pkg/policy" sigs "github.com/sigstore/cosign/v2/pkg/signature" - "github.com/sigstore/cosign/v2/pkg/types" - "github.com/sigstore/sigstore/pkg/signature" - "github.com/sigstore/sigstore/pkg/signature/dsse" + "github.com/sigstore/sigstore/pkg/cryptoutils" ) // VerifyBlobAttestationCommand verifies an attestation on a supplied blob // nolint type VerifyBlobAttestationCommand struct { + options.KeyOpts + options.CertVerifyOptions + + CertRef string + CertChain string + + CertGithubWorkflowTrigger string + CertGithubWorkflowSHA string + CertGithubWorkflowName string + CertGithubWorkflowRepository string + CertGithubWorkflowRef string + + IgnoreSCT bool + SCTRef string + Offline bool + SkipTlogVerify bool + CheckClaims bool - KeyRef string PredicateType string + // TODO: Add policies SignaturePath string // Path to the signature } // Exec runs the verification command -func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath string) error { - if c.SignaturePath == "" { - return fmt.Errorf("please specify path to the base64 encoded DSSE envelope signature via --signature") +func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath string) (err error) { + if options.NOf(c.SignaturePath, c.BundlePath) == 0 { + return fmt.Errorf("please specify path to the DSSE envelope signature via --signature or --bundle") } - // TODO: Add support for security keys and keyless signing - if !options.OneOf(c.KeyRef) { - return &options.PubKeyParseError{} + // Require a certificate/key OR a local bundle file that has the cert. + if options.NOf(c.KeyRef, c.CertRef, c.Sk, c.BundlePath) == 0 { + return fmt.Errorf("provide a key with --key or --sk, a certificate to verify against with --certificate, or a bundle with --bundle") } - var err error - co := &cosign.CheckOpts{} + // We can't have both a key and a security key + if options.NOf(c.KeyRef, c.Sk) > 1 { + return &options.KeyParseError{} + } + + var identities []cosign.Identity + if c.KeyRef == "" { + identities, err = c.Identities() + if err != nil { + return err + } + } + co := &cosign.CheckOpts{ + Identities: identities, + CertGithubWorkflowTrigger: c.CertGithubWorkflowTrigger, + CertGithubWorkflowSha: c.CertGithubWorkflowSHA, + CertGithubWorkflowName: c.CertGithubWorkflowName, + CertGithubWorkflowRepository: c.CertGithubWorkflowRepository, + CertGithubWorkflowRef: c.CertGithubWorkflowRef, + IgnoreSCT: c.IgnoreSCT, + Offline: c.Offline, + SkipTlogVerify: c.SkipTlogVerify, + } if c.CheckClaims { co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier } - keyRef := c.KeyRef - - // TODO: keyless signing - co.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, keyRef) + // Get the actual digest of the blob + var payload internal.HashReader + f, err := os.Open(filepath.Clean(artifactPath)) if err != nil { - return fmt.Errorf("loading public key: %w", err) - } - pkcs11Key, ok := co.SigVerifier.(*pkcs11key.Key) - if ok { - defer pkcs11Key.Close() + return err } + defer f.Close() - // Read the signature and decode it (it should be base64-encoded) - encodedSig, err := os.ReadFile(filepath.Clean(c.SignaturePath)) - if err != nil { - return fmt.Errorf("reading %s: %w", c.SignaturePath, err) + payload = internal.NewHashReader(f, sha256.New()) + if _, err := io.ReadAll(&payload); err != nil { + return err + } + digest := payload.Sum(nil) + h := v1.Hash{ + Hex: hex.EncodeToString(digest), + Algorithm: "sha256", } - // Verify the signature on the attestation against the provided public key - env := ssldsse.Envelope{} - if err := json.Unmarshal(encodedSig, &env); err != nil { - return fmt.Errorf("marshaling envelope: %w", err) + // Set up TSA, Fulcio roots and tlog public keys and clients. + if c.RFC3161TimestampPath != "" && c.KeyOpts.TSACertChainPath == "" { + return fmt.Errorf("timestamp-cert-chain is required to validate a rfc3161 timestamp bundle") } + if c.KeyOpts.TSACertChainPath != "" { + _, err := os.Stat(c.TSACertChainPath) + if err != nil { + return fmt.Errorf("unable to open timestamp certificate chain file: %w", err) + } + // TODO: Add support for TUF certificates. + pemBytes, err := os.ReadFile(filepath.Clean(c.TSACertChainPath)) + if err != nil { + return fmt.Errorf("error reading certification chain path file: %w", err) + } - if env.PayloadType != types.IntotoPayloadType { - return cosign.NewVerificationError("invalid payloadType %s on envelope. Expected %s", env.PayloadType, types.IntotoPayloadType) + leaves, intermediates, roots, err := tsa.SplitPEMCertificateChain(pemBytes) + if err != nil { + return fmt.Errorf("error splitting certificates: %w", err) + } + if len(leaves) > 1 { + return fmt.Errorf("certificate chain must contain at most one TSA certificate") + } + if len(leaves) == 1 { + co.TSACertificate = leaves[0] + } + co.TSAIntermediateCertificates = intermediates + co.TSARootCertificates = roots } - dssev, err := ssldsse.NewEnvelopeVerifier(&dsse.VerifierAdapter{SignatureVerifier: co.SigVerifier}) - if err != nil { - return fmt.Errorf("new envelope verifier: %w", err) + + if !c.SkipTlogVerify { + if c.RekorURL != "" { + rekorClient, err := rekor.NewClient(c.RekorURL) + if err != nil { + return fmt.Errorf("creating Rekor client: %w", err) + } + co.RekorClient = rekorClient + } + // This performs an online fetch of the Rekor public keys, but this is needed + // for verifying tlog entries (both online and offline). + co.RekorPubKeys, err = cosign.GetRekorPubs(ctx) + if err != nil { + return fmt.Errorf("getting Rekor public keys: %w", err) + } } - if _, err := dssev.Verify(&env); err != nil { - return fmt.Errorf("dsse verify: %w", err) + if keylessVerification(c.KeyRef, c.Sk) { + // Use default TUF roots if a cert chain is not provided. + // This performs an online fetch of the Fulcio roots. This is needed + // for verifying keyless certificates (both online and offline). + if c.CertChain == "" { + co.RootCerts, err = fulcio.GetRoots() + if err != nil { + return fmt.Errorf("getting Fulcio roots: %w", err) + } + co.IntermediateCerts, err = fulcio.GetIntermediates() + if err != nil { + return fmt.Errorf("getting Fulcio intermediates: %w", err) + } + } } - co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) - if err != nil { - return fmt.Errorf("getting ctlog public keys: %w", err) + if !c.IgnoreSCT { + co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) + if err != nil { + return fmt.Errorf("getting ctlog public keys: %w", err) + } } - // Verify the attestation is for the provided blob and the predicate type - if err := verifyBlobAttestation(env, artifactPath, c.PredicateType); err != nil { - return err + var encodedSig []byte + if c.SignaturePath != "" { + encodedSig, err = os.ReadFile(filepath.Clean(c.SignaturePath)) + if err != nil { + return fmt.Errorf("reading %s: %w", c.SignaturePath, err) + } } - fmt.Fprintln(os.Stderr, "Verified OK") - return nil -} - -func verifyBlobAttestation(env ssldsse.Envelope, blobPath, predicateType string) error { - artifact, err := os.ReadFile(blobPath) - if err != nil { - return fmt.Errorf("reading %s: %w", blobPath, err) + // Keys are optional! + var cert *x509.Certificate + opts := make([]static.Option, 0) + switch { + case c.KeyRef != "": + co.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, c.KeyRef) + if err != nil { + return fmt.Errorf("loading public key: %w", err) + } + pkcs11Key, ok := co.SigVerifier.(*pkcs11key.Key) + if ok { + defer pkcs11Key.Close() + } + case c.Sk: + sk, err := pivkey.GetKeyWithSlot(c.Slot) + if err != nil { + return fmt.Errorf("opening piv token: %w", err) + } + defer sk.Close() + co.SigVerifier, err = sk.Verifier() + if err != nil { + return fmt.Errorf("loading public key from token: %w", err) + } + case c.CertRef != "": + cert, err = loadCertFromFileOrURL(c.CertRef) + if err != nil { + return err + } } - - // Get the actual digest of the blob - digest, _, err := signature.ComputeDigestForSigning(bytes.NewReader(artifact), crypto.SHA256, []crypto.Hash{crypto.SHA256, crypto.SHA384}) - if err != nil { - return err + if c.BundlePath != "" { + b, err := cosign.FetchLocalSignedPayloadFromPath(c.BundlePath) + if err != nil { + return err + } + // A certificate is required in the bundle unless we specified with + // --key, --sk, or --certificate. + if b.Cert == "" && co.SigVerifier == nil && cert == nil { + return fmt.Errorf("bundle does not contain cert for verification, please provide public key") + } + // We have to condition on this because sign-blob may not output the signing + // key to the bundle when there is no tlog upload. + if b.Cert != "" { + // b.Cert can either be a certificate or public key + certBytes := []byte(b.Cert) + if isb64(certBytes) { + certBytes, _ = base64.StdEncoding.DecodeString(b.Cert) + } + cert, err = loadCertFromPEM(certBytes) + if err != nil { + // check if cert is actually a public key + co.SigVerifier, err = sigs.LoadPublicKeyRaw(certBytes, crypto.SHA256) + if err != nil { + return fmt.Errorf("loading verifier from bundle: %w", err) + } + } + } + encodedSig, err = base64.StdEncoding.DecodeString(b.Base64Signature) + if err != nil { + return fmt.Errorf("decoding signature: %w", err) + } + opts = append(opts, static.WithBundle(b.Bundle)) } - actualDigest := strings.ToLower(hex.EncodeToString(digest)) - - // Get the expected digest from the attestation - decodedPredicate, err := base64.StdEncoding.DecodeString(env.Payload) - if err != nil { - return fmt.Errorf("decoding dsse payload: %w", err) + if c.RFC3161TimestampPath != "" { + var rfc3161Timestamp bundle.RFC3161Timestamp + ts, err := blob.LoadFileOrURL(c.RFC3161TimestampPath) + if err != nil { + return err + } + if err := json.Unmarshal(ts, &rfc3161Timestamp); err != nil { + return err + } + opts = append(opts, static.WithRFC3161Timestamp(&rfc3161Timestamp)) } - var statement in_toto.Statement - if err := json.Unmarshal(decodedPredicate, &statement); err != nil { - return fmt.Errorf("decoding predicate: %w", err) + // Set an SCT if provided via the CLI. + if c.SCTRef != "" { + sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) + if err != nil { + return fmt.Errorf("reading sct from file: %w", err) + } + co.SCT = sct } - - // Compare the actual and expected - if statement.Subject == nil { - return fmt.Errorf("no subject in intoto statement") + // Set a cert chain if provided. + var chainPEM []byte + if c.CertChain != "" { + chain, err := loadCertChainFromFileOrURL(c.CertChain) + if err != nil { + return err + } + if chain == nil { + return errors.New("expected certificate chain in --certificate-chain") + } + // Set the last one in the co.RootCerts. This is trusted, as its passed in + // via the CLI. + if co.RootCerts == nil { + co.RootCerts = x509.NewCertPool() + } + co.RootCerts.AddCert(chain[len(chain)-1]) + // Use the whole as the cert chain in the signature object. + // The last one is omitted because it is considered the "root". + chainPEM, err = cryptoutils.MarshalCertificatesToPEM(chain) + if err != nil { + return err + } } - for _, subj := range statement.Subject { - if err := verifySubject(statement, subj, blobPath, actualDigest, predicateType); err == nil { - return nil + // Gather the cert for the signature and add the cert along with the + // cert chain into the signature object. + var certPEM []byte + if cert != nil { + certPEM, err = cryptoutils.MarshalCertificateToPEM(cert) + if err != nil { + return err } + opts = append(opts, static.WithCertChain(certPEM, chainPEM)) } - return fmt.Errorf("attestation does not contain a subject matching the provided blob") -} -func verifySubject(statement in_toto.Statement, subject in_toto.Subject, blobPath, actualDigest, predicateType string) error { - sha256Digest, ok := subject.Digest["sha256"] - if !ok { - return fmt.Errorf("no sha256 digest available") + signature, err := static.NewAttestation(encodedSig, opts...) + if err != nil { + return err } - if sha256Digest != actualDigest { - return fmt.Errorf("expected digest %s but %s has a digest of %s", sha256Digest, blobPath, actualDigest) + // TODO: This verifier only supports verification of a single signer/signature on + // the envelope. Either have the verifier validate that only one signature exists, + // or use a multi-signature verifier. + if _, err = cosign.VerifyBlobAttestation(ctx, signature, h, co); err != nil { + return err } - // Check the predicate - parsedPredicateType, err := options.ParsePredicateType(predicateType) - if err != nil { - return fmt.Errorf("parsing predicate type %s: %w", predicateType, err) - } - if statement.PredicateType != parsedPredicateType { - return fmt.Errorf("expected predicate type %s but is %s: specify an expected predicate type with the --type flag", parsedPredicateType, statement.PredicateType) + // This checks the predicate type -- if no error is returned and no payload is, then + // the attestation is not of the given predicate type. + if b, err := policy.AttestationToPayloadJSON(ctx, c.PredicateType, signature); b == nil && err == nil { + return fmt.Errorf("invalid predicate type, expected %s", c.PredicateType) } + + fmt.Fprintln(os.Stderr, "Verified OK") return nil } diff --git a/cmd/cosign/cli/verify/verify_blob_attestation_test.go b/cmd/cosign/cli/verify/verify_blob_attestation_test.go index 37dce9e27d0..bb9f5a58029 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation_test.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation_test.go @@ -15,36 +15,38 @@ package verify import ( + "context" "encoding/base64" - "encoding/json" "os" - "path/filepath" "testing" - ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" ) +const pubkey = `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESF79b1ToAtoakhBOHEU5UjnEiihV +gZPFIp557+TOoDxf14FODWc+sIPETk0OgCplAk60doVXbCv33IU4rXZHrg== +-----END PUBLIC KEY----- +` + const ( blobContents = "some-payload" anotherBlobContents = "another-blob" - blobSLSAProvenanceSignature = "eyJwYXlsb2FkVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5pbi10b3RvK2pzb24iLCJwYXlsb2FkIjoiZXlKZmRIbHdaU0k2SW1oMGRIQnpPaTh2YVc0dGRHOTBieTVwYnk5VGRHRjBaVzFsYm5RdmRqQXVNU0lzSW5CeVpXUnBZMkYwWlZSNWNHVWlPaUpvZEhSd2N6b3ZMM05zYzJFdVpHVjJMM0J5YjNabGJtRnVZMlV2ZGpBdU1pSXNJbk4xWW1wbFkzUWlPbHQ3SW01aGJXVWlPaUppYkc5aUlpd2laR2xuWlhOMElqcDdJbk5vWVRJMU5pSTZJalkxT0RjNE1XTmtOR1ZrT1dKallUWXdaR0ZqWkRBNVpqZGlZamt4TkdKaU5URTFNREpsT0dJMVpEWXhPV1kxTjJZek9XRXhaRFkxTWpVNU5tTmpNalFpZlgxZExDSndjbVZrYVdOaGRHVWlPbnNpWW5WcGJHUmxjaUk2ZXlKcFpDSTZJaklpZlN3aVluVnBiR1JVZVhCbElqb2llQ0lzSW1sdWRtOWpZWFJwYjI0aU9uc2lZMjl1Wm1sblUyOTFjbU5sSWpwN2ZYMTlmUT09Iiwic2lnbmF0dXJlcyI6W3sia2V5aWQiOiIiLCJzaWciOiJNR1lDTVFEWHZhVVAwZmlYdXJUcmZNNmtQNjRPcERCM0pzSlEzbFFHZWE5UmZBOVBCY3JmWTJOc0dxK1J0MzdnMlpqaUpKOENNUUNNY3pzVy9wOGJiekZOSkRqeEhlOFNRdTRTazhBa3htTEdLMVE2R2lUazAzb2hHU3dsZkZRNXMrTWxRTFpGZXpBPSJ9XX0=" - dssePredicateEmptySubject = "ewogICJwYXlsb2FkVHlwZSI6ICJhcHBsaWNhdGlvbi92bmQuaW4tdG90bytqc29uIiwKICAicGF5bG9hZCI6ICJld29nSUNKZmRIbHdaU0k2SUNKb2RIUndjem92TDJsdUxYUnZkRzh1YVc4dlUzUmhkR1Z0Wlc1MEwzWXdMakVpTEFvZ0lDSndjbVZrYVdOaGRHVlVlWEJsSWpvZ0ltaDBkSEJ6T2k4dmMyeHpZUzVrWlhZdmNISnZkbVZ1WVc1alpTOTJNQzR5SWl3S0lDQWljM1ZpYW1WamRDSTZJRnNLSUNCZExBb2dJQ0p3Y21Wa2FXTmhkR1VpT2lCN0NpQWdJQ0FpWW5WcGJHUmxjaUk2SUhzS0lDQWdJQ0FnSW1sa0lqb2dJaklpQ2lBZ0lDQjlMQW9nSUNBZ0ltSjFhV3hrVkhsd1pTSTZJQ0o0SWl3S0lDQWdJQ0pwYm5adlkyRjBhVzl1SWpvZ2V3b2dJQ0FnSUNBaVkyOXVabWxuVTI5MWNtTmxJam9nZTMwS0lDQWdJSDBLSUNCOUNuMEsiLAogICJzaWduYXR1cmVzIjogWwogICAgewogICAgICAia2V5aWQiOiAiIiwKICAgICAgInNpZyI6ICJNR1lDTVFEWHZhVVAwZmlYdXJUcmZNNmtQNjRPcERCM0pzSlEzbFFHZWE5UmZBOVBCY3JmWTJOc0dxK1J0MzdnMlpqaUpKOENNUUNNY3pzVy9wOGJiekZOSkRqeEhlOFNRdTRTazhBa3htTEdLMVE2R2lUazAzb2hHU3dsZkZRNXMrTWxRTFpGZXpBPSIKICAgIH0KICBdCn0K" - dssePredicateMissingSha256 = "ewogICJwYXlsb2FkVHlwZSI6ICJhcHBsaWNhdGlvbi92bmQuaW4tdG90bytqc29uIiwKICAicGF5bG9hZCI6ICJld29nSUNKZmRIbHdaU0k2SUNKb2RIUndjem92TDJsdUxYUnZkRzh1YVc4dlUzUmhkR1Z0Wlc1MEwzWXdMakVpTEFvZ0lDSndjbVZrYVdOaGRHVlVlWEJsSWpvZ0ltaDBkSEJ6T2k4dmMyeHpZUzVrWlhZdmNISnZkbVZ1WVc1alpTOTJNQzR5SWl3S0lDQWljM1ZpYW1WamRDSTZJRnNLSUNBZ0lIc0tJQ0FnSUNBZ0ltNWhiV1VpT2lBaVlteHZZaUlzQ2lBZ0lDQWdJQ0prYVdkbGMzUWlPaUI3Q2lBZ0lDQWdJQ0FnSW01dmRITm9ZVEkxTmlJNklDSTJOVGczT0RGalpEUmxaRGxpWTJFMk1HUmhZMlF3T1dZM1ltSTVNVFJpWWpVeE5UQXlaVGhpTldRMk1UbG1OVGRtTXpsaE1XUTJOVEkxT1Raall6STBJZ29nSUNBZ0lDQjlDaUFnSUNCOUNpQWdYU3dLSUNBaWNISmxaR2xqWVhSbElqb2dld29nSUNBZ0ltSjFhV3hrWlhJaU9pQjdDaUFnSUNBZ0lDSnBaQ0k2SUNJeUlnb2dJQ0FnZlN3S0lDQWdJQ0ppZFdsc1pGUjVjR1VpT2lBaWVDSXNDaUFnSUNBaWFXNTJiMk5oZEdsdmJpSTZJSHNLSUNBZ0lDQWdJbU52Ym1acFoxTnZkWEpqWlNJNklIdDlDaUFnSUNCOUNpQWdmUXA5Q2c9PSIsCiAgInNpZ25hdHVyZXMiOiBbCiAgICB7CiAgICAgICJrZXlpZCI6ICIiLAogICAgICAic2lnIjogIk1HWUNNUURYdmFVUDBmaVh1clRyZk02a1A2NE9wREIzSnNKUTNsUUdlYTlSZkE5UEJjcmZZMk5zR3ErUnQzN2cyWmppSko4Q01RQ01jenNXL3A4YmJ6Rk5KRGp4SGU4U1F1NFNrOEFreG1MR0sxUTZHaVRrMDNvaEdTd2xmRlE1cytNbFFMWkZlekE9IgogICAgfQogIF0KfQo=" - dssePredicateMultipleSubjects = "ewogICJwYXlsb2FkVHlwZSI6ICJhcHBsaWNhdGlvbi92bmQuaW4tdG90bytqc29uIiwKICAicGF5bG9hZCI6ICJld29nSUNKZmRIbHdaU0k2SUNKb2RIUndjem92TDJsdUxYUnZkRzh1YVc4dlUzUmhkR1Z0Wlc1MEwzWXdMakVpTEFvZ0lDSndjbVZrYVdOaGRHVlVlWEJsSWpvZ0ltaDBkSEJ6T2k4dmMyeHpZUzVrWlhZdmNISnZkbVZ1WVc1alpTOTJNQzR5SWl3S0lDQWljM1ZpYW1WamRDSTZJRnNLSUNBZ0lIc0tJQ0FnSUNBZ0ltNWhiV1VpT2lBaVlXNXZkR2hsY21Kc2IySWlMQW9nSUNBZ0lDQWlaR2xuWlhOMElqb2dld29nSUNBZ0lDQWdJQ0p6YUdFeU5UWWlPaUFpTlRnek1HWXhOVGd5WVRNek1HRTRNbVEyWkRkak5qRmtNV0UxWVdRM01EWmtOMk0zTkdSak5USTFOVGMxWlRjeE9EVmlObU14WWpJd05UY3hOekkyTmlJS0lDQWdJQ0FnZlFvZ0lDQWdmU3dLSUNBZ0lIc0tJQ0FnSUNBZ0ltNWhiV1VpT2lBaVlteHZZaUlzQ2lBZ0lDQWdJQ0prYVdkbGMzUWlPaUI3Q2lBZ0lDQWdJQ0FnSW5Ob1lUSTFOaUk2SUNJMk5UZzNPREZqWkRSbFpEbGlZMkUyTUdSaFkyUXdPV1kzWW1JNU1UUmlZalV4TlRBeVpUaGlOV1EyTVRsbU5UZG1NemxoTVdRMk5USTFPVFpqWXpJMElnb2dJQ0FnSUNCOUNpQWdJQ0I5Q2lBZ1hTd0tJQ0FpY0hKbFpHbGpZWFJsSWpvZ2V3b2dJQ0FnSW1KMWFXeGtaWElpT2lCN0NpQWdJQ0FnSUNKcFpDSTZJQ0l5SWdvZ0lDQWdmU3dLSUNBZ0lDSmlkV2xzWkZSNWNHVWlPaUFpZUNJc0NpQWdJQ0FpYVc1MmIyTmhkR2x2YmlJNklIc0tJQ0FnSUNBZ0ltTnZibVpwWjFOdmRYSmpaU0k2SUh0OUNpQWdJQ0I5Q2lBZ2ZRcDlDZz09IiwKICAic2lnbmF0dXJlcyI6IFsKICAgIHsKICAgICAgImtleWlkIjogIiIsCiAgICAgICJzaWciOiAiTUdZQ01RRFh2YVVQMGZpWHVyVHJmTTZrUDY0T3BEQjNKc0pRM2xRR2VhOVJmQTlQQmNyZlkyTnNHcStSdDM3ZzJaamlKSjhDTVFDTWN6c1cvcDhiYnpGTkpEanhIZThTUXU0U2s4QWt4bUxHSzFRNkdpVGswM29oR1N3bGZGUTVzK01sUUxaRmV6QT0iCiAgICB9CiAgXQp9Cg==" - dssePredicateMultipleSubjectsInvalid = "ewogICJwYXlsb2FkVHlwZSI6ICJhcHBsaWNhdGlvbi92bmQuaW4tdG90bytqc29uIiwKICAicGF5bG9hZCI6ICJld29nSUNKZmRIbHdaU0k2SUNKb2RIUndjem92TDJsdUxYUnZkRzh1YVc4dlUzUmhkR1Z0Wlc1MEwzWXdMakVpTEFvZ0lDSndjbVZrYVdOaGRHVlVlWEJsSWpvZ0ltaDBkSEJ6T2k4dmMyeHpZUzVrWlhZdmNISnZkbVZ1WVc1alpTOTJNQzR5SWl3S0lDQWljM1ZpYW1WamRDSTZJRnNLSUNBZ0lIc0tJQ0FnSUNBZ0ltNWhiV1VpT2lBaVlXNXZkR2hsY21Kc2IySWlMQW9nSUNBZ0lDQWlaR2xuWlhOMElqb2dld29nSUNBZ0lDQWdJQ0p6YUdFeU5UWWlPaUFpTlRnek1HWXhOVGd5WVRNek1HRTRNbVEyWkRkak5qRmtNV0UxWVdRM01EWmtOMk0zTkdSak5USTFOVGMxWlRjeE9EVmlObU14WWpJd05UY3hOekkyTmlJS0lDQWdJQ0FnZlFvZ0lDQWdmU3dLSUNBZ0lIc0tJQ0FnSUNBZ0ltNWhiV1VpT2lBaVlteHZZaUlzQ2lBZ0lDQWdJQ0prYVdkbGMzUWlPaUI3Q2lBZ0lDQWdJQ0FnSW5Ob1lUSTFOaUk2SUNJMU9ETXdaakUxT0RKaE16TXdZVGd5WkRaa04yTTJNV1F4WVRWaFpEY3dObVEzWXpjMFpHTTFNalUxTnpWbE56RTROV0kyWXpGaU1qQTFOekUzTWpZMklnb2dJQ0FnSUNCOUNpQWdJQ0I5Q2lBZ1hTd0tJQ0FpY0hKbFpHbGpZWFJsSWpvZ2V3b2dJQ0FnSW1KMWFXeGtaWElpT2lCN0NpQWdJQ0FnSUNKcFpDSTZJQ0l5SWdvZ0lDQWdmU3dLSUNBZ0lDSmlkV2xzWkZSNWNHVWlPaUFpZUNJc0NpQWdJQ0FpYVc1MmIyTmhkR2x2YmlJNklIc0tJQ0FnSUNBZ0ltTnZibVpwWjFOdmRYSmpaU0k2SUh0OUNpQWdJQ0I5Q2lBZ2ZRcDlDZz09IiwKICAic2lnbmF0dXJlcyI6IFsKICAgIHsKICAgICAgImtleWlkIjogIiIsCiAgICAgICJzaWciOiAiTUdZQ01RRFh2YVVQMGZpWHVyVHJmTTZrUDY0T3BEQjNKc0pRM2xRR2VhOVJmQTlQQmNyZlkyTnNHcStSdDM3ZzJaamlKSjhDTVFDTWN6c1cvcDhiYnpGTkpEanhIZThTUXU0U2s4QWt4bUxHSzFRNkdpVGswM29oR1N3bGZGUTVzK01sUUxaRmV6QT0iCiAgICB9CiAgXQp9Cg==" + blobSLSAProvenanceSignature = "eyJwYXlsb2FkVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5pbi10b3RvK2pzb24iLCJwYXlsb2FkIjoiZXlKZmRIbHdaU0k2SW1oMGRIQnpPaTh2YVc0dGRHOTBieTVwYnk5VGRHRjBaVzFsYm5RdmRqQXVNU0lzSW5CeVpXUnBZMkYwWlZSNWNHVWlPaUpvZEhSd2N6b3ZMM05zYzJFdVpHVjJMM0J5YjNabGJtRnVZMlV2ZGpBdU1pSXNJbk4xWW1wbFkzUWlPbHQ3SW01aGJXVWlPaUppYkc5aUlpd2laR2xuWlhOMElqcDdJbk5vWVRJMU5pSTZJalkxT0RjNE1XTmtOR1ZrT1dKallUWXdaR0ZqWkRBNVpqZGlZamt4TkdKaU5URTFNREpsT0dJMVpEWXhPV1kxTjJZek9XRXhaRFkxTWpVNU5tTmpNalFpZlgxZExDSndjbVZrYVdOaGRHVWlPbnNpWW5WcGJHUmxjaUk2ZXlKcFpDSTZJaklpZlN3aVluVnBiR1JVZVhCbElqb2llQ0lzSW1sdWRtOWpZWFJwYjI0aU9uc2lZMjl1Wm1sblUyOTFjbU5sSWpwN2ZYMTlmUT09Iiwic2lnbmF0dXJlcyI6W3sia2V5aWQiOiIiLCJzaWciOiJNRVVDSUE4S2pacWtydDkwZnpCb2pTd3d0ajNCcWI0MUU2cnV4UWs5N1RMbnB6ZFlBaUVBek9Bak9Uenl2VEhxYnBGREFuNnpocmc2RVp2N2t4SzVmYVJvVkdZTWgyYz0ifV19" + dssePredicateEmptySubject = "eyJwYXlsb2FkVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5pbi10b3RvK2pzb24iLCJwYXlsb2FkIjoiZXlKZmRIbHdaU0k2SW1oMGRIQnpPaTh2YVc0dGRHOTBieTVwYnk5VGRHRjBaVzFsYm5RdmRqQXVNU0lzSW5CeVpXUnBZMkYwWlZSNWNHVWlPaUpvZEhSd2N6b3ZMM05zYzJFdVpHVjJMM0J5YjNabGJtRnVZMlV2ZGpBdU1pSXNJbk4xWW1wbFkzUWlPbHRkTENKd2NtVmthV05oZEdVaU9uc2lZblZwYkdSbGNpSTZleUpwWkNJNklqSWlmU3dpWW5WcGJHUlVlWEJsSWpvaWVDSXNJbWx1ZG05allYUnBiMjRpT25zaVkyOXVabWxuVTI5MWNtTmxJanA3ZlgxOWZRPT0iLCJzaWduYXR1cmVzIjpbeyJrZXlpZCI6IiIsInNpZyI6Ik1FWUNJUUNrTEV2NkhZZ0svZDdUK0N3NTdXbkZGaHFUTC9WalAyVDA5Q2t1dk1nbDRnSWhBT1hBM0lhWWg1M1FscVk1eVU4cWZxRXJma2tGajlEakZnaWovUTQ2NnJSViJ9XX0=" + dssePredicateMissingSha256 = "eyJwYXlsb2FkVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5pbi10b3RvK2pzb24iLCJwYXlsb2FkIjoiZXlKZmRIbHdaU0k2SW1oMGRIQnpPaTh2YVc0dGRHOTBieTVwYnk5VGRHRjBaVzFsYm5RdmRqQXVNU0lzSW5CeVpXUnBZMkYwWlZSNWNHVWlPaUpvZEhSd2N6b3ZMM05zYzJFdVpHVjJMM0J5YjNabGJtRnVZMlV2ZGpBdU1pSXNJbk4xWW1wbFkzUWlPbHQ3SW01aGJXVWlPaUppYkc5aUlpd2laR2xuWlhOMElqcDdmWDFkTENKd2NtVmthV05oZEdVaU9uc2lZblZwYkdSbGNpSTZleUpwWkNJNklqSWlmU3dpWW5WcGJHUlVlWEJsSWpvaWVDSXNJbWx1ZG05allYUnBiMjRpT25zaVkyOXVabWxuVTI5MWNtTmxJanA3ZlgxOWZRPT0iLCJzaWduYXR1cmVzIjpbeyJrZXlpZCI6IiIsInNpZyI6Ik1FVUNJQysvM2M4RFo1TGFZTEx6SFZGejE3ZmxHUENlZXVNZ2tIKy8wa2s1cFFLUEFpRUFqTStyYnBBRlJybDdpV0I2Vm9BYVZPZ3U3NjRRM0JKdHI1bHk4VEFHczNrPSJ9XX0=" + dssePredicateMultipleSubjects = "eyJwYXlsb2FkVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5pbi10b3RvK2pzb24iLCJwYXlsb2FkIjoiZXlKZmRIbHdaU0k2SW1oMGRIQnpPaTh2YVc0dGRHOTBieTVwYnk5VGRHRjBaVzFsYm5RdmRqQXVNU0lzSW5CeVpXUnBZMkYwWlZSNWNHVWlPaUpvZEhSd2N6b3ZMM05zYzJFdVpHVjJMM0J5YjNabGJtRnVZMlV2ZGpBdU1pSXNJbk4xWW1wbFkzUWlPbHQ3SW01aGJXVWlPaUppYkc5aUlpd2laR2xuWlhOMElqcDdJbk5vWVRJMU5pSTZJalkxT0RjNE1XTmtOR1ZrT1dKallUWXdaR0ZqWkRBNVpqZGlZamt4TkdKaU5URTFNREpsT0dJMVpEWXhPV1kxTjJZek9XRXhaRFkxTWpVNU5tTmpNalFpZlgwc2V5SnVZVzFsSWpvaWIzUm9aWElpTENKa2FXZGxjM1FpT25zaWMyaGhNalUySWpvaU1HUmhOVFU1WXpKbU1USTNNak13WVRGbVlXSmpabUppTWpCa05XUmlPR1JpWVRjMk5Ua3lNMk0yWldaak5tWTBPRE14TmpVeE1UbGpOR015WXpWa05DSjlmVjBzSW5CeVpXUnBZMkYwWlNJNmV5SmlkV2xzWkdWeUlqcDdJbWxrSWpvaU1pSjlMQ0ppZFdsc1pGUjVjR1VpT2lKNElpd2lhVzUyYjJOaGRHbHZiaUk2ZXlKamIyNW1hV2RUYjNWeVkyVWlPbnQ5ZlgxOSIsInNpZ25hdHVyZXMiOlt7ImtleWlkIjoiIiwic2lnIjoiTUVZQ0lRQ20yR2FwNzRzbDkyRC80V2FoWHZiVHFrNFVCaHZsb3oreDZSZm1NQXUyaWdJaEFNcXRFV29DalpGdkpmZWJxRDJFank3aTlHaGc0a0V0WE51bVdLbVBtdEphIn1dfQ==" + dssePredicateMultipleSubjectsInvalid = "eyJwYXlsb2FkVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5pbi10b3RvK2pzb24iLCJwYXlsb2FkIjoiZXlKZmRIbHdaU0k2SW1oMGRIQnpPaTh2YVc0dGRHOTBieTVwYnk5VGRHRjBaVzFsYm5RdmRqQXVNU0lzSW5CeVpXUnBZMkYwWlZSNWNHVWlPaUpvZEhSd2N6b3ZMM05zYzJFdVpHVjJMM0J5YjNabGJtRnVZMlV2ZGpBdU1pSXNJbk4xWW1wbFkzUWlPbHQ3SW01aGJXVWlPaUppYkc5aUlpd2laR2xuWlhOMElqcDdJbk5vWVRJMU5pSTZJbUUyT0RJelpqbGpOekEyTWpCalltWmpOVGt4T0dJMVpUWmtOR0ZoTVRjMFlUaGhNakJrTlRaa1lUVm1NVEEyWWpZMU5qSTNOR013TldRMlptVXhZVGNpZlgwc2V5SnVZVzFsSWpvaWIzUm9aWElpTENKa2FXZGxjM1FpT25zaWMyaGhNalUySWpvaU1HUmhOVFU1WXpKbU1USTNNak13WVRGbVlXSmpabUppTWpCa05XUmlPR1JpWVRjMk5Ua3lNMk0yWldaak5tWTBPRE14TmpVeE1UbGpOR015WXpWa05DSjlmVjBzSW5CeVpXUnBZMkYwWlNJNmV5SmlkV2xzWkdWeUlqcDdJbWxrSWpvaU1pSjlMQ0ppZFdsc1pGUjVjR1VpT2lKNElpd2lhVzUyYjJOaGRHbHZiaUk2ZXlKamIyNW1hV2RUYjNWeVkyVWlPbnQ5ZlgxOSIsInNpZ25hdHVyZXMiOlt7ImtleWlkIjoiIiwic2lnIjoiTUVVQ0lRRGhZbCtWUlBtcWFJc2xxdS9yWGRVbnc2VmpQcXR4RG84bHdqc3p1cWl6MmdJZ0NNRVVlcUZ5RkFZejcyM2IvSTI2L0p3K0U3YkFLMExqeElsUExvTGxPczQ9In1dfQ==" ) func TestVerifyBlobAttestation(t *testing.T) { - tmpdir := t.TempDir() - blobPath := filepath.Join(tmpdir, "blob") - if err := os.WriteFile(blobPath, []byte(blobContents), 0755); err != nil { - t.Fatal(err) - } - anotherBlobPath := filepath.Join(tmpdir, "another-blob") - if err := os.WriteFile(anotherBlobPath, []byte(anotherBlobContents), 0755); err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpdir) + ctx := context.Background() + td := t.TempDir() + defer os.RemoveAll(td) + + blobPath := writeBlobFile(t, td, blobContents, "blob") + anotherBlobPath := writeBlobFile(t, td, anotherBlobContents, "other-blob") + keyRef := writeBlobFile(t, td, pubkey, "cosign.pub") tests := []struct { description string @@ -99,14 +101,17 @@ func TestVerifyBlobAttestation(t *testing.T) { if err != nil { t.Fatal(err) } + sigRef := writeBlobFile(t, td, string(decodedSig), "signature") - // Verify the signature on the attestation against the provided public key - env := ssldsse.Envelope{} - if err := json.Unmarshal(decodedSig, &env); err != nil { - t.Fatal(err) + cmd := VerifyBlobAttestationCommand{ + KeyOpts: options.KeyOpts{KeyRef: keyRef}, + SignaturePath: sigRef, + SkipTlogVerify: true, + CheckClaims: true, + PredicateType: test.predicateType, } + err = cmd.Exec(ctx, test.blobPath) - err = verifyBlobAttestation(env, test.blobPath, test.predicateType) if (err != nil) != test.shouldErr { t.Fatalf("verifyBlobAttestation()= %s, expected shouldErr=%t ", err, test.shouldErr) } diff --git a/cmd/cosign/cli/verify/verify_blob_test.go b/cmd/cosign/cli/verify/verify_blob_test.go index e3670f65ad8..b0eb5ed74db 100644 --- a/cmd/cosign/cli/verify/verify_blob_test.go +++ b/cmd/cosign/cli/verify/verify_blob_test.go @@ -38,7 +38,6 @@ import ( "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" "github.com/go-openapi/runtime" "github.com/go-openapi/swag" - ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/internal/pkg/cosign/tsa/mock" "github.com/sigstore/cosign/v2/pkg/cosign" @@ -124,52 +123,6 @@ func TestSignaturesBundle(t *testing.T) { } } -func TestIsIntotoDSSEWithEnvelopes(t *testing.T) { - tts := []struct { - envelope ssldsse.Envelope - isIntotoDSSE bool - }{ - { - envelope: ssldsse.Envelope{ - PayloadType: "application/vnd.in-toto+json", - Payload: base64.StdEncoding.EncodeToString([]byte("This is a test")), - Signatures: []ssldsse.Signature{}, - }, - isIntotoDSSE: true, - }, - } - for _, tt := range tts { - envlopeBytes, _ := json.Marshal(tt.envelope) - got := isIntotoDSSE(envlopeBytes) - if got != tt.isIntotoDSSE { - t.Fatalf("unexpected envelope content") - } - } -} - -func TestIsIntotoDSSEWithBytes(t *testing.T) { - tts := []struct { - envelope []byte - isIntotoDSSE bool - }{ - { - envelope: []byte("This is no valid"), - isIntotoDSSE: false, - }, - { - envelope: []byte("MEUCIQDBmE1ZRFjUVic1hzukesJlmMFG1JqWWhcthnhawTeBNQIga3J9/WKsNlSZaySnl8V360bc2S8dIln2/qo186EfjHA="), - isIntotoDSSE: false, - }, - } - for _, tt := range tts { - envlopeBytes, _ := json.Marshal(tt.envelope) - got := isIntotoDSSE(envlopeBytes) - if got != tt.isIntotoDSSE { - t.Fatalf("unexpected envelope content") - } - } -} - // Does not test identity options, only blob verification with different // options. func TestVerifyBlob(t *testing.T) { @@ -808,6 +761,7 @@ func makeLocalBundleWithoutRekorBundle(t *testing.T, sig []byte, svBytes []byte) func TestVerifyBlobCmdWithBundle(t *testing.T) { keyless := newKeylessStack(t) + defer os.RemoveAll(keyless.td) t.Run("Normal verification", func(t *testing.T) { identity := "hello@foo.com" @@ -932,16 +886,16 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { blobPath := writeBlobFile(t, keyless.td, string(signedPayload), "attestation.txt") // Verify command - cmd := VerifyBlobCmd{ + cmd := VerifyBlobAttestationCommand{ CertVerifyOptions: options.CertVerifyOptions{ CertIdentity: identity, CertOidcIssuer: issuer, }, - CertRef: "", // Cert is fetched from bundle - CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE - SigRef: "", // Sig is fetched from bundle - KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - IgnoreSCT: true, + CertRef: "", // Cert is fetched from bundle + CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE + SignaturePath: "", // Sig is fetched from bundle + KeyOpts: options.KeyOpts{BundlePath: bundlePath}, + IgnoreSCT: true, } if err := cmd.Exec(context.Background(), blobPath); err != nil { t.Fatal(err) @@ -1242,12 +1196,53 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { t.Fatalf("expected error with mismatched root, got %v", err) } }) + t.Run("Attestation with keyless", func(t *testing.T) { + identity := "hello@foo.com" + issuer := "issuer" + leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) + + stmt := `{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"customFoo","subject":[{"name":"subject","digest":{"sha256":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}}],"predicate":{}}` + wrapped := dsse.WrapSigner(signer, ctypes.IntotoPayloadType) + signedPayload, err := wrapped.SignMessage(bytes.NewReader([]byte(stmt)), signatureoptions.WithContext(context.Background())) + if err != nil { + t.Fatal(err) + } + // intoto sig = json-serialized dsse envelope + sig := signedPayload + + // Create bundle + entry := genRekorEntry(t, intoto.KIND, "0.0.1", signedPayload, leafPemCert, sig) + b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) + b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) + bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") + blobPath := writeBlobFile(t, keyless.td, string(signedPayload), "attestation.txt") + + // Verify command with bundle + cmd := VerifyBlobAttestationCommand{ + CertVerifyOptions: options.CertVerifyOptions{ + CertOidcIssuer: issuer, + CertIdentity: identity, + }, + CertRef: "", // Cert is fetched from bundle + CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE + SignaturePath: "", // Sig is fetched from bundle + KeyOpts: options.KeyOpts{BundlePath: bundlePath}, + IgnoreSCT: true, + CheckClaims: false, // Intentionally false. This checks the subject claim. This is tested in verify_blob_attestation_test.go + } + if err := cmd.Exec(context.Background(), blobPath); err != nil { + t.Fatal(err) + } + }) } func TestVerifyBlobCmdInvalidRootCA(t *testing.T) { keyless := newKeylessStack(t) + defer os.RemoveAll(keyless.td) + // Change the keyless stack. newKeyless := newKeylessStack(t) + defer os.RemoveAll(newKeyless.td) t.Run("Invalid certificate root when specifying cert via certRef", func(t *testing.T) { identity := "hello@foo.com" issuer := "issuer" diff --git a/doc/cosign_verify-blob-attestation.md b/doc/cosign_verify-blob-attestation.md index da29a74e123..fe567e89fe3 100644 --- a/doc/cosign_verify-blob-attestation.md +++ b/doc/cosign_verify-blob-attestation.md @@ -28,10 +28,32 @@ cosign verify-blob-attestation [flags] ### Options ``` - -h, --help help for verify-blob-attestation - --key string path to the public key file, KMS URI or Kubernetes Secret - --signature string path to base64-encoded signature over attestation in DSSE format - --type string specify a predicate type (slsaprovenance|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom") + --bundle string path to bundle FILE + --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. + --certificate-chain string path to a list of CA certificates in PEM format which will be needed when building the certificate chain for the signing certificate. Must start with the parent intermediate CA certificate of the signing certificate and end with the root certificate + --certificate-github-workflow-name string contains the workflow claim from the GitHub OIDC Identity token that contains the name of the executed workflow. + --certificate-github-workflow-ref string contains the ref claim from the GitHub OIDC Identity token that contains the git ref that the workflow run was based upon. + --certificate-github-workflow-repository string contains the repository claim from the GitHub OIDC Identity token that contains the repository that the workflow run was based upon + --certificate-github-workflow-sha string contains the sha claim from the GitHub OIDC Identity token that contains the commit SHA that the workflow run was based upon. + --certificate-github-workflow-trigger string contains the event_name claim from the GitHub OIDC Identity token that contains the name of the event that triggered the workflow run + --certificate-identity string The identity expected in a valid Fulcio certificate. Valid values include email address, DNS names, IP addresses, and URIs. Either --certificate-identity or --certificate-identity-regexp must be set for keyless flows. + --certificate-identity-regexp string A regular expression alternative to --certificate-identity. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --certificate-identity or --certificate-identity-regexp must be set for keyless flows. + --certificate-oidc-issuer string The OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth. Either --certificate-oidc-issuer or --certificate-oidc-issuer-regexp must be set for keyless flows. + --certificate-oidc-issuer-regexp string A regular expression alternative to --certificate-oidc-issuer. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --certificate-oidc-issuer or --certificate-oidc-issuer-regexp must be set for keyless flows. + --check-claims whether to check the claims found (default true) + -h, --help help for verify-blob-attestation + --insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log + --insecure-skip-tlog-verify skip transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts cannot be publicly verified when not included in a log + --key string path to the public key file, KMS URI or Kubernetes Secret + --offline only allow offline verification + --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") + --rfc3161-timestamp string path to RFC3161 timestamp FILE + --sct string path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. If a certificate contains an SCT, verification will check both the detached and embedded SCTs. + --signature string path to base64-encoded signature over attestation in DSSE format + --sk whether to use a hardware security key + --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) + --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp + --type string specify a predicate type (slsaprovenance|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom") ``` ### Options inherited from parent commands diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md index ebe087a28bd..7c5162db52c 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -58,34 +58,30 @@ cosign verify-blob [flags] ### Options ``` - --allow-http-registry whether to allow using HTTP protocol while connecting to registries. Don't use this for anything but testing - --allow-insecure-registry whether to allow insecure connections to registries (e.g., with expired or self-signed TLS certificates). Don't use this for anything but testing - --attachment-tag-prefix [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] optional custom prefix to use for attached image tags. Attachment images are tagged as: [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] - --bundle string path to bundle FILE - --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --certificate-chain string path to a list of CA certificates in PEM format which will be needed when building the certificate chain for the signing certificate. Must start with the parent intermediate CA certificate of the signing certificate and end with the root certificate - --certificate-github-workflow-name string contains the workflow claim from the GitHub OIDC Identity token that contains the name of the executed workflow. - --certificate-github-workflow-ref string contains the ref claim from the GitHub OIDC Identity token that contains the git ref that the workflow run was based upon. - --certificate-github-workflow-repository string contains the repository claim from the GitHub OIDC Identity token that contains the repository that the workflow run was based upon - --certificate-github-workflow-sha string contains the sha claim from the GitHub OIDC Identity token that contains the commit SHA that the workflow run was based upon. - --certificate-github-workflow-trigger string contains the event_name claim from the GitHub OIDC Identity token that contains the name of the event that triggered the workflow run - --certificate-identity string The identity expected in a valid Fulcio certificate. Valid values include email address, DNS names, IP addresses, and URIs. Either --certificate-identity or --certificate-identity-regexp must be set for keyless flows. - --certificate-identity-regexp string A regular expression alternative to --certificate-identity. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --certificate-identity or --certificate-identity-regexp must be set for keyless flows. - --certificate-oidc-issuer string The OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth. Either --certificate-oidc-issuer or --certificate-oidc-issuer-regexp must be set for keyless flows. - --certificate-oidc-issuer-regexp string A regular expression alternative to --certificate-oidc-issuer. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --certificate-oidc-issuer or --certificate-oidc-issuer-regexp must be set for keyless flows. - -h, --help help for verify-blob - --insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log - --insecure-skip-tlog-verify skip transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts cannot be publicly verified when not included in a log - --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). - --key string path to the public key file, KMS URI or Kubernetes Secret - --offline only allow offline verification - --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") - --rfc3161-timestamp string path to RFC3161 timestamp FILE - --sct string path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. If a certificate contains an SCT, verification will check both the detached and embedded SCTs. - --signature string signature content or path or remote URL - --sk whether to use a hardware security key - --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) - --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp + --bundle string path to bundle FILE + --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. + --certificate-chain string path to a list of CA certificates in PEM format which will be needed when building the certificate chain for the signing certificate. Must start with the parent intermediate CA certificate of the signing certificate and end with the root certificate + --certificate-github-workflow-name string contains the workflow claim from the GitHub OIDC Identity token that contains the name of the executed workflow. + --certificate-github-workflow-ref string contains the ref claim from the GitHub OIDC Identity token that contains the git ref that the workflow run was based upon. + --certificate-github-workflow-repository string contains the repository claim from the GitHub OIDC Identity token that contains the repository that the workflow run was based upon + --certificate-github-workflow-sha string contains the sha claim from the GitHub OIDC Identity token that contains the commit SHA that the workflow run was based upon. + --certificate-github-workflow-trigger string contains the event_name claim from the GitHub OIDC Identity token that contains the name of the event that triggered the workflow run + --certificate-identity string The identity expected in a valid Fulcio certificate. Valid values include email address, DNS names, IP addresses, and URIs. Either --certificate-identity or --certificate-identity-regexp must be set for keyless flows. + --certificate-identity-regexp string A regular expression alternative to --certificate-identity. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --certificate-identity or --certificate-identity-regexp must be set for keyless flows. + --certificate-oidc-issuer string The OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth. Either --certificate-oidc-issuer or --certificate-oidc-issuer-regexp must be set for keyless flows. + --certificate-oidc-issuer-regexp string A regular expression alternative to --certificate-oidc-issuer. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --certificate-oidc-issuer or --certificate-oidc-issuer-regexp must be set for keyless flows. + -h, --help help for verify-blob + --insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log + --insecure-skip-tlog-verify skip transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts cannot be publicly verified when not included in a log + --key string path to the public key file, KMS URI or Kubernetes Secret + --offline only allow offline verification + --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") + --rfc3161-timestamp string path to RFC3161 timestamp FILE + --sct string path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. If a certificate contains an SCT, verification will check both the detached and embedded SCTs. + --signature string signature content or path or remote URL + --sk whether to use a hardware security key + --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) + --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp ``` ### Options inherited from parent commands diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index de6dd766c83..58b55b2a376 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -858,11 +858,9 @@ func VerifyLocalImageAttestations(ctx context.Context, path string, co *CheckOpt return verifyImageAttestations(ctx, atts, h, co) } -func VerifyBlobAttestation(ctx context.Context, att oci.Signature, co *CheckOpts) ( +func VerifyBlobAttestation(ctx context.Context, att oci.Signature, h v1.Hash, co *CheckOpts) ( bool, error) { - // A blob attestation does not have an associated artifact (currently) to check the claims against. - // So we can safely add a nil hash. - return verifyInternal(ctx, att, v1.Hash{}, verifyOCIAttestation, co) + return verifyInternal(ctx, att, h, verifyOCIAttestation, co) } func verifyImageAttestations(ctx context.Context, atts oci.Signatures, h v1.Hash, co *CheckOpts) (checkedAttestations []oci.Signature, bundleVerified bool, err error) { diff --git a/test/e2e_test.go b/test/e2e_test.go index f130144c7ad..fa914530081 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -1849,10 +1849,15 @@ func TestAttestBlobSignVerify(t *testing.T) { _, privKeyPath1, pubKeyPath1 := keypair(t, td1) ctx := context.Background() + ko := options.KeyOpts{ + KeyRef: pubKeyPath1, + } blobVerifyAttestationCmd := cliverify.VerifyBlobAttestationCommand{ - KeyRef: pubKeyPath1, - SignaturePath: outputSignature, - PredicateType: predicateType, + KeyOpts: ko, + SignaturePath: outputSignature, + PredicateType: predicateType, + SkipTlogVerify: true, + CheckClaims: true, } // Verify should fail on a bad input mustErr(blobVerifyAttestationCmd.Exec(ctx, bp), t)