diff --git a/cmd/cosign/cli/options/verify.go b/cmd/cosign/cli/options/verify.go index 5e4ebb11a71..c42c6ecde57 100644 --- a/cmd/cosign/cli/options/verify.go +++ b/cmd/cosign/cli/options/verify.go @@ -139,7 +139,6 @@ type VerifyBlobOptions struct { SecurityKey SecurityKeyOptions CertVerify CertVerifyOptions Rekor RekorOptions - Registry RegistryOptions CommonVerifyOptions CommonVerifyOptions RFC3161TimestampPath string @@ -152,7 +151,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", "", @@ -188,7 +186,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) @@ -196,10 +204,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 235cf5d9478..c2cc6602dcb 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -335,10 +335,34 @@ 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, + CertRef: o.CertVerify.Cert, + CertEmail: o.CertVerify.CertEmail, + CertIdentity: o.CertVerify.CertIdentity, + CertOIDCIssuer: o.CertVerify.CertOidcIssuer, + 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 a4fb641a3cb..20ad56aede9 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/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/cmd/cosign/cli/rekor" @@ -39,7 +38,6 @@ import ( "github.com/sigstore/cosign/pkg/oci/static" sigs "github.com/sigstore/cosign/pkg/signature" - ctypes "github.com/sigstore/cosign/pkg/types" "github.com/sigstore/sigstore/pkg/cryptoutils" ) @@ -269,31 +267,12 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } opts = append(opts, static.WithCertChain(certPEM, chainPEM)) } - - // 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") @@ -343,16 +322,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 f0f35ca348f..6309646b603 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -16,154 +16,310 @@ 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/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/cmd/cosign/cli/options" + "github.com/sigstore/cosign/cmd/cosign/cli/rekor" + internal "github.com/sigstore/cosign/internal/pkg/cosign" + "github.com/sigstore/cosign/pkg/blob" "github.com/sigstore/cosign/pkg/cosign" + "github.com/sigstore/cosign/pkg/cosign/bundle" + "github.com/sigstore/cosign/pkg/cosign/pivkey" "github.com/sigstore/cosign/pkg/cosign/pkcs11key" + "github.com/sigstore/cosign/pkg/oci/static" + "github.com/sigstore/cosign/pkg/policy" sigs "github.com/sigstore/cosign/pkg/signature" - "github.com/sigstore/cosign/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 + CertRef string + CertEmail string + CertIdentity string + CertOIDCIssuer 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") + 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 err error + co := &cosign.CheckOpts{ + CertEmail: c.CertEmail, + CertIdentity: c.CertIdentity, + CertOidcIssuer: c.CertOIDCIssuer, + 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) + return err } - pkcs11Key, ok := co.SigVerifier.(*pkcs11key.Key) - if ok { - defer pkcs11Key.Close() + payload = internal.NewHashReader(f, sha256.New()) + if _, err := io.ReadAll(&payload); err != nil { + return err } - - // 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) + 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 env.PayloadType != types.IntotoPayloadType { - return cosign.NewVerificationError("invalid payloadType %s on envelope. Expected %s", env.PayloadType, types.IntotoPayloadType) + if c.KeyOpts.TSACertChainPath != "" { + _, err := os.Stat(c.KeyOpts.TSACertChainPath) + if err != nil { + return fmt.Errorf("unable to open timestamp certificate chain file '%s: %w", c.KeyOpts.TSACertChainPath, err) + } + // TODO: Add support for TUF certificates. + pemBytes, err := os.ReadFile(filepath.Clean(c.KeyOpts.TSACertChainPath)) + if err != nil { + return fmt.Errorf("error reading certification chain path file: %w", err) + } + // TODO: Update this logic once https://github.com/sigstore/timestamp-authority/issues/121 gets merged. + // This relies on untrusted leaf certificate. + tsaCertPool := x509.NewCertPool() + ok := tsaCertPool.AppendCertsFromPEM(pemBytes) + if !ok { + return fmt.Errorf("error parsing response into Timestamp while appending certs from PEM") + } + co.TSACerts = tsaCertPool } - 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) + } + } } - // 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..4ebf9ba523d 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/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 d5356672cff..40847cfc3a5 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/cmd/cosign/cli/options" "github.com/sigstore/cosign/internal/pkg/cosign/payload" "github.com/sigstore/cosign/internal/pkg/cosign/tsa" @@ -128,52 +127,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) { @@ -686,6 +639,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" @@ -786,39 +740,6 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { t.Fatal("expected error due to expired cert, received nil") } }) - t.Run("Attestation", 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 - cmd := VerifyBlobCmd{ - 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, - } - if err := cmd.Exec(context.Background(), blobPath); err != nil { - t.Fatal(err) - } - }) t.Run("Invalid blob signature", func(t *testing.T) { identity := "hello@foo.com" issuer := "issuer" @@ -1116,12 +1037,49 @@ 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{ + 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..839c03ea2f9 100644 --- a/doc/cosign_verify-blob-attestation.md +++ b/doc/cosign_verify-blob-attestation.md @@ -28,10 +28,31 @@ 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-email string the email expected in a valid Fulcio 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. + --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 + --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 tlog verification + --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-cert-chain string path to certificate chain PEM file for the Timestamp Authority + --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 29124ca48d4..bfe5792800f 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -58,33 +58,29 @@ 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-email string the email expected in a valid Fulcio 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. - --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 - -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 tlog verification - --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-cert-chain string path to certificate chain PEM file for the Timestamp Authority + --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-email string the email expected in a valid Fulcio 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. + --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 + -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 tlog verification + --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-cert-chain string path to certificate chain PEM file for the Timestamp Authority ``` ### Options inherited from parent commands diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 298528606ba..92b0ef85a06 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -893,11 +893,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 cc7c54ffb26..afcdc66afdb 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -1856,10 +1856,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)