diff --git a/cmd/cosign/cli/options/verify.go b/cmd/cosign/cli/options/verify.go index 869197b3332..cb9dec1e939 100644 --- a/cmd/cosign/cli/options/verify.go +++ b/cmd/cosign/cli/options/verify.go @@ -109,14 +109,15 @@ func (o *VerifyAttestationOptions) AddFlags(cmd *cobra.Command) { // VerifyBlobOptions is the top level wrapper for the `verify blob` command. type VerifyBlobOptions struct { - Key string - Signature string - BundlePath string + Key string + SignatureRef string + BundlePath string - SecurityKey SecurityKeyOptions - CertVerify CertVerifyOptions - Rekor RekorOptions - Registry RegistryOptions + SecurityKey SecurityKeyOptions + CertVerify CertVerifyOptions + Rekor RekorOptions + Registry RegistryOptions + SignatureDigest SignatureDigestOptions } var _ Interface = (*VerifyBlobOptions)(nil) @@ -127,11 +128,12 @@ func (o *VerifyBlobOptions) AddFlags(cmd *cobra.Command) { o.Rekor.AddFlags(cmd) o.CertVerify.AddFlags(cmd) o.Registry.AddFlags(cmd) + o.SignatureDigest.AddFlags(cmd) cmd.Flags().StringVar(&o.Key, "key", "", "path to the public key file, KMS URI or Kubernetes Secret") - cmd.Flags().StringVar(&o.Signature, "signature", "", + cmd.Flags().StringVar(&o.SignatureRef, "signature", "", "signature content or path or remote URL") cmd.Flags().StringVar(&o.BundlePath, "bundle", "", diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index 08a4fe9bfcb..ad79b87254f 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -16,8 +16,6 @@ package cli import ( - "fmt" - "github.com/spf13/cobra" "github.com/sigstore/cosign/cmd/cosign/cli/options" @@ -259,21 +257,31 @@ The blob may be specified as a path to a file or - for stdin.`, Args: cobra.ExactArgs(1), 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, + hashAlgorithm, err := o.SignatureDigest.HashAlgorithm() + if err != nil { + return err } - if err := verify.VerifyBlobCmd(cmd.Context(), ko, o.CertVerify.Cert, - o.CertVerify.CertEmail, o.CertVerify.CertOidcIssuer, o.CertVerify.CertChain, - o.Signature, args[0], o.CertVerify.CertGithubWorkflowTrigger, o.CertVerify.CertGithubWorkflowSha, - o.CertVerify.CertGithubWorkflowName, o.CertVerify.CertGithubWorkflowRepository, o.CertVerify.CertGithubWorkflowRef, - o.CertVerify.EnforceSCT); err != nil { - return fmt.Errorf("verifying blob %s: %w", args, err) + v := verify.VerifyBlobCommand{ + KeyRef: o.Key, + CertRef: o.CertVerify.Cert, + CertEmail: o.CertVerify.CertEmail, + CertOidcIssuer: o.CertVerify.CertOidcIssuer, + CertGithubWorkflowTrigger: o.CertVerify.CertGithubWorkflowTrigger, + CertGithubWorkflowSha: o.CertVerify.CertGithubWorkflowSha, + CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName, + CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, + CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, + CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, + Sk: o.SecurityKey.Use, + Slot: o.SecurityKey.Slot, + RekorURL: o.Rekor.URL, + HashAlgorithm: hashAlgorithm, + BundlePath: o.BundlePath, + SignatureRef: o.SignatureRef, } - return nil + + return v.Exec(cmd.Context(), args) }, } diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 4754072c325..a281d5a2ac8 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -16,42 +16,26 @@ package verify import ( - "bytes" "context" "crypto" _ "crypto/sha256" // for `crypto.SHA256` "crypto/x509" "encoding/base64" - "encoding/hex" - "encoding/json" - "errors" + "flag" "fmt" "io" "os" - "time" - "github.com/go-openapi/runtime" - 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" "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" sigs "github.com/sigstore/cosign/pkg/signature" - "github.com/sigstore/sigstore/pkg/tuf" - - ctypes "github.com/sigstore/cosign/pkg/types" - "github.com/sigstore/rekor/pkg/generated/client" - "github.com/sigstore/rekor/pkg/generated/models" - "github.com/sigstore/rekor/pkg/types" - hashedrekord "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1" - rekord "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1" - "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" - "github.com/sigstore/sigstore/pkg/signature/dsse" - signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) func isb64(data []byte) bool { @@ -59,66 +43,131 @@ func isb64(data []byte) bool { return err == nil } +// VerifyBlobCommand verifies a signature on a supplied blob data // nolint -func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail, - certOidcIssuer, certChain, sigRef, blobRef, certGithubWorkflowTrigger, certGithubWorkflowSha, - certGithubWorkflowName, - certGithubWorkflowRepository, - certGithubWorkflowRef string, enforceSCT bool) error { - var verifier signature.Verifier - var cert *x509.Certificate +type VerifyBlobCommand struct { + KeyRef string + CertRef string + CertEmail string + CertOidcIssuer string + CertGithubWorkflowTrigger string + CertGithubWorkflowSha string + CertGithubWorkflowName string + CertGithubWorkflowRepository string + CertGithubWorkflowRef string + CertChain string + EnforceSCT bool + Sk bool + Slot string + RekorURL string + BundlePath string + SignatureRef string + HashAlgorithm crypto.Hash +} - if !options.OneOf(ko.KeyRef, ko.Sk, certRef) && !options.EnableExperimental() && ko.BundlePath == "" { - return &options.PubKeyParseError{} +// nolint +func (c *VerifyBlobCommand) Exec(ctx context.Context, blobRefs []string) error { + if len(blobRefs) == 0 { + return flag.ErrHelp } - sig, b64sig, err := signatures(sigRef, ko.BundlePath) - if err != nil { - return err + // always default to sha256 if the algorithm hasn't been explicitly set + if c.HashAlgorithm == 0 { + c.HashAlgorithm = crypto.SHA256 } - blobBytes, err := payloadBytes(blobRef) - if err != nil { - return err + if !options.OneOf(c.KeyRef, c.CertRef, c.Sk) && !options.EnableExperimental() { + return &options.PubKeyParseError{} } + co := &cosign.CheckOpts{ + CertEmail: c.CertEmail, + CertOidcIssuer: c.CertOidcIssuer, + CertGithubWorkflowTrigger: c.CertGithubWorkflowTrigger, + CertGithubWorkflowSha: c.CertGithubWorkflowSha, + CertGithubWorkflowName: c.CertGithubWorkflowName, + CertGithubWorkflowRepository: c.CertGithubWorkflowRepository, + CertGithubWorkflowRef: c.CertGithubWorkflowRef, + EnforceSCT: c.EnforceSCT, + SignatureRef: c.SignatureRef, + } + var err error + if options.EnableExperimental() { + if c.RekorURL != "" { + rekorClient, err := rekor.NewClient(c.RekorURL) + if err != nil { + return fmt.Errorf("creating Rekor client: %w", err) + } + co.RekorClient = rekorClient + } + 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) + } + } + keyRef := c.KeyRef + certRef := c.CertRef + bundlePath := c.BundlePath + // Keys are optional! + var pubKey signature.Verifier + var cert *x509.Certificate + var chain []*x509.Certificate + var bundle *bundle.RekorBundle switch { - case ko.KeyRef != "": - verifier, err = sigs.PublicKeyFromKeyRef(ctx, ko.KeyRef) + case keyRef != "": + pubKey, err = sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, c.HashAlgorithm) if err != nil { return fmt.Errorf("loading public key: %w", err) } - pkcs11Key, ok := verifier.(*pkcs11key.Key) + pkcs11Key, ok := pubKey.(*pkcs11key.Key) if ok { defer pkcs11Key.Close() } - case ko.Sk: - sk, err := pivkey.GetKeyWithSlot(ko.Slot) + case c.Sk: + sk, err := pivkey.GetKeyWithSlot(c.Slot) if err != nil { return fmt.Errorf("opening piv token: %w", err) } defer sk.Close() - verifier, err = sk.Verifier() + pubKey, err = sk.Verifier() if err != nil { - return fmt.Errorf("loading public key from token: %w", err) + return fmt.Errorf("initializing piv token verifier: %w", err) + } + case bundlePath != "": + b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath) + if err != nil { + return err + } + if b.Cert == "" { + return fmt.Errorf("bundle does not contain cert for verification, please provide public key") + } + bundle = b.Bundle + // cert can either be a cert 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 + pubKey, err = sigs.LoadPublicKeyRaw(certBytes, crypto.SHA256) + } else { + pubKey, err = signature.LoadVerifier(cert.PublicKey, crypto.SHA256) } - case certRef != "": - cert, err = loadCertFromFileOrURL(certRef) if err != nil { return err } - co := &cosign.CheckOpts{ - CertEmail: certEmail, - CertOidcIssuer: certOidcIssuer, - CertGithubWorkflowTrigger: certGithubWorkflowTrigger, - CertGithubWorkflowSha: certGithubWorkflowSha, - CertGithubWorkflowName: certGithubWorkflowName, - CertGithubWorkflowRepository: certGithubWorkflowRepository, - CertGithubWorkflowRef: certGithubWorkflowRef, - EnforceSCT: enforceSCT, + case certRef != "": + cert, err = loadCertFromFileOrURL(c.CertRef) + if err != nil { + return err } - if certChain == "" { + if c.CertChain == "" { // If no certChain is passed, the Fulcio root certificate will be used co.RootCerts, err = fulcio.GetRoots() if err != nil { @@ -128,138 +177,45 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail, if err != nil { return fmt.Errorf("getting Fulcio intermediates: %w", err) } - verifier, err = cosign.ValidateAndUnpackCert(cert, co) + pubKey, err = cosign.ValidateAndUnpackCert(cert, co) if err != nil { return err } } else { // Verify certificate with chain - chain, err := loadCertChainFromFileOrURL(certChain) + chain, err = loadCertChainFromFileOrURL(c.CertChain) if err != nil { return err } - verifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) + pubKey, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) if err != nil { return err } } - case ko.BundlePath != "": - b, err := cosign.FetchLocalSignedPayloadFromPath(ko.BundlePath) - if err != nil { - return err - } - if b.Cert == "" { - return fmt.Errorf("bundle does not contain cert for verification, please provide public key") - } - // cert can either be a cert 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 - verifier, err = sigs.LoadPublicKeyRaw(certBytes, crypto.SHA256) - } else { - verifier, err = signature.LoadVerifier(cert.PublicKey, crypto.SHA256) - } - if err != nil { - return err - } - case options.EnableExperimental(): - rClient, err := rekor.NewClient(ko.RekorURL) - if err != nil { - return err - } - - uuids, err := cosign.FindTLogEntriesByPayload(ctx, rClient, blobBytes) - if err != nil { - return err - } - - if len(uuids) == 0 { - return errors.New("could not find a tlog entry for provided blob") - } - return verifySigByUUID(ctx, ko, rClient, certEmail, certOidcIssuer, sig, b64sig, uuids, blobBytes, enforceSCT) } + co.SigVerifier = pubKey - // Use the DSSE verifier if the payload is a DSSE with the In-Toto format. - if isIntotoDSSE(blobBytes) { - verifier = dsse.WrapVerifier(verifier) - } + fulcioVerified := (co.SigVerifier == nil) - // verify the signature - if err := verifier.VerifySignature(bytes.NewReader([]byte(sig)), bytes.NewReader(blobBytes)); err != nil { + _, b64sig, err := signatures(co.SignatureRef, bundlePath) + if err != nil { return err } - // verify the rekor entry - if err := verifyRekorEntry(ctx, ko, nil, verifier, cert, b64sig, blobBytes); err != nil { + blobRef := blobRefs[0] + blobBytes, err := payloadBytes(blobRef) + if err != nil { return err } - fmt.Fprintln(os.Stderr, "Verified OK") - return nil -} - -func verifySigByUUID(ctx context.Context, ko options.KeyOpts, rClient *client.Rekor, certEmail, certOidcIssuer, sig, b64sig string, - uuids []string, blobBytes []byte, enforceSCT bool) error { - var validSigExists bool - for _, u := range uuids { - tlogEntry, err := cosign.GetTlogEntry(ctx, rClient, u) - if err != nil { - continue - } - - certs, err := extractCerts(tlogEntry) - if err != nil { - continue - } - - co := &cosign.CheckOpts{ - CertEmail: certEmail, - CertOidcIssuer: certOidcIssuer, - EnforceSCT: enforceSCT, - } - - 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) - } - - cert := certs[0] - verifier, err := cosign.ValidateAndUnpackCert(cert, co) - if err != nil { - continue - } - // Use the DSSE verifier if the payload is a DSSE with the In-Toto format. - if isIntotoDSSE(blobBytes) { - verifier = dsse.WrapVerifier(verifier) - } - // verify the signature - if err := verifier.VerifySignature(bytes.NewReader([]byte(sig)), bytes.NewReader(blobBytes)); err != nil { - continue - } - - // verify the rekor entry - if err := verifyRekorEntry(ctx, ko, tlogEntry, verifier, cert, b64sig, blobBytes); err != nil { - continue - } - validSigExists = true + verified, bundleVerified, err := cosign.VerifyBlobSignature(ctx, blobBytes, b64sig, bundle, co) + if err != nil { + return err } - if !validSigExists { - fmt.Fprintln(os.Stderr, `WARNING: No valid entries were found in rekor to verify this blob. -Transparency log support for blobs is experimental, and occasionally an entry isn't found even if one exists. - -We recommend requesting the certificate/signature from the original signer of this blob and manually verifying with cosign verify-blob --cert [cert] --signature [signature].`) - return fmt.Errorf("could not find a valid tlog entry for provided blob, found %d invalid entries", len(uuids)) - } - fmt.Fprintln(os.Stderr, "Verified OK") + output := "text" + PrintVerificationHeader(blobRef, co, bundleVerified, fulcioVerified) + PrintVerification(blobRef, verified, output) return nil } @@ -312,154 +268,3 @@ func payloadBytes(blobRef string) ([]byte, error) { } return blobBytes, nil } - -func verifyRekorEntry(ctx context.Context, ko options.KeyOpts, e *models.LogEntryAnon, pubKey signature.Verifier, cert *x509.Certificate, b64sig string, blobBytes []byte) error { - // TODO: This can be moved below offline bundle verification when SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY - // is removed. - rekorClient, err := rekor.NewClient(ko.RekorURL) - if err != nil { - return err - } - - // If we have a bundle with a rekor entry, let's first try to verify offline - if ko.BundlePath != "" { - if err := verifyRekorBundle(ctx, ko.BundlePath, cert, rekorClient); err == nil { - fmt.Fprintf(os.Stderr, "tlog entry verified offline\n") - return nil - } - } - if !options.EnableExperimental() { - return nil - } - - // Only fetch from rekor tlog if we don't already have the entry. - if e == nil { - var pubBytes []byte - if pubKey != nil { - pubBytes, err = sigs.PublicKeyPem(pubKey, signatureoptions.WithContext(ctx)) - if err != nil { - return err - } - } - if cert != nil { - pubBytes, err = cryptoutils.MarshalCertificateToPEM(cert) - if err != nil { - return err - } - } - e, err = cosign.FindTlogEntry(ctx, rekorClient, b64sig, blobBytes, pubBytes) - if err != nil { - return err - } - } - - if err := cosign.VerifyTLogEntry(ctx, rekorClient, e); err != nil { - return nil - } - - uuid, err := cosign.ComputeLeafHash(e) - if err != nil { - return err - } - - fmt.Fprintf(os.Stderr, "tlog entry verified with uuid: %s index: %d\n", hex.EncodeToString(uuid), *e.LogIndex) - if cert == nil { - return nil - } - // if we have a cert, we should check expiry - return cosign.CheckExpiry(cert, time.Unix(*e.IntegratedTime, 0)) -} - -func verifyRekorBundle(ctx context.Context, bundlePath string, cert *x509.Certificate, rekorClient *client.Rekor) error { - b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath) - if err != nil { - return err - } - if b.Bundle == nil { - return fmt.Errorf("rekor entry is not available") - } - publicKeys, err := cosign.GetRekorPubs(ctx, rekorClient) - if err != nil { - return fmt.Errorf("retrieving rekor public key: %w", err) - } - - pubKey, ok := publicKeys[b.Bundle.Payload.LogID] - if !ok { - return errors.New("rekor log public key not found for payload") - } - err = cosign.VerifySET(b.Bundle.Payload, b.Bundle.SignedEntryTimestamp, pubKey.PubKey) - if err != nil { - return err - } - if pubKey.Status != tuf.Active { - fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") - } - - if cert == nil { - return nil - } - it := time.Unix(b.Bundle.Payload.IntegratedTime, 0) - return cosign.CheckExpiry(cert, it) -} - -func extractCerts(e *models.LogEntryAnon) ([]*x509.Certificate, error) { - b, err := base64.StdEncoding.DecodeString(e.Body.(string)) - if err != nil { - return nil, err - } - - pe, err := models.UnmarshalProposedEntry(bytes.NewReader(b), runtime.JSONConsumer()) - if err != nil { - return nil, err - } - - eimpl, err := types.NewEntry(pe) - if err != nil { - return nil, err - } - - var publicKeyB64 []byte - switch e := eimpl.(type) { - case *rekord.V001Entry: - publicKeyB64, err = e.RekordObj.Signature.PublicKey.Content.MarshalText() - if err != nil { - return nil, err - } - case *hashedrekord.V001Entry: - publicKeyB64, err = e.HashedRekordObj.Signature.PublicKey.Content.MarshalText() - if err != nil { - return nil, err - } - default: - return nil, errors.New("unexpected tlog entry type") - } - - publicKey, err := base64.StdEncoding.DecodeString(string(publicKeyB64)) - if err != nil { - return nil, err - } - - certs, err := cryptoutils.UnmarshalCertificatesFromPEM(publicKey) - if err != nil { - return nil, err - } - - if len(certs) == 0 { - return nil, errors.New("no certs found in pem tlog") - } - - return certs, err -} - -// 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_test.go b/cmd/cosign/cli/verify/verify_blob_test.go index b6aa05f3e03..0faf5fe1c72 100644 --- a/cmd/cosign/cli/verify/verify_blob_test.go +++ b/cmd/cosign/cli/verify/verify_blob_test.go @@ -15,13 +15,11 @@ package verify import ( - "encoding/base64" "encoding/json" "os" "path/filepath" "testing" - "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/sigstore/cosign/pkg/cosign" ) @@ -95,49 +93,3 @@ func TestSignaturesBundle(t *testing.T) { t.Fatalf("unexpected encoded signature, expected: %s got: %s", b64sig, gotb64Sig) } } - -func TestIsIntotoDSSEWithEnvelopes(t *testing.T) { - tts := []struct { - envelope dsse.Envelope - isIntotoDSSE bool - }{ - { - envelope: dsse.Envelope{ - PayloadType: "application/vnd.in-toto+json", - Payload: base64.StdEncoding.EncodeToString([]byte("This is a test")), - Signatures: []dsse.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") - } - } -} diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md index 2793bbb6a55..931f3d21b8c 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -79,6 +79,7 @@ cosign verify-blob [flags] --key string path to the public key file, KMS URI or Kubernetes Secret --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") --signature string signature content or path or remote URL + --signature-digest-algorithm string digest algorithm to use when processing a signature (sha224|sha256|sha384|sha512) (default "sha256") --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) ``` diff --git a/go.mod b/go.mod index 76f68445e1a..18949dc4df7 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/miekg/pkcs11 v1.1.1 github.com/mozillazg/docker-credential-acr-helper v0.3.0 github.com/open-policy-agent/opa v0.43.0 + github.com/pkg/errors v0.9.1 github.com/secure-systems-lab/go-securesystemslib v0.4.0 github.com/sigstore/fulcio v0.5.3 github.com/sigstore/rekor v0.11.0 @@ -219,7 +220,6 @@ require ( github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.13.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect diff --git a/pkg/cosign/verify_blob.go b/pkg/cosign/verify_blob.go new file mode 100644 index 00000000000..45b36212868 --- /dev/null +++ b/pkg/cosign/verify_blob.go @@ -0,0 +1,223 @@ +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cosign + +import ( + "bytes" + "context" + "crypto/x509" + "encoding/base64" + "encoding/json" + "fmt" + "os" + + "github.com/go-openapi/runtime" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/pkg/errors" + ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/cosign/pkg/cosign/bundle" + "github.com/sigstore/cosign/pkg/oci" + ctypes "github.com/sigstore/cosign/pkg/types" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/types" + hashedrekord "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1" + rekord "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/dsse" +) + +type blobSignature struct { + payload []byte + b64sig string + bundle *bundle.RekorBundle + v1.Layer +} + +func newBundleSignature(blobBytes []byte, b64sig string, bundle *bundle.RekorBundle) (*blobSignature, error) { + if blobBytes == nil { + return nil, errors.New("blobBytes must be non nil") + } + if b64sig == "" { + return nil, errors.New("b64sig must be non empty string") + } + return &blobSignature{ + payload: blobBytes, + b64sig: b64sig, + bundle: bundle, + }, nil +} + +func (s *blobSignature) Annotations() (map[string]string, error) { + return nil, nil +} + +func (s *blobSignature) Payload() ([]byte, error) { + return s.payload, nil +} + +func (s *blobSignature) Base64Signature() (string, error) { + return s.b64sig, nil +} + +func (s *blobSignature) Cert() (*x509.Certificate, error) { + return nil, errors.New("no cert in blobSignature") +} + +func (s *blobSignature) Chain() ([]*x509.Certificate, error) { + return nil, errors.New("no cert chain in blobSignature") +} + +func (s *blobSignature) Bundle() (*bundle.RekorBundle, error) { + return s.bundle, nil +} + +// VerifyBlobSignature verifies a signature +func VerifyBlobSignature(ctx context.Context, blobBytes []byte, b64sig string, bundle *bundle.RekorBundle, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { + sig, err := newBundleSignature(blobBytes, b64sig, bundle) + if err != nil { + return checkedSignatures, bundleVerified, errors.Wrap(err, "failed to create bundle signature") + } + + verifiers := []signature.Verifier{} + if co.SigVerifier != nil { + verifiers = append(verifiers, co.SigVerifier) + } else { + uuids, err := FindTLogEntriesByPayload(ctx, co.RekorClient, blobBytes) + if err != nil { + return checkedSignatures, bundleVerified, errors.Wrap(err, "failed to get tlog entries by payload") + } + if len(uuids) == 0 { + return checkedSignatures, bundleVerified, errors.New("could not find a tlog entry for provided blob") + } + for _, u := range uuids { + tlogEntry, err := GetTlogEntry(ctx, co.RekorClient, u) + if err != nil { + continue + } + certs, err := extractCerts(tlogEntry) + if err != nil { + continue + } + cert := certs[0] + verifier, err := ValidateAndUnpackCert(cert, co) + if err != nil { + continue + } + // Use the DSSE verifier if the payload is a DSSE with the In-Toto format. + if isIntotoDSSE(blobBytes) { + verifier = dsse.WrapVerifier(verifier) + } + verifiers = append(verifiers, verifier) + } + } + + var validSigExists bool + for _, verifier := range verifiers { + if err := verifyOCISignature(ctx, verifier, sig); err != nil { + continue + } + bundleVerified = true + validSigExists = true + break + } + if !validSigExists { + fmt.Fprintln(os.Stderr, `WARNING: No valid entries were found in rekor to verify this blob. +Transparency log support for blobs is experimental, and occasionally an entry isn't found even if one exists. +We recommend requesting the certificate/signature from the original signer of this blob and manually verifying with cosign verify-blob --cert [cert] --signature [signature].`) + return checkedSignatures, bundleVerified, fmt.Errorf("could not find a valid tlog entry for provided blob, found %d invalid entries", len(verifiers)) + } + + if validSigExists { + fmt.Fprintln(os.Stderr, "Verified OK") + checkedSignatures = append(checkedSignatures, sig) + } + + if !validSigExists && co.RekorClient != nil { + if co.SigVerifier != nil { + pub, err := co.SigVerifier.PublicKey(co.PKOpts...) + if err != nil { + return checkedSignatures, bundleVerified, errors.Wrap(err, "failed to get pubkey") + } + return checkedSignatures, bundleVerified, tlogValidatePublicKey(ctx, co.RekorClient, pub, sig) + } + + return checkedSignatures, bundleVerified, tlogValidateCertificate(ctx, co.RekorClient, sig) + } + + return checkedSignatures, bundleVerified, nil +} + +func extractCerts(e *models.LogEntryAnon) ([]*x509.Certificate, error) { + b, err := base64.StdEncoding.DecodeString(e.Body.(string)) + if err != nil { + return nil, err + } + + pe, err := models.UnmarshalProposedEntry(bytes.NewReader(b), runtime.JSONConsumer()) + if err != nil { + return nil, err + } + + eimpl, err := types.NewEntry(pe) + if err != nil { + return nil, err + } + + var publicKeyB64 []byte + switch e := eimpl.(type) { + case *rekord.V001Entry: + publicKeyB64, err = e.RekordObj.Signature.PublicKey.Content.MarshalText() + if err != nil { + return nil, err + } + case *hashedrekord.V001Entry: + publicKeyB64, err = e.HashedRekordObj.Signature.PublicKey.Content.MarshalText() + if err != nil { + return nil, err + } + default: + return nil, errors.New("unexpected tlog entry type") + } + + publicKey, err := base64.StdEncoding.DecodeString(string(publicKeyB64)) + if err != nil { + return nil, err + } + + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(publicKey) + if err != nil { + return nil, err + } + + if len(certs) == 0 { + return nil, errors.New("no certs found in pem tlog") + } + + return certs, err +} + +// 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/pkg/cosign/verify_blob_test.go b/pkg/cosign/verify_blob_test.go new file mode 100644 index 00000000000..9a1d1d667c2 --- /dev/null +++ b/pkg/cosign/verify_blob_test.go @@ -0,0 +1,69 @@ +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cosign + +import ( + "encoding/base64" + "encoding/json" + "testing" + + "github.com/secure-systems-lab/go-securesystemslib/dsse" +) + +func TestIsIntotoDSSEWithEnvelopes(t *testing.T) { + tts := []struct { + envelope dsse.Envelope + isIntotoDSSE bool + }{ + { + envelope: dsse.Envelope{ + PayloadType: "application/vnd.in-toto+json", + Payload: base64.StdEncoding.EncodeToString([]byte("This is a test")), + Signatures: []dsse.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") + } + } +} diff --git a/test/e2e_test.go b/test/e2e_test.go index a56948cb341..be1bbd8d2cd 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -109,6 +109,20 @@ var verifyLocal = func(keyRef, path string, checkClaims bool, annotations map[st return cmd.Exec(context.Background(), args) } +var verifyBlob = func(keyRef, sigRef, blobRef, bundlePath string) error { + cmd := cliverify.VerifyBlobCommand{ + KeyRef: keyRef, + SignatureRef: sigRef, + BundlePath: bundlePath, + RekorURL: rekorURL, + HashAlgorithm: crypto.SHA256, + } + + args := []string{blobRef} + + return cmd.Exec(context.Background(), args) +} + var ro = &options.RootOptions{Timeout: options.DefaultTimeout} func TestSignVerify(t *testing.T) { @@ -618,17 +632,9 @@ func TestSignBlob(t *testing.T) { _, privKeyPath1, pubKeyPath1 := keypair(t, td1) _, _, pubKeyPath2 := keypair(t, td2) - ctx := context.Background() - - ko1 := options.KeyOpts{ - KeyRef: pubKeyPath1, - } - ko2 := options.KeyOpts{ - KeyRef: pubKeyPath2, - } // Verify should fail on a bad input - mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob, "" /*certGithubWorkflowTrigger*/, "" /*certGithubWorkflowName*/, "", "" /*certGithubWorkflowRepository*/, "" /*certGithubWorkflowRef*/, false), t) - mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob, "" /*certGithubWorkflowTrigger*/, "" /*certGithubWorkflowName*/, "", "" /*certGithubWorkflowRepository*/, "" /*certGithubWorkflowRef*/, false), t) + mustErr(verifyBlob(pubKeyPath1, "badsig", blob, ""), t) + mustErr(verifyBlob(pubKeyPath2, "badsig", blob, ""), t) // Now sign the blob with one key ko := options.KeyOpts{ @@ -640,8 +646,8 @@ func TestSignBlob(t *testing.T) { t.Fatal(err) } // Now verify should work with that one, but not the other - must(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp, "", "", "", "", "", false), t) - mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp, "", "", "", "", "", false), t) + must(verifyBlob(pubKeyPath1, string(sig), bp, ""), t) + mustErr(verifyBlob(pubKeyPath2, string(sig), bp, ""), t) } func TestSignBlobBundle(t *testing.T) { @@ -659,14 +665,8 @@ func TestSignBlobBundle(t *testing.T) { _, privKeyPath1, pubKeyPath1 := keypair(t, td1) - ctx := context.Background() - - ko1 := options.KeyOpts{ - KeyRef: pubKeyPath1, - BundlePath: bundlePath, - } // Verify should fail on a bad input - mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", blob, "", "", "", "", "", false), t) + mustErr(verifyBlob(pubKeyPath1, "", blob, bundlePath), t) // Now sign the blob with one key ko := options.KeyOpts{ @@ -679,7 +679,7 @@ func TestSignBlobBundle(t *testing.T) { t.Fatal(err) } // Now verify should work - must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp, "", "", "", "", "", false), t) + must(verifyBlob(pubKeyPath1, "", bp, bundlePath), t) // Now we turn on the tlog and sign again defer setenv(t, options.ExperimentalEnv, "1")() @@ -689,7 +689,7 @@ func TestSignBlobBundle(t *testing.T) { // Point to a fake rekor server to make sure offline verification of the tlog entry works os.Setenv(serverEnv, "notreal") - must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp, "", "", "", "", "", false), t) + must(verifyBlob(pubKeyPath1, "", bp, bundlePath), t) } func TestGenerate(t *testing.T) {