From e9e9cb2c282767321505de9389dd38a1c97301d9 Mon Sep 17 00:00:00 2001 From: Hayden B Date: Wed, 7 Dec 2022 02:19:28 -0800 Subject: [PATCH] Updates to Timestamp signing and verification (#2499) * Updates to Timestamp signing and verification * Switch to using the raw signature rather than base64 signature for OCI and blob signing * For blob signing, write only the timestamp to disk, not the LocalSignedPayload (since that's already written with the bundle) * For blob verification, expect only a timestamp in the file. If you don't pass a bundle, you'll need to also pass the signature by flag * Some nits from the previous PR Signed-off-by: Hayden Blauzvern * Add testing in verify, update e2e test Signed-off-by: Hayden Blauzvern * Fix test Signed-off-by: Hayden Blauzvern * Add missing setting of SkipTlogVerify Signed-off-by: Hayden Blauzvern * Address comments Signed-off-by: Hayden Blauzvern * Fix docs Signed-off-by: Hayden Blauzvern * Address nits Signed-off-by: Hayden Blauzvern * Fix e2e test Signed-off-by: Hayden Blauzvern Signed-off-by: Hayden Blauzvern --- cmd/cosign/cli/options/signblob.go | 6 +- cmd/cosign/cli/options/verify.go | 4 +- cmd/cosign/cli/policy_init.go | 3 +- cmd/cosign/cli/sign/sign_blob.go | 37 +++---- cmd/cosign/cli/verify.go | 1 + cmd/cosign/cli/verify/verify_blob.go | 39 ++------ cmd/cosign/cli/verify/verify_blob_test.go | 102 ++++++------------- doc/cosign_sign-blob.md | 46 ++++----- doc/cosign_verify-blob.md | 2 +- internal/pkg/cosign/tsa/signer.go | 16 ++- pkg/cosign/bundle/tsa.go | 7 +- pkg/cosign/fetch.go | 7 +- pkg/cosign/verify.go | 41 ++++---- pkg/cosign/verify_test.go | 113 +++++++++++++++++++++- specs/SIGNATURE_SPEC.md | 5 + test/e2e_test.go | 6 +- 16 files changed, 255 insertions(+), 180 deletions(-) diff --git a/cmd/cosign/cli/options/signblob.go b/cmd/cosign/cli/options/signblob.go index 59f96607457..8cc23e23a35 100644 --- a/cmd/cosign/cli/options/signblob.go +++ b/cmd/cosign/cli/options/signblob.go @@ -79,7 +79,7 @@ func (o *SignBlobOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.TSAServerURL, "timestamp-server-url", "", "url to the Timestamp RFC3161 server, default none") - cmd.Flags().StringVar(&o.RFC3161TimestampPath, "rfc3161-timestamp-bundle", "", - "write everything required to verify the blob to a FILE") - _ = cmd.Flags().SetAnnotation("rfc3161-timestamp-bundle", cobra.BashCompFilenameExt, []string{}) + cmd.Flags().StringVar(&o.RFC3161TimestampPath, "rfc3161-timestamp", "", + "write the RFC3161 timestamp to a file") + _ = cmd.Flags().SetAnnotation("rfc3161-timestamp", cobra.BashCompFilenameExt, []string{}) } diff --git a/cmd/cosign/cli/options/verify.go b/cmd/cosign/cli/options/verify.go index 005bac9afb1..5e4ebb11a71 100644 --- a/cmd/cosign/cli/options/verify.go +++ b/cmd/cosign/cli/options/verify.go @@ -164,8 +164,8 @@ func (o *VerifyBlobOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.BundlePath, "bundle", "", "path to bundle FILE") - cmd.Flags().StringVar(&o.RFC3161TimestampPath, "rfc3161-timestamp-bundle", "", - "path to timestamp bundle FILE") + cmd.Flags().StringVar(&o.RFC3161TimestampPath, "rfc3161-timestamp", "", + "path to RFC3161 timestamp FILE") } // VerifyDockerfileOptions is the top level wrapper for the `dockerfile verify` command. diff --git a/cmd/cosign/cli/policy_init.go b/cmd/cosign/cli/policy_init.go index 15fb5bdde9e..cf3ea4ee754 100644 --- a/cmd/cosign/cli/policy_init.go +++ b/cmd/cosign/cli/policy_init.go @@ -273,8 +273,7 @@ func signPolicy() *cobra.Command { return fmt.Errorf("failed to create TSA client: %w", err) } // Here we get the response from the timestamped authority server - _, err = tsa.GetTimestampedSignature(signed.Signed, clientTSA) - if err != nil { + if _, err := tsa.GetTimestampedSignature(signed.Signed, clientTSA); err != nil { return err } } diff --git a/cmd/cosign/cli/sign/sign_blob.go b/cmd/cosign/cli/sign/sign_blob.go index ebe80351334..8f77c6b0487 100644 --- a/cmd/cosign/cli/sign/sign_blob.go +++ b/cmd/cosign/cli/sign/sign_blob.go @@ -72,19 +72,36 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string signedPayload := cosign.LocalSignedPayload{} + var rfc3161Timestamp *cbundle.RFC3161Timestamp if ko.TSAServerURL != "" { + if ko.RFC3161TimestampPath == "" { + return nil, fmt.Errorf("timestamp output path must be set") + } + clientTSA, err := tsaclient.GetTimestampClient(ko.TSAServerURL) if err != nil { return nil, fmt.Errorf("failed to create TSA client: %w", err) } - b64Sig := []byte(base64.StdEncoding.EncodeToString(sig)) - respBytes, err := tsa.GetTimestampedSignature(b64Sig, clientTSA) + respBytes, err := tsa.GetTimestampedSignature(sig, clientTSA) if err != nil { return nil, err } - signedPayload.RFC3161Timestamp = cbundle.TimestampToRFC3161Timestamp(respBytes) + rfc3161Timestamp = cbundle.TimestampToRFC3161Timestamp(respBytes) + // TODO: Consider uploading RFC3161 TS to Rekor + + if rfc3161Timestamp == nil { + return nil, fmt.Errorf("rfc3161 timestamp is nil") + } + ts, err := json.Marshal(rfc3161Timestamp) + if err != nil { + return nil, err + } + if err := os.WriteFile(ko.RFC3161TimestampPath, ts, 0600); err != nil { + return nil, fmt.Errorf("create RFC3161 timestamp file: %w", err) + } + fmt.Fprintf(os.Stderr, "RFC3161 timestamp written to file %s\n", ko.RFC3161TimestampPath) } if ShouldUploadToTlog(ctx, ko, nil, tlogUpload) { rekorBytes, err = sv.Bytes(ctx) @@ -103,20 +120,6 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string signedPayload.Bundle = cbundle.EntryToBundle(entry) } - // if bundle is specified, just do that and ignore the rest - if ko.RFC3161TimestampPath != "" { - signedPayload.Base64Signature = base64.StdEncoding.EncodeToString(sig) - - contents, err := json.Marshal(signedPayload) - if err != nil { - return nil, err - } - if err := os.WriteFile(ko.RFC3161TimestampPath, contents, 0600); err != nil { - return nil, fmt.Errorf("create rfc3161 timestamp file: %w", err) - } - fmt.Printf("RF3161 timestamp bundle wrote in the file %s\n", ko.RFC3161TimestampPath) - } - // if bundle is specified, just do that and ignore the rest if ko.BundlePath != "" { signedPayload.Base64Signature = base64.StdEncoding.EncodeToString(sig) diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index c5eb7a287a5..235cf5d9478 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -301,6 +301,7 @@ The blob may be specified as a path to a file or - for stdin.`, IgnoreSCT: o.CertVerify.IgnoreSCT, SCTRef: o.CertVerify.SCT, Offline: o.CommonVerifyOptions.Offline, + SkipTlogVerify: o.CommonVerifyOptions.SkipTlogVerify, } if err := verifyBlobCmd.Exec(cmd.Context(), args[0]); err != nil { return fmt.Errorf("verifying blob %s: %w", args, err) diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index d108681a6d6..08e3789d282 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -33,6 +33,7 @@ import ( "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" "github.com/sigstore/cosign/pkg/oci/static" @@ -73,8 +74,8 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { opts := make([]static.Option, 0) // Require a certificate/key OR a local bundle file that has the cert. - if options.NOf(c.KeyRef, c.CertRef, c.Sk, c.BundlePath, c.RFC3161TimestampPath) == 0 { - return fmt.Errorf("please provide a cert to verify against via --certificate or a bundle via --bundle or --rfc3161-timestamp-bundle") + 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") } // Key, sk, and cert are mutually exclusive. @@ -82,7 +83,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { return &options.KeyParseError{} } - sig, err := base64signature(c.SigRef, c.BundlePath, c.RFC3161TimestampPath) + sig, err := base64signature(c.SigRef, c.BundlePath) if err != nil { return err } @@ -208,29 +209,15 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { opts = append(opts, static.WithBundle(b.Bundle)) } if c.RFC3161TimestampPath != "" { - b, err := cosign.FetchLocalSignedPayloadFromPath(c.RFC3161TimestampPath) + var rfc3161Timestamp bundle.RFC3161Timestamp + ts, err := blob.LoadFileOrURL(c.RFC3161TimestampPath) if err != nil { return err } - // Note: RFC3161 timestamp does not set the certificate. - // 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 rfc3161 timestamp bundle: %w", err) - } - } + if err := json.Unmarshal(ts, &rfc3161Timestamp); err != nil { + return err } - opts = append(opts, static.WithRFC3161Timestamp(b.RFC3161Timestamp)) + opts = append(opts, static.WithRFC3161Timestamp(&rfc3161Timestamp)) } // Set an SCT if provided via the CLI. if c.SCTRef != "" { @@ -306,7 +293,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } // base64signature returns the base64 encoded signature -func base64signature(sigRef string, bundlePath, rfc3161TimestampPath string) (string, error) { +func base64signature(sigRef, bundlePath string) (string, error) { var targetSig []byte var err error switch { @@ -325,12 +312,6 @@ func base64signature(sigRef string, bundlePath, rfc3161TimestampPath string) (st return "", err } targetSig = []byte(b.Base64Signature) - case rfc3161TimestampPath != "": - b, err := cosign.FetchLocalSignedPayloadFromPath(rfc3161TimestampPath) - if err != nil { - return "", err - } - targetSig = []byte(b.Base64Signature) default: return "", fmt.Errorf("missing flag '--signature'") } diff --git a/cmd/cosign/cli/verify/verify_blob_test.go b/cmd/cosign/cli/verify/verify_blob_test.go index aec30046c7c..f623eb88b38 100644 --- a/cmd/cosign/cli/verify/verify_blob_test.go +++ b/cmd/cosign/cli/verify/verify_blob_test.go @@ -87,7 +87,7 @@ func TestSignaturesRef(t *testing.T) { for _, test := range tests { t.Run(test.description, func(t *testing.T) { - gotSig, err := base64signature(test.sigRef, "", "") + gotSig, err := base64signature(test.sigRef, "") if test.shouldErr && err != nil { return } @@ -119,34 +119,7 @@ func TestSignaturesBundle(t *testing.T) { t.Fatal(err) } - gotSig, err := base64signature("", fp, "") - if err != nil { - t.Fatal(err) - } - if gotSig != b64sig { - t.Fatalf("unexpected signature, expected: %s got: %s", b64sig, gotSig) - } -} - -func TestSignaturesRFC3161TimestampBundle(t *testing.T) { - td := t.TempDir() - fp := filepath.Join(td, "file") - - 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 := os.WriteFile(fp, contents, 0644); err != nil { - t.Fatal(err) - } - - gotSig, err := base64signature("", "", fp) + gotSig, err := base64signature("", fp) if err != nil { t.Fatal(err) } @@ -550,6 +523,10 @@ func TestVerifyBlob(t *testing.T) { expiredLeafPem, true)}, shouldErr: true, }, + // TODO: Add tests for TSA: + // * With or without bundle + // * Mismatched signature + // * Unexpired and expired certificate } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { @@ -1015,12 +992,6 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { // Create blob blob := "someblob" - // Sign blob with private key - sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) - if err != nil { - t.Fatal(err) - } - // TODO: Replace with a full TSA mock client, related to https://github.com/sigstore/timestamp-authority/issues/146 viper.Set("timestamp-signer", "memory") apiServer := server.NewRestAPIServer("localhost", 0, []string{"http"}, 10*time.Second, 10*time.Second) @@ -1032,8 +1003,7 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { t.Error(err) } - payloadSigner := payload.NewSigner(keyless.rekorSigner) - + payloadSigner := payload.NewSigner(signer) tsaSigner := tsa.NewSigner(payloadSigner, client) var sigTSA oci.Signature sigTSA, _, err = tsaSigner.Sign(context.Background(), bytes.NewReader([]byte(blob))) @@ -1045,6 +1015,7 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { if err != nil { t.Fatalf("unexpected error getting rfc3161 timestamp bundle: %v", err) } + tsPath := writeTimestampFile(t, keyless.td, rfc3161Timestamp, "rfc3161TS.json") chain, err := client.Timestamp.GetTimestampCertChain(nil) if err != nil { @@ -1058,8 +1029,16 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { defer os.Remove(tsaCertChainPath) // Create bundle + b64Sig, err := sigTSA.Base64Signature() + if err != nil { + t.Fatal(err) + } + sig, err := base64.StdEncoding.DecodeString(b64Sig) + if err != nil { + t.Fatal(err) + } entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig) - b := createRFC3161TimestampAndOrRekorBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry, rfc3161Timestamp.SignedRFC3161Timestamp) + 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, blob, "blob.txt") @@ -1070,12 +1049,12 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { CertEmail: identity, CertChain: os.Getenv("SIGSTORE_ROOT_FILE"), SigRef: "", // Sig is fetched from bundle - KeyOpts: options.KeyOpts{BundlePath: bundlePath, TSACertChainPath: tsaCertChainPath}, + KeyOpts: options.KeyOpts{BundlePath: bundlePath, TSACertChainPath: tsaCertChainPath, RFC3161TimestampPath: tsPath}, IgnoreSCT: true, } err = cmd.Exec(context.Background(), blobPath) if err != nil { - t.Fatalf("expected success specifying the intermediates, got %v", err) + t.Fatalf("expected success verifying with timestamp, got %v", err) } }) t.Run("Explicit Fulcio chain with bundle in non-experimental mode", func(t *testing.T) { @@ -1384,37 +1363,6 @@ func createBundle(_ *testing.T, sig []byte, certPem []byte, logID string, integr return b } -func createRFC3161TimestampAndOrRekorBundle(_ *testing.T, sig []byte, certPem []byte, logID string, integratedTime int64, rekorEntry string, rfc3161timestamp []byte) *cosign.LocalSignedPayload { - // Create bundle with: - // * Blob signature - // * Signing certificate - b := &cosign.LocalSignedPayload{ - Base64Signature: base64.StdEncoding.EncodeToString(sig), - Cert: string(certPem), - } - - if rekorEntry != "" { - // * Bundle with a payload and signature over the payload - b.Bundle = &bundle.RekorBundle{ - SignedEntryTimestamp: []byte{}, - Payload: bundle.RekorPayload{ - LogID: logID, - IntegratedTime: integratedTime, - LogIndex: 1, - Body: rekorEntry, - }, - } - } - - if rfc3161timestamp != nil { - b.RFC3161Timestamp = &bundle.RFC3161Timestamp{ - SignedRFC3161Timestamp: rfc3161timestamp, - } - } - - return b -} - func createEntry(ctx context.Context, kind, apiVersion string, blobBytes, certBytes, sigBytes []byte) (types.EntryImpl, error) { props := types.ArtifactProperties{ PublicKeyBytes: [][]byte{certBytes}, @@ -1475,3 +1423,15 @@ func writeBlobFile(t *testing.T, td string, blob string, name string) string { } return blobPath } + +func writeTimestampFile(t *testing.T, td string, ts *bundle.RFC3161Timestamp, name string) string { + jsonBundle, err := json.Marshal(ts) + if err != nil { + t.Fatal(err) + } + path := filepath.Join(td, name) + if err := os.WriteFile(path, jsonBundle, 0644); err != nil { + t.Fatal(err) + } + return path +} diff --git a/doc/cosign_sign-blob.md b/doc/cosign_sign-blob.md index 45b07447069..4c67fb1853c 100644 --- a/doc/cosign_sign-blob.md +++ b/doc/cosign_sign-blob.md @@ -33,29 +33,29 @@ cosign sign-blob [flags] ### Options ``` - --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://fulcio.sigstore.dev") - -h, --help help for sign-blob - --identity-token string [EXPERIMENTAL] identity token to use for certificate from fulcio - --insecure-skip-verify [EXPERIMENTAL] skip verifying fulcio published to the SCT (this should only be used for testing). - --key string path to the private key file, KMS URI or Kubernetes Secret - --oidc-client-id string [EXPERIMENTAL] OIDC client ID for application (default "sigstore") - --oidc-client-secret-file string [EXPERIMENTAL] Path to file containing OIDC client secret for application - --oidc-disable-ambient-providers [EXPERIMENTAL] Disable ambient OIDC providers. When true, ambient credentials will not be read - --oidc-issuer string [EXPERIMENTAL] OIDC provider to be used to issue ID token (default "https://oauth2.sigstore.dev/auth") - --oidc-provider string [EXPERIMENTAL] Specify the provider to get the OIDC token from (Optional). If unset, all options will be tried. Options include: [spiffe, google, github, filesystem] - --oidc-redirect-url string [EXPERIMENTAL] OIDC redirect URL (Optional). The default oidc-redirect-url is 'http://localhost:0/auth/callback'. - --output string write the signature to FILE - --output-certificate string write the certificate to FILE - --output-signature string write the signature to FILE - --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") - --rfc3161-timestamp-bundle string write everything required to verify the blob to a FILE - --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-server-url string url to the Timestamp RFC3161 server, default none - --tlog-upload whether or not to upload to the tlog (default true) - -y, --yes skip confirmation prompts for non-destructive operations + --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://fulcio.sigstore.dev") + -h, --help help for sign-blob + --identity-token string [EXPERIMENTAL] identity token to use for certificate from fulcio + --insecure-skip-verify [EXPERIMENTAL] skip verifying fulcio published to the SCT (this should only be used for testing). + --key string path to the private key file, KMS URI or Kubernetes Secret + --oidc-client-id string [EXPERIMENTAL] OIDC client ID for application (default "sigstore") + --oidc-client-secret-file string [EXPERIMENTAL] Path to file containing OIDC client secret for application + --oidc-disable-ambient-providers [EXPERIMENTAL] Disable ambient OIDC providers. When true, ambient credentials will not be read + --oidc-issuer string [EXPERIMENTAL] OIDC provider to be used to issue ID token (default "https://oauth2.sigstore.dev/auth") + --oidc-provider string [EXPERIMENTAL] Specify the provider to get the OIDC token from (Optional). If unset, all options will be tried. Options include: [spiffe, google, github, filesystem] + --oidc-redirect-url string [EXPERIMENTAL] OIDC redirect URL (Optional). The default oidc-redirect-url is 'http://localhost:0/auth/callback'. + --output string write the signature to FILE + --output-certificate string write the certificate to FILE + --output-signature string write the signature to FILE + --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") + --rfc3161-timestamp string write the RFC3161 timestamp to a file + --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-server-url string url to the Timestamp RFC3161 server, default none + --tlog-upload whether or not to upload to the tlog (default true) + -y, --yes skip confirmation prompts for non-destructive operations ``` ### Options inherited from parent commands diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md index 43771a0110b..29124ca48d4 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -79,7 +79,7 @@ cosign verify-blob [flags] --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-bundle string path to timestamp bundle FILE + --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 diff --git a/internal/pkg/cosign/tsa/signer.go b/internal/pkg/cosign/tsa/signer.go index c4afc6a7194..74dda4f2756 100644 --- a/internal/pkg/cosign/tsa/signer.go +++ b/internal/pkg/cosign/tsa/signer.go @@ -18,6 +18,7 @@ import ( "bytes" "context" "crypto" + "encoding/base64" "fmt" "io" "os" @@ -36,12 +37,13 @@ import ( ts "github.com/sigstore/timestamp-authority/pkg/generated/client/timestamp" ) +// GetTimestampedSignature queries a timestamp authority to fetch an RFC3161 timestamp. sigBytes is an +// opaque blob, but is typically a signature over an artifact. func GetTimestampedSignature(sigBytes []byte, tsaClient *tsaclient.TimestampAuthority) ([]byte, error) { requestBytes, err := createTimestampAuthorityRequest(sigBytes, crypto.SHA256, "") if err != nil { return nil, err } - fmt.Fprintln(os.Stderr, "Calling TSA authority ...") params := ts.NewGetTimestampResponseParams() params.SetTimeout(time.Second * 10) params.Request = io.NopCloser(bytes.NewReader(requestBytes)) @@ -58,7 +60,7 @@ func GetTimestampedSignature(sigBytes []byte, tsaClient *tsaclient.TimestampAuth return nil, err } - fmt.Fprintln(os.Stderr, "Timestamp fetched with time:", ts.Time) + fmt.Fprintln(os.Stderr, "Timestamp fetched with time: ", ts.Time) return respBytes.Bytes(), nil } @@ -84,8 +86,14 @@ func (rs *signerWrapper) Sign(ctx context.Context, payload io.Reader) (oci.Signa return nil, nil, err } - // Here we get the response from the timestamped authority server - responseBytes, err := GetTimestampedSignature([]byte(b64Sig), rs.tsaClient) + // create timestamp over raw bytes of signature + rawSig, err := base64.StdEncoding.DecodeString(b64Sig) + if err != nil { + return nil, nil, err + } + + // fetch rfc3161 timestamp from timestamp authority + responseBytes, err := GetTimestampedSignature(rawSig, rs.tsaClient) if err != nil { return nil, nil, err } diff --git a/pkg/cosign/bundle/tsa.go b/pkg/cosign/bundle/tsa.go index e11864b7a75..bbb846759b2 100644 --- a/pkg/cosign/bundle/tsa.go +++ b/pkg/cosign/bundle/tsa.go @@ -16,9 +16,10 @@ package bundle // RFC3161Timestamp holds metadata about timestamp RFC3161 verification data. type RFC3161Timestamp struct { - // SignedRFC3161Timestamp contains a RFC3161 signed timestamp provided by a time-stamping server. - // Clients MUST verify the hashed message in the message imprint - // against the signature in the bundle. This is encoded as base64. + // SignedRFC3161Timestamp contains a DER encoded TimeStampResponse. + // See https://www.rfc-editor.org/rfc/rfc3161.html#section-2.4.2 + // Clients MUST verify the hashed message in the message imprint, + // typically using the artifact signature. SignedRFC3161Timestamp []byte } diff --git a/pkg/cosign/fetch.go b/pkg/cosign/fetch.go index b42a949e146..70ffb2f3a67 100644 --- a/pkg/cosign/fetch.go +++ b/pkg/cosign/fetch.go @@ -39,10 +39,9 @@ type SignedPayload struct { } type LocalSignedPayload struct { - Base64Signature string `json:"base64Signature"` - Cert string `json:"cert,omitempty"` - Bundle *bundle.RekorBundle `json:"rekorBundle,omitempty"` - RFC3161Timestamp *bundle.RFC3161Timestamp `json:"rfc3161Timestamp,omitempty"` + Base64Signature string `json:"base64Signature"` + Cert string `json:"cert,omitempty"` + Bundle *bundle.RekorBundle `json:"rekorBundle,omitempty"` } type Signatures struct { diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 99962afd9da..024dcbce0f5 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -666,7 +666,7 @@ func verifyInternal(ctx context.Context, sig oci.Signature, h v1.Hash, } } if co.TSACerts != nil { - bundleVerified, err = VerifyRFC3161Timestamp(ctx, sig, co.TSACerts) + err = VerifyRFC3161Timestamp(sig, co.TSACerts) if err != nil { return false, fmt.Errorf("unable to verify RFC3161 timestamp bundle: %w", err) } @@ -986,52 +986,59 @@ func VerifyBundle(ctx context.Context, sig oci.Signature, co *CheckOpts, rekorCl return true, nil } -func VerifyRFC3161Timestamp(ctx context.Context, sig oci.Signature, tsaCerts *x509.CertPool) (bool, error) { - bundle, err := sig.RFC3161Timestamp() +func VerifyRFC3161Timestamp(sig oci.Signature, tsaCerts *x509.CertPool) error { + ts, err := sig.RFC3161Timestamp() if err != nil { - return false, err - } else if bundle == nil { - return false, nil + return err + } else if ts == nil { + return nil } b64Sig, err := sig.Base64Signature() if err != nil { - return false, fmt.Errorf("reading base64signature: %w", err) + return fmt.Errorf("reading base64signature: %w", err) } cert, err := sig.Cert() if err != nil { - return false, err + return err } - verifiedBytes := []byte(b64Sig) + var tsBytes []byte if len(b64Sig) == 0 { // For attestations, the Base64Signature is not set, therefore we rely on the signed payload signedPayload, err := sig.Payload() if err != nil { - return false, fmt.Errorf("reading the payload: %w", err) + return fmt.Errorf("reading the payload: %w", err) } - verifiedBytes = signedPayload + tsBytes = signedPayload + } else { + // create timestamp over raw bytes of signature + rawSig, err := base64.StdEncoding.DecodeString(b64Sig) + if err != nil { + return err + } + tsBytes = rawSig } - err = tsaverification.VerifyTimestampResponse(bundle.SignedRFC3161Timestamp, bytes.NewReader(verifiedBytes), tsaCerts) + err = tsaverification.VerifyTimestampResponse(ts.SignedRFC3161Timestamp, bytes.NewReader(tsBytes), tsaCerts) if err != nil { - return false, fmt.Errorf("unable to verify TimestampResponse: %w", err) + return fmt.Errorf("unable to verify TimestampResponse: %w", err) } if cert != nil { - ts, err := timestamp.ParseResponse(bundle.SignedRFC3161Timestamp) + ts, err := timestamp.ParseResponse(ts.SignedRFC3161Timestamp) if err != nil { - return false, fmt.Errorf("error parsing response into timestamp: %w", err) + return fmt.Errorf("error parsing response into timestamp: %w", err) } // Verify the cert against the integrated time. // Note that if the caller requires the certificate to be present, it has to ensure that itself. if err := CheckExpiry(cert, ts.Time); err != nil { - return false, fmt.Errorf("checking expiry on cert: %w", err) + return fmt.Errorf("checking expiry on certificate: %w", err) } } - return true, nil + return nil } // compare bundle signature to the signature we are verifying diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index f61ca64cbf3..95e28f75d22 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -18,6 +18,7 @@ import ( "bytes" "context" "crypto" + "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/sha256" @@ -465,7 +466,7 @@ func TestVerifyImageSignatureWithSigVerifierAndTSA(t *testing.T) { SigVerifier: sv, TSACerts: tsaCertPool, SkipTlogVerify: true, - }); err != nil || !bundleVerified { + }); err != nil || bundleVerified { // bundle is not verified since there's no Rekor bundle t.Fatalf("unexpected error while verifying signature, got %v", err) } } @@ -1287,3 +1288,113 @@ func Test_getSubjectAltnernativeNames(t *testing.T) { t.Fatalf("unexpected URL SAN value") } } + +func TestVerifyRFC3161Timestamp(t *testing.T) { + // generate signed artifact + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, privKey, _ := test.GenerateLeafCert("subject", "oidc-issuer", rootCert, rootKey) + pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) + pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) + payload := []byte{1, 2, 3, 4} + h := sha256.Sum256(payload) + signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) + + // TODO: Replace with a TSA mock client, blocked by https://github.com/sigstore/timestamp-authority/issues/146 + viper.Set("timestamp-signer", "memory") + apiServer := server.NewRestAPIServer("localhost", 0, []string{"http"}, 10*time.Second, 10*time.Second) + server := httptest.NewServer(apiServer.GetHandler()) + t.Cleanup(server.Close) + client, err := tsaclient.GetTimestampClient(server.URL) + if err != nil { + t.Fatal(err) + } + + tsBytes, err := tsa.GetTimestampedSignature(signature, client) + if err != nil { + t.Fatalf("unexpected error creating timestamp: %v", err) + } + rfc3161TS := bundle.RFC3161Timestamp{SignedRFC3161Timestamp: tsBytes} + chain, err := client.Timestamp.GetTimestampCertChain(nil) + if err != nil { + t.Fatalf("unexpected error getting timestamp chain: %v", err) + } + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM([]byte(chain.Payload)) { + t.Fatalf("error creating trust root pool") + } + + ociSig, _ := static.NewSignature(payload, + base64.StdEncoding.EncodeToString(signature), + static.WithCertChain(pemLeaf, appendSlices([][]byte{pemRoot})), + static.WithRFC3161Timestamp(&rfc3161TS)) + + // success, signing over signature + err = VerifyRFC3161Timestamp(ociSig, pool) + if err != nil { + t.Fatalf("unexpected error verifying timestamp with signature: %v", err) + } + + // success, signing over payload + tsBytes, err = tsa.GetTimestampedSignature(payload, client) + if err != nil { + t.Fatalf("unexpected error creating timestamp: %v", err) + } + rfc3161TS = bundle.RFC3161Timestamp{SignedRFC3161Timestamp: tsBytes} + ociSig, _ = static.NewSignature(payload, + "", /*signature*/ + static.WithCertChain(pemLeaf, appendSlices([][]byte{pemRoot})), + static.WithRFC3161Timestamp(&rfc3161TS)) + err = VerifyRFC3161Timestamp(ociSig, pool) + if err != nil { + t.Fatalf("unexpected error verifying timestamp with payload: %v", err) + } + + // failure with non-base64 encoded signature + ociSig, _ = static.NewSignature(payload, + string(signature), + static.WithCertChain(pemLeaf, appendSlices([][]byte{pemRoot})), + static.WithRFC3161Timestamp(&rfc3161TS)) + err = VerifyRFC3161Timestamp(ociSig, pool) + if err == nil || !strings.Contains(err.Error(), "base64 data") { + t.Fatalf("expected error verifying timestamp with raw signature, got: %v", err) + } + + // failure with mismatched signature + tsBytes, err = tsa.GetTimestampedSignature(signature, client) + if err != nil { + t.Fatalf("unexpected error creating timestamp: %v", err) + } + rfc3161TS = bundle.RFC3161Timestamp{SignedRFC3161Timestamp: tsBytes} + // regenerate signature + signature, _ = privKey.Sign(rand.Reader, h[:], crypto.SHA256) + ociSig, _ = static.NewSignature(payload, + base64.StdEncoding.EncodeToString(signature), + static.WithCertChain(pemLeaf, appendSlices([][]byte{pemRoot})), + static.WithRFC3161Timestamp(&rfc3161TS)) + err = VerifyRFC3161Timestamp(ociSig, pool) + if err == nil || !strings.Contains(err.Error(), "hashed messages don't match") { + t.Fatalf("expected error verifying mismatched signatures, got: %v", err) + } + + // failure with old certificate + leafPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + // generate old certificate + leafCert, _ = test.GenerateLeafCertWithExpiration("subject", "oidc-issuer", time.Now().AddDate(-1, 0, 0), leafPriv, rootCert, rootKey) + pemLeaf = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) + tsBytes, err = tsa.GetTimestampedSignature(signature, client) + if err != nil { + t.Fatalf("unexpected error creating timestamp: %v", err) + } + rfc3161TS = bundle.RFC3161Timestamp{SignedRFC3161Timestamp: tsBytes} + ociSig, _ = static.NewSignature(payload, + base64.StdEncoding.EncodeToString(signature), + static.WithCertChain(pemLeaf, appendSlices([][]byte{pemRoot})), + static.WithRFC3161Timestamp(&rfc3161TS)) + err = VerifyRFC3161Timestamp(ociSig, pool) + if err == nil || !strings.Contains(err.Error(), "checking expiry on certificate") { + t.Fatalf("expected error verifying old certificate, got: %v", err) + } +} diff --git a/specs/SIGNATURE_SPEC.md b/specs/SIGNATURE_SPEC.md index e2ad2aba3e6..20cf5460555 100644 --- a/specs/SIGNATURE_SPEC.md +++ b/specs/SIGNATURE_SPEC.md @@ -129,6 +129,11 @@ Gyp4apdU7AXEwysEQIb034aPrTlpmxh90SnTZFs2DHOvCjCPPAmoWfuQUwPhSPRb For instructions on using the `bundle` for verification, see [USAGE.md](../USAGE.md#verify-a-signature-was-added-to-the-transparency-log). +* `rfc3161timestamp` string + + This OPTIONAL property contains a JSON formatted `RFC3161Timestamp` containing the timestamp response from a + timestamp authority. + ## Storage `cosign` image signatures are stored in an OCI registry and are designed to make use of the existing specifications. diff --git a/test/e2e_test.go b/test/e2e_test.go index a5b99b5a5d6..cc7c54ffb26 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -997,8 +997,8 @@ func TestSignBlobRFC3161TimestampBundle(t *testing.T) { os.RemoveAll(td1) }) bp := filepath.Join(td1, blob) - tsPath := filepath.Join(td1, "rfc3161TimestampBundle.sig") bundlePath := filepath.Join(td1, "bundle.sig") + tsPath := filepath.Join(td1, "rfc3161Timestamp.json") if err := os.WriteFile(bp, []byte(blob), 0644); err != nil { t.Fatal(err) @@ -1030,9 +1030,9 @@ func TestSignBlobRFC3161TimestampBundle(t *testing.T) { ko1 := options.KeyOpts{ KeyRef: pubKeyPath1, + BundlePath: bundlePath, RFC3161TimestampPath: tsPath, TSACertChainPath: file.Name(), - BundlePath: bundlePath, } // Verify should fail on a bad input verifyBlobCmd := cliverify.VerifyBlobCmd{ @@ -1045,10 +1045,10 @@ func TestSignBlobRFC3161TimestampBundle(t *testing.T) { ko := options.KeyOpts{ KeyRef: privKeyPath1, PassFunc: passFunc, + BundlePath: bundlePath, RFC3161TimestampPath: tsPath, TSAServerURL: server.URL, RekorURL: rekorURL, - BundlePath: bundlePath, } if _, err := sign.SignBlobCmd(ro, ko, bp, true, "", "", false); err != nil { t.Fatal(err)