From 566ff3b62e4a41898b3b82c43a6a49b32cd0bf64 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Wed, 12 Jan 2022 15:22:33 -0800 Subject: [PATCH 1/2] Add --bundle flag to sign-blob and verify-blob Signed-off-by: Priya Wadhwa --- cmd/cosign/cli/options/signblob.go | 4 ++ cmd/cosign/cli/options/verify.go | 10 ++- cmd/cosign/cli/sign/sign_blob.go | 18 ++++++ cmd/cosign/cli/signblob.go | 1 + cmd/cosign/cli/verify.go | 9 +-- cmd/cosign/cli/verify/verify.go | 5 +- cmd/cosign/cli/verify/verify_blob.go | 74 ++++++++++++++++++++++- cmd/cosign/cli/verify/verify_blob_test.go | 40 +++++++++++- doc/cosign_sign-blob.md | 1 + doc/cosign_verify-blob.md | 1 + pkg/cosign/fetch.go | 20 ++++++ pkg/signature/keys.go | 7 ++- test/e2e_test.go | 50 ++++++++++++++- test/e2e_test_secrets.sh | 3 + 14 files changed, 226 insertions(+), 17 deletions(-) diff --git a/cmd/cosign/cli/options/signblob.go b/cmd/cosign/cli/options/signblob.go index e3cdcbb4275..3f7cc2968d0 100644 --- a/cmd/cosign/cli/options/signblob.go +++ b/cmd/cosign/cli/options/signblob.go @@ -35,6 +35,7 @@ type SignBlobOptions struct { OIDC OIDCOptions Registry RegistryOptions Timeout time.Duration + BundlePath string } var _ Interface = (*SignBlobOptions)(nil) @@ -64,4 +65,7 @@ func (o *SignBlobOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().DurationVar(&o.Timeout, "timeout", time.Second*30, "HTTP Timeout defaults to 30 seconds") + + cmd.Flags().StringVar(&o.BundlePath, "bundle", "", + "write everything required to verify the blob to a FILE") } diff --git a/cmd/cosign/cli/options/verify.go b/cmd/cosign/cli/options/verify.go index 0d76aee336e..a9f1a4d8c03 100644 --- a/cmd/cosign/cli/options/verify.go +++ b/cmd/cosign/cli/options/verify.go @@ -116,9 +116,10 @@ func (o *VerifyAttestationOptions) AddFlags(cmd *cobra.Command) { // VerifyBlobOptions is the top level wrapper for the `verify blob` command. type VerifyBlobOptions struct { - Key string - Cert string - Signature string + Key string + Cert string + Signature string + BundlePath string SecurityKey SecurityKeyOptions Rekor RekorOptions @@ -141,6 +142,9 @@ func (o *VerifyBlobOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.Signature, "signature", "", "signature content or path or remote URL") + + cmd.Flags().StringVar(&o.BundlePath, "bundle", "", + "path to bundle FILE") } // VerifyBlobOptions is the top level wrapper for the `verify blob` command. diff --git a/cmd/cosign/cli/sign/sign_blob.go b/cmd/cosign/cli/sign/sign_blob.go index f6052a66ccc..e0653067f8e 100644 --- a/cmd/cosign/cli/sign/sign_blob.go +++ b/cmd/cosign/cli/sign/sign_blob.go @@ -19,6 +19,7 @@ import ( "bytes" "context" "encoding/base64" + "encoding/json" "fmt" "io" "os" @@ -26,6 +27,7 @@ import ( "time" "github.com/pkg/errors" + cbundle "github.com/sigstore/cosign/pkg/cosign/bundle" "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/cmd/cosign/cli/rekor" @@ -44,6 +46,7 @@ type KeyOpts struct { OIDCIssuer string OIDCClientID string OIDCClientSecret string + BundlePath string // Modeled after InsecureSkipVerify in tls.Config, this disables // verifying the SCT. @@ -82,6 +85,8 @@ func SignBlobCmd(ctx context.Context, ko KeyOpts, regOpts options.RegistryOption return nil, errors.Wrap(err, "signing blob") } + signedPayload := cosign.LocalSignedPayload{} + if options.EnableExperimental() { rekorBytes, err = sv.Bytes(ctx) if err != nil { @@ -96,6 +101,19 @@ func SignBlobCmd(ctx context.Context, ko KeyOpts, regOpts options.RegistryOption return nil, err } fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex) + signedPayload.Bundle = cbundle.EntryToBundle(entry) + } + + // if bundle is specified, just do that and ignore the rest + if ko.BundlePath != "" { + signedPayload.Base64Signature = base64.StdEncoding.EncodeToString(sig) + signedPayload.Cert = base64.StdEncoding.EncodeToString(rekorBytes) + + contents, err := json.Marshal(signedPayload) + if err != nil { + return nil, err + } + return []byte(signedPayload.Base64Signature), os.WriteFile(ko.BundlePath, contents, 0600) } if outputSignature != "" { diff --git a/cmd/cosign/cli/signblob.go b/cmd/cosign/cli/signblob.go index ba98587693b..23c6b4c04ce 100644 --- a/cmd/cosign/cli/signblob.go +++ b/cmd/cosign/cli/signblob.go @@ -76,6 +76,7 @@ func SignBlob() *cobra.Command { OIDCIssuer: o.OIDC.Issuer, OIDCClientID: o.OIDC.ClientID, OIDCClientSecret: o.OIDC.ClientSecret, + BundlePath: o.BundlePath, } for _, blob := range args { // TODO: remove when the output flag has been deprecated diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index cd0e2559567..a75d1e93a3d 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -231,10 +231,11 @@ 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 := sign.KeyOpts{ - KeyRef: o.Key, - Sk: o.SecurityKey.Use, - Slot: o.SecurityKey.Slot, - RekorURL: o.Rekor.URL, + KeyRef: o.Key, + Sk: o.SecurityKey.Use, + Slot: o.SecurityKey.Slot, + RekorURL: o.Rekor.URL, + BundlePath: o.BundlePath, } if err := verify.VerifyBlobCmd(cmd.Context(), ko, o.Cert, o.Signature, args[0]); err != nil { return errors.Wrapf(err, "verifying blob %s", args) diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 0e71a60bdcf..422273abf5a 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -265,9 +265,12 @@ func loadCertFromFileOrURL(path string) (*x509.Certificate, error) { if err != nil { return nil, err } + return loadCertFromPEM(pems) +} +func loadCertFromPEM(pems []byte) (*x509.Certificate, error) { var out []byte - out, err = base64.StdEncoding.DecodeString(string(pems)) + out, err := base64.StdEncoding.DecodeString(string(pems)) if err != nil { // not a base64 out = pems diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index cebc3624f84..38404b52499 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -58,11 +58,11 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, sigRef, blobRe var pubKey signature.Verifier var cert *x509.Certificate - if !options.OneOf(ko.KeyRef, ko.Sk, certRef) && !options.EnableExperimental() { + if !options.OneOf(ko.KeyRef, ko.Sk, certRef) && !options.EnableExperimental() && ko.BundlePath == "" { return &options.PubKeyParseError{} } - sig, b64sig, err := signatures(sigRef) + sig, b64sig, err := signatures(sigRef, ko.BundlePath) if err != nil { return err } @@ -102,6 +102,32 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, sigRef, blobRe 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 + pubKey, err = sigs.LoadPublicKeyRaw(certBytes, crypto.SHA256) + if err != nil { + return err + } + } else { + pubKey, err = signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256) + if err != nil { + return err + } + } case options.EnableExperimental(): rClient, err := rekor.NewClient(ko.RekorURL) if err != nil { @@ -153,7 +179,7 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, sigRef, blobRe } // signatures returns the raw signature and the base64 encoded signature -func signatures(sigRef string) (string, string, error) { +func signatures(sigRef string, bundlePath string) (string, string, error) { var targetSig []byte var err error switch { @@ -166,6 +192,12 @@ func signatures(sigRef string) (string, string, error) { } targetSig = []byte(sigRef) } + case bundlePath != "": + b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath) + if err != nil { + return "", "", err + } + targetSig = []byte(b.Base64Signature) default: return "", "", fmt.Errorf("missing flag '--signature'") } @@ -213,9 +245,17 @@ func verifyCert(cert *x509.Certificate) error { } func verifyRekorEntry(ctx context.Context, ko sign.KeyOpts, pubKey signature.Verifier, cert *x509.Certificate, b64sig string, blobBytes []byte) error { + // 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); err == nil { + fmt.Fprintf(os.Stderr, "tlog entry verified offline\n") + return nil + } + } if !options.EnableExperimental() { return nil } + rekorClient, err := rekor.NewClient(ko.RekorURL) if err != nil { return err @@ -250,6 +290,34 @@ func verifyRekorEntry(ctx context.Context, ko sign.KeyOpts, pubKey signature.Ver return cosign.CheckExpiry(cert, time.Unix(*e.IntegratedTime, 0)) } +func verifyRekorBundle(ctx context.Context, bundlePath string, cert *x509.Certificate) error { + b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath) + if err != nil { + return err + } + if b.Bundle == nil { + return fmt.Errorf("rekor entry is not available") + } + pub, err := cosign.GetRekorPub(ctx) + if err != nil { + return errors.Wrap(err, "retrieving rekor public key") + } + + rekorPubKey, err := cosign.PemToECDSAKey(pub) + if err != nil { + return errors.Wrap(err, "pem to ecdsa") + } + + if err := cosign.VerifySET(b.Bundle.Payload, b.Bundle.SignedEntryTimestamp, rekorPubKey); err != nil { + return err + } + 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 { diff --git a/cmd/cosign/cli/verify/verify_blob_test.go b/cmd/cosign/cli/verify/verify_blob_test.go index 0fceca06fdf..683a8e426b1 100644 --- a/cmd/cosign/cli/verify/verify_blob_test.go +++ b/cmd/cosign/cli/verify/verify_blob_test.go @@ -15,10 +15,15 @@ package verify import ( + "encoding/json" + "io/ioutil" + "path/filepath" "testing" + + "github.com/sigstore/cosign/pkg/cosign" ) -func TestSignatures(t *testing.T) { +func TestSignaturesRef(t *testing.T) { sig := "a==" b64sig := "YT09" tests := []struct { @@ -41,7 +46,7 @@ func TestSignatures(t *testing.T) { for _, test := range tests { t.Run(test.description, func(t *testing.T) { - gotSig, gotb64Sig, err := signatures(test.sigRef) + gotSig, gotb64Sig, err := signatures(test.sigRef, "") if test.shouldErr && err != nil { return } @@ -57,3 +62,34 @@ func TestSignatures(t *testing.T) { }) } } + +func TestSignaturesBundle(t *testing.T) { + td := t.TempDir() + fp := filepath.Join(td, "file") + + sig := "a==" + b64sig := "YT09" + + // save as a LocalSignedPayload to the file + lsp := cosign.LocalSignedPayload{ + Base64Signature: b64sig, + } + contents, err := json.Marshal(lsp) + if err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(fp, contents, 0644); err != nil { + t.Fatal(err) + } + + gotSig, gotb64Sig, err := signatures("", fp) + if err != nil { + t.Fatal(err) + } + if gotSig != sig { + t.Fatalf("unexpected signature, expected: %s got: %s", sig, gotSig) + } + if gotb64Sig != b64sig { + t.Fatalf("unexpected encoded signature, expected: %s got: %s", b64sig, gotb64Sig) + } +} diff --git a/doc/cosign_sign-blob.md b/doc/cosign_sign-blob.md index 34b12b7ac0b..538a6582994 100644 --- a/doc/cosign_sign-blob.md +++ b/doc/cosign_sign-blob.md @@ -36,6 +36,7 @@ cosign sign-blob [flags] --allow-insecure-registry whether to allow insecure connections to registries. 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] --b64 whether to base64 encode the output (default true) + --bundle string write everything required to verify the blob to a FILE --fulcio-url string [EXPERIMENTAL] address of sigstore PKI server (default "https://v1.fulcio.sigstore.dev") -h, --help help for sign-blob --identity-token string [EXPERIMENTAL] identity token to use for certificate from fulcio diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md index 5769df3c83c..5cce53af069 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -63,6 +63,7 @@ cosign verify-blob [flags] ``` --allow-insecure-registry whether to allow insecure connections to registries. 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 --cert string path to the public certificate -h, --help help for verify-blob --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). diff --git a/pkg/cosign/fetch.go b/pkg/cosign/fetch.go index c784b04c104..04bad1744e5 100644 --- a/pkg/cosign/fetch.go +++ b/pkg/cosign/fetch.go @@ -20,6 +20,7 @@ import ( "crypto/x509" "encoding/json" "fmt" + "io/ioutil" "runtime" "github.com/google/go-containerregistry/pkg/name" @@ -37,6 +38,12 @@ type SignedPayload struct { Bundle *bundle.RekorBundle } +type LocalSignedPayload struct { + Base64Signature string `json:"base64Signature"` + Cert string `json:"cert,omitempty"` + Bundle *bundle.RekorBundle `json:"rekorBundle,omitempty"` +} + type Signatures struct { KeyID string `json:"keyid"` Sig string `json:"sig"` @@ -147,3 +154,16 @@ func FetchAttestationsForReference(ctx context.Context, ref name.Reference, opts return attestations, nil } + +// FetchLocalSignedPayloadFromPath fetches a local signed payload from a path to a file +func FetchLocalSignedPayloadFromPath(path string) (*LocalSignedPayload, error) { + contents, err := ioutil.ReadFile(path) + if err != nil { + return nil, errors.Wrapf(err, "reading %s", path) + } + var b *LocalSignedPayload + if err := json.Unmarshal(contents, &b); err != nil { + return nil, err + } + return b, nil +} diff --git a/pkg/signature/keys.go b/pkg/signature/keys.go index 2d071686896..4db73c7e469 100644 --- a/pkg/signature/keys.go +++ b/pkg/signature/keys.go @@ -77,7 +77,8 @@ func loadKey(keyPath string, pf cosign.PassFunc) (signature.SignerVerifier, erro return cosign.LoadPrivateKey(kb, pass) } -func loadPublicKey(raw []byte, hashAlgorithm crypto.Hash) (signature.Verifier, error) { +// LoadPublicKeyRaw loads a verifier from a raw public key passed in +func LoadPublicKeyRaw(raw []byte, hashAlgorithm crypto.Hash) (signature.Verifier, error) { // PEM encoded file. ed, err := cosign.PemToECDSAKey(raw) if err != nil { @@ -164,7 +165,7 @@ func PublicKeyFromKeyRefWithHashAlgo(ctx context.Context, keyRef string, hashAlg } if len(s.Data) > 0 { - return loadPublicKey(s.Data["cosign.pub"], hashAlgorithm) + return LoadPublicKeyRaw(s.Data["cosign.pub"], hashAlgorithm) } } @@ -203,7 +204,7 @@ func PublicKeyFromKeyRefWithHashAlgo(ctx context.Context, keyRef string, hashAlg } if len(pubKey) > 0 { - return loadPublicKey([]byte(pubKey), hashAlgorithm) + return LoadPublicKeyRaw([]byte(pubKey), hashAlgorithm) } } diff --git a/test/e2e_test.go b/test/e2e_test.go index 7a403c35700..f06676576b2 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -256,7 +256,7 @@ func TestAttestVerify(t *testing.T) { mustErr(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar"}, ""), t) } -func TestBundle(t *testing.T) { +func TestRekorBundle(t *testing.T) { // turn on the tlog defer setenv(t, options.ExperimentalEnv, "1")() @@ -466,6 +466,54 @@ func TestSignBlob(t *testing.T) { mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "", string(sig), bp), t) } +func TestSignBlobBundle(t *testing.T) { + blob := "someblob" + td1 := t.TempDir() + t.Cleanup(func() { + os.RemoveAll(td1) + }) + bp := filepath.Join(td1, blob) + bundlePath := filepath.Join(td1, "bundle.sig") + + if err := os.WriteFile(bp, []byte(blob), 0644); err != nil { + t.Fatal(err) + } + + _, privKeyPath1, pubKeyPath1 := keypair(t, td1) + + ctx := context.Background() + + ko1 := sign.KeyOpts{ + KeyRef: pubKeyPath1, + BundlePath: bundlePath, + } + // Verify should fail on a bad input + mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "", "", blob), t) + + // Now sign the blob with one key + ko := sign.KeyOpts{ + KeyRef: privKeyPath1, + PassFunc: passFunc, + BundlePath: bundlePath, + RekorURL: rekorURL, + } + if _, err := sign.SignBlobCmd(ctx, ko, options.RegistryOptions{}, bp, true, "", "", time.Duration(30*time.Second)); err != nil { + t.Fatal(err) + } + // Now verify should work + must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", bp), t) + + // Now we turn on the tlog and sign again + defer setenv(t, options.ExperimentalEnv, "1")() + if _, err := sign.SignBlobCmd(ctx, ko, options.RegistryOptions{}, bp, true, "", "", time.Duration(30*time.Second)); err != nil { + t.Fatal(err) + } + + // 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), t) +} + func TestGenerate(t *testing.T) { repo, stop := reg(t) defer stop() diff --git a/test/e2e_test_secrets.sh b/test/e2e_test_secrets.sh index 70531656500..fa0ca436027 100755 --- a/test/e2e_test_secrets.sh +++ b/test/e2e_test_secrets.sh @@ -115,6 +115,9 @@ if (./cosign verify-blob --key ${verification_key} --signature myblob.sig myblob if (./cosign verify-blob --key ${verification_key} --signature myblob2.sig myblob); then false; fi ./cosign verify-blob --key ${verification_key} --signature myblob2.sig myblob2 +./cosign sign-blob --key ${signing_key} --bundle bundle.sig myblob +./cosign verify-blob --key ${verification_key} --bundle bundle.sig myblob + ## sign and verify multiple blobs ./cosign sign-blob --key ${signing_key} myblob myblob2 > sigs head -n 1 sigs > car.sig From caed75787b7d041487d39fbd960e7fa288dee49d Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Thu, 13 Jan 2022 13:53:52 -0800 Subject: [PATCH 2/2] Add TUF timestamp when signing Signed-off-by: Priya Wadhwa --- cmd/cosign/cli/sign/sign_blob.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/cosign/cli/sign/sign_blob.go b/cmd/cosign/cli/sign/sign_blob.go index e0653067f8e..8014c771a51 100644 --- a/cmd/cosign/cli/sign/sign_blob.go +++ b/cmd/cosign/cli/sign/sign_blob.go @@ -28,6 +28,7 @@ import ( "github.com/pkg/errors" cbundle "github.com/sigstore/cosign/pkg/cosign/bundle" + "github.com/sigstore/cosign/pkg/cosign/tuf" "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/cmd/cosign/cli/rekor" @@ -102,6 +103,11 @@ func SignBlobCmd(ctx context.Context, ko KeyOpts, regOpts options.RegistryOption } fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex) signedPayload.Bundle = cbundle.EntryToBundle(entry) + ts, err := tuf.GetTimestamp(ctx) + if err != nil { + return nil, err + } + signedPayload.Timestamp = ts } // if bundle is specified, just do that and ignore the rest