diff --git a/.github/workflows/validate-release.yml b/.github/workflows/validate-release.yml index 45545a5e3b7..a55ce53d3f5 100644 --- a/.github/workflows/validate-release.yml +++ b/.github/workflows/validate-release.yml @@ -39,7 +39,7 @@ jobs: statuses: none env: - CROSS_BUILDER_IMAGE: ghcr.io/gythialy/golang-cross:v1.19.3-0@sha256:1072190e76d68f455f1bedb7430a633916b6629a722c42246037ac518fdb0ff2 + CROSS_BUILDER_IMAGE: ghcr.io/gythialy/golang-cross:v1.19.4-0@sha256:53ee894818ac14377996a6fe7c8fe6156d018a20f82aaf69f2519fc45d897bec COSIGN_IMAGE: gcr.io/projectsigstore/cosign:v1.13.1@sha256:fd5b09be23ef1027e1bdd490ce78dcc65d2b15902e1f4ba8e04f3b4019cc1057 steps: 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.go b/cmd/cosign/cli/verify/verify.go index e7b5b829f40..9872366a537 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -150,8 +150,16 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { } 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 keylessVerification(c.KeyRef, c.Sk) { + // This performs an online fetch of the Fulcio roots. This is needed + // for verifying keyless certificates (both online and offline). co.RootCerts, err = fulcio.GetRoots() if err != nil { return fmt.Errorf("getting Fulcio roots: %w", err) diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index 8514362b02f..8fd0fb9a2a9 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -129,8 +129,16 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e } 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 keylessVerification(c.KeyRef, c.Sk) { + // This performs an online fetch of the Fulcio roots. This is needed + // for verifying keyless certificates (both online and offline). co.RootCerts, err = fulcio.GetRoots() if err != nil { return fmt.Errorf("getting Fulcio roots: %w", err) diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index d108681a6d6..a4fb641a3cb 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 } @@ -136,9 +137,17 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } 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 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 { @@ -208,29 +217,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 +301,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 +320,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 2517e7c98c1..d5356672cff 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) } @@ -273,8 +246,6 @@ func TestVerifyBlob(t *testing.T) { key []byte cert *x509.Certificate bundlePath string - // If online lookups to Rekor are enabled - experimental bool // The rekor entry response when Rekor is enabled rekorEntry []*models.LogEntry skipTlogVerify bool @@ -285,280 +256,255 @@ func TestVerifyBlob(t *testing.T) { blob: blobBytes, signature: blobSignature, key: pubKeyBytes, - experimental: false, shouldErr: false, skipTlogVerify: true, }, { - name: "valid signature with public key - experimental no rekor fail", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - experimental: true, - rekorEntry: nil, - shouldErr: true, + name: "valid signature with public key - experimental no rekor fail", + blob: blobBytes, + signature: blobSignature, + key: pubKeyBytes, + rekorEntry: nil, + shouldErr: true, }, { - name: "valid signature with public key - experimental rekor entry success", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - experimental: true, + name: "valid signature with public key - experimental rekor entry success", + blob: blobBytes, + signature: blobSignature, + key: pubKeyBytes, rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), pubKeyBytes, true)}, shouldErr: false, }, { - name: "valid signature with public key - good bundle provided", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - experimental: false, + name: "valid signature with public key - good bundle provided", + blob: blobBytes, + signature: blobSignature, + key: pubKeyBytes, bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), pubKeyBytes, true), shouldErr: false, }, { - name: "valid signature with public key - bundle without rekor bundle fails", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - experimental: false, - bundlePath: makeLocalBundleWithoutRekorBundle(t, []byte(blobSignature), pubKeyBytes), - shouldErr: true, + name: "valid signature with public key - bundle without rekor bundle fails", + blob: blobBytes, + signature: blobSignature, + key: pubKeyBytes, + bundlePath: makeLocalBundleWithoutRekorBundle(t, []byte(blobSignature), pubKeyBytes), + shouldErr: true, }, { - name: "valid signature with public key - bad bundle SET", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - experimental: false, + name: "valid signature with public key - bad bundle SET", + blob: blobBytes, + signature: blobSignature, + key: pubKeyBytes, bundlePath: makeLocalBundle(t, *signer, blobBytes, []byte(blobSignature), unexpiredCertPem, true), shouldErr: true, }, { - name: "valid signature with public key - bad bundle cert mismatch", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - experimental: false, + name: "valid signature with public key - bad bundle cert mismatch", + blob: blobBytes, + signature: blobSignature, + key: pubKeyBytes, bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), unexpiredCertPem, true), shouldErr: true, }, { - name: "valid signature with public key - bad bundle signature mismatch", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - experimental: false, + name: "valid signature with public key - bad bundle signature mismatch", + blob: blobBytes, + signature: blobSignature, + key: pubKeyBytes, bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(makeSignature(blobBytes)), pubKeyBytes, true), shouldErr: true, }, { - name: "valid signature with public key - bad bundle msg & signature mismatch", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - experimental: false, + name: "valid signature with public key - bad bundle msg & signature mismatch", + blob: blobBytes, + signature: blobSignature, + key: pubKeyBytes, bundlePath: makeLocalBundle(t, *rekorSigner, otherBytes, []byte(otherSignature), pubKeyBytes, true), shouldErr: true, }, { - name: "invalid signature with public key", - blob: blobBytes, - signature: otherSignature, - key: pubKeyBytes, - experimental: false, - shouldErr: true, + name: "invalid signature with public key", + blob: blobBytes, + signature: otherSignature, + key: pubKeyBytes, + shouldErr: true, }, { - name: "invalid signature with public key - experimental", - blob: blobBytes, - signature: otherSignature, - key: pubKeyBytes, - experimental: true, - shouldErr: true, + name: "invalid signature with public key - experimental", + blob: blobBytes, + signature: otherSignature, + key: pubKeyBytes, + shouldErr: true, }, { - name: "valid signature with unexpired certificate - no rekor entry", - blob: blobBytes, - signature: blobSignature, - cert: unexpiredLeafCert, - experimental: false, - shouldErr: true, + name: "valid signature with unexpired certificate - no rekor entry", + blob: blobBytes, + signature: blobSignature, + cert: unexpiredLeafCert, + shouldErr: true, }, { - name: "valid signature with unexpired certificate - bad bundle cert mismatch", - blob: blobBytes, - signature: blobSignature, - experimental: false, - key: pubKeyBytes, + name: "valid signature with unexpired certificate - bad bundle cert mismatch", + blob: blobBytes, + signature: blobSignature, + key: pubKeyBytes, bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), unexpiredCertPem, true), shouldErr: true, }, { - name: "valid signature with unexpired certificate - bad bundle signature mismatch", - blob: blobBytes, - signature: blobSignature, - experimental: false, - cert: unexpiredLeafCert, + name: "valid signature with unexpired certificate - bad bundle signature mismatch", + blob: blobBytes, + signature: blobSignature, + cert: unexpiredLeafCert, bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(makeSignature(blobBytes)), unexpiredCertPem, true), shouldErr: true, }, { - name: "valid signature with unexpired certificate - bad bundle msg & signature mismatch", - blob: blobBytes, - signature: blobSignature, - experimental: false, - cert: unexpiredLeafCert, + name: "valid signature with unexpired certificate - bad bundle msg & signature mismatch", + blob: blobBytes, + signature: blobSignature, + cert: unexpiredLeafCert, bundlePath: makeLocalBundle(t, *rekorSigner, otherBytes, []byte(otherSignature), unexpiredCertPem, true), shouldErr: true, }, { - name: "invalid signature with unexpired certificate", - blob: blobBytes, - signature: otherSignature, - cert: unexpiredLeafCert, - experimental: false, - shouldErr: true, + name: "invalid signature with unexpired certificate", + blob: blobBytes, + signature: otherSignature, + cert: unexpiredLeafCert, + shouldErr: true, }, { - name: "valid signature with unexpired certificate - experimental", - blob: blobBytes, - signature: blobSignature, - cert: unexpiredLeafCert, - experimental: true, + name: "valid signature with unexpired certificate - experimental", + blob: blobBytes, + signature: blobSignature, + cert: unexpiredLeafCert, rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), unexpiredCertPem, true)}, shouldErr: false, }, { - name: "valid signature with unexpired certificate - experimental & rekor entry found", - blob: blobBytes, - signature: blobSignature, - cert: unexpiredLeafCert, - experimental: true, + name: "valid signature with unexpired certificate - experimental & rekor entry found", + blob: blobBytes, + signature: blobSignature, + cert: unexpiredLeafCert, rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), unexpiredCertPem, true)}, shouldErr: false, }, { - name: "valid signature with expired certificate + Rekor", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - experimental: false, - shouldErr: true, + name: "valid signature with expired certificate + Rekor", + blob: blobBytes, + signature: blobSignature, + cert: expiredLeafCert, + shouldErr: true, }, { name: "valid signature with expired certificate, no Rekor", blob: blobBytes, signature: blobSignature, cert: expiredLeafCert, - experimental: false, skipTlogVerify: true, shouldErr: true, }, { - name: "valid signature with expired certificate - experimental good rekor lookup", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - experimental: true, + name: "valid signature with expired certificate - experimental good rekor lookup", + blob: blobBytes, + signature: blobSignature, + cert: expiredLeafCert, rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), expiredLeafPem, true)}, shouldErr: false, }, { - name: "valid signature with expired certificate - experimental multiple rekor entries", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - experimental: true, + name: "valid signature with expired certificate - experimental multiple rekor entries", + blob: blobBytes, + signature: blobSignature, + cert: expiredLeafCert, rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), expiredLeafPem, true), makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), expiredLeafPem, false)}, shouldErr: false, }, { - name: "valid signature with expired certificate - experimental bad rekor integrated time", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - experimental: true, + name: "valid signature with expired certificate - experimental bad rekor integrated time", + blob: blobBytes, + signature: blobSignature, + cert: expiredLeafCert, rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), expiredLeafPem, false)}, shouldErr: true, }, { - name: "valid signature with unexpired certificate - good bundle, nonexperimental", - blob: blobBytes, - signature: blobSignature, - cert: unexpiredLeafCert, - experimental: false, + name: "valid signature with unexpired certificate - good bundle, nonexperimental", + blob: blobBytes, + signature: blobSignature, + cert: unexpiredLeafCert, bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), unexpiredCertPem, true), shouldErr: false, }, { - name: "valid signature with expired certificate - good bundle, nonexperimental", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - experimental: false, + name: "valid signature with expired certificate - good bundle, nonexperimental", + blob: blobBytes, + signature: blobSignature, + cert: expiredLeafCert, bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), expiredLeafPem, true), shouldErr: false, }, { - name: "valid signature with expired certificate - bundle with bad expiration", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - experimental: false, + name: "valid signature with expired certificate - bundle with bad expiration", + blob: blobBytes, + signature: blobSignature, + cert: expiredLeafCert, bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), expiredLeafPem, false), shouldErr: true, }, { - name: "valid signature with expired certificate - bundle with bad SET", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - experimental: false, + name: "valid signature with expired certificate - bundle with bad SET", + blob: blobBytes, + signature: blobSignature, + cert: expiredLeafCert, bundlePath: makeLocalBundle(t, *signer, blobBytes, []byte(blobSignature), expiredLeafPem, true), shouldErr: true, }, { - name: "valid signature with expired certificate - experimental good bundle", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - experimental: true, + name: "valid signature with expired certificate - experimental good bundle", + blob: blobBytes, + signature: blobSignature, + cert: expiredLeafCert, bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), expiredLeafPem, true), shouldErr: false, }, { - name: "valid signature with expired certificate - experimental bad rekor entry", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - experimental: true, + name: "valid signature with expired certificate - experimental bad rekor entry", + blob: blobBytes, + signature: blobSignature, + cert: expiredLeafCert, // This is the wrong signer for the SET! rekorEntry: []*models.LogEntry{makeRekorEntry(t, *signer, blobBytes, []byte(blobSignature), 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) { @@ -1024,12 +970,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) @@ -1041,8 +981,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))) @@ -1054,6 +993,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 { @@ -1067,8 +1007,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") @@ -1079,12 +1027,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) { @@ -1393,37 +1341,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}, @@ -1484,3 +1401,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/go.mod b/go.mod index 6c71047c45b..a5eff735574 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/manifoldco/promptui v0.9.0 github.com/miekg/pkcs11 v1.1.1 github.com/mozillazg/docker-credential-acr-helper v0.3.0 - github.com/open-policy-agent/opa v0.47.0 + github.com/open-policy-agent/opa v0.47.1 github.com/pkg/errors v0.9.1 github.com/secure-systems-lab/go-securesystemslib v0.4.0 github.com/sigstore/fulcio v1.0.0 @@ -39,11 +39,11 @@ require ( github.com/transparency-dev/merkle v0.0.1 github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1 github.com/xanzy/go-gitlab v0.76.0 - golang.org/x/crypto v0.3.0 + golang.org/x/crypto v0.4.0 golang.org/x/oauth2 v0.3.0 golang.org/x/sync v0.1.0 golang.org/x/term v0.3.0 - google.golang.org/api v0.103.0 + google.golang.org/api v0.104.0 k8s.io/api v0.23.5 k8s.io/apimachinery v0.23.5 k8s.io/client-go v0.23.5 @@ -52,9 +52,9 @@ require ( ) require ( - cloud.google.com/go/compute v1.12.1 // indirect - cloud.google.com/go/compute/metadata v0.2.1 // indirect - cloud.google.com/go/iam v0.7.0 // indirect + cloud.google.com/go/compute v1.13.0 // indirect + cloud.google.com/go/compute/metadata v0.2.2 // indirect + cloud.google.com/go/iam v0.8.0 // indirect cloud.google.com/go/kms v1.6.0 // indirect filippo.io/edwards25519 v1.0.0 // indirect github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo/helper v0.2.0 // indirect @@ -250,7 +250,7 @@ require ( golang.org/x/time v0.2.0 // indirect golang.org/x/tools v0.1.12 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e // indirect + google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6 // indirect google.golang.org/grpc v1.51.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 4c4cbd5eec6..66c532a2606 100644 --- a/go.sum +++ b/go.sum @@ -43,16 +43,16 @@ cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6m cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= -cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0= -cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48= -cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/compute v1.13.0 h1:AYrLkB8NPdDRslNp4Jxmzrhdr03fUAIDbiGFjLWowoU= +cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= +cloud.google.com/go/compute/metadata v0.2.2 h1:aWKAjYaBaOSrpKl57+jnS/3fJRQnxL7TvR/u1VVbt6k= +cloud.google.com/go/compute/metadata v0.2.2/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs= -cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= +cloud.google.com/go/iam v0.8.0 h1:E2osAkZzxI/+8pZcxVLcDtAQx/u+hZXVryUaYQ5O0Kk= +cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= cloud.google.com/go/kms v1.6.0 h1:OWRZzrPmOZUzurjI2FBGtgY2mB1WaJkqhw6oIwSj0Yg= cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= @@ -826,8 +826,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/open-policy-agent/opa v0.47.0 h1:d6g0oDNLraIcWl9LXW8cBzRYf2zt7vSbPGEd2+8K3Lg= -github.com/open-policy-agent/opa v0.47.0/go.mod h1:cM7ngEoEdAIfyu9mOHaVcgLAHYkY6amrYfotm+BSkYQ= +github.com/open-policy-agent/opa v0.47.1 h1:4Nf8FwguZeE5P83akiwaaoWx1XkmSkRcKmCEskiD/1c= +github.com/open-policy-agent/opa v0.47.1/go.mod h1:cM7ngEoEdAIfyu9mOHaVcgLAHYkY6amrYfotm+BSkYQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= @@ -1113,8 +1113,8 @@ golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= -golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1495,8 +1495,8 @@ google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69 google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ= -google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= +google.golang.org/api v0.104.0 h1:KBfmLRqdZEbwQleFlSLnzpQJwhjpmNOk4cKQIBDZ9mg= +google.golang.org/api v0.104.0/go.mod h1:JCspTXJbBxa5ySXw4UgUqVer7DfVxbvc/CTUFqAED5U= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1587,8 +1587,8 @@ google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e h1:azcyH5lGzGy7pkLCbhPe0KkKxsM7c6UA/FZIXImKE7M= -google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6 h1:AGXp12e/9rItf6/4QymU7WsAUwCf+ICW75cuR91nJIc= +google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6/go.mod h1:1dOng4TWOomJrDGhpXjfCD35wQC6jnC7HpRmOFRqEV0= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 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/tlog.go b/pkg/cosign/tlog.go index 0ede85db2d0..a065edc237e 100644 --- a/pkg/cosign/tlog.go +++ b/pkg/cosign/tlog.go @@ -57,6 +57,13 @@ type RekorPubKey struct { Status tuf.StatusKind } +// This is a map of RekorPubKeys indexed by log ID that's used in verification +// for the trusted set of RekorPubKeys. +type TrustedRekorPubKeys struct { + // A map of keys indexed by log ID + Keys map[string]RekorPubKey +} + const treeIDHexStringLen = 16 const uuidHexStringLen = 64 const entryIDHexStringLen = treeIDHexStringLen + uuidHexStringLen @@ -91,8 +98,8 @@ func intotoEntry(ctx context.Context, signature, pubKey []byte) (models.Proposed // There are two Env variable that can be used to override this behaviour: // SIGSTORE_REKOR_PUBLIC_KEY - If specified, location of the file that contains // the Rekor Public Key on local filesystem -func GetRekorPubs(ctx context.Context, _ *client.Rekor) (map[string]RekorPubKey, error) { - publicKeys := make(map[string]RekorPubKey) +func GetRekorPubs(ctx context.Context) (*TrustedRekorPubKeys, error) { + publicKeys := NewTrustedRekorPubKeys() altRekorPub := env.Getenv(env.VariableSigstoreRekorPublicKey) if altRekorPub != "" { @@ -100,15 +107,9 @@ func GetRekorPubs(ctx context.Context, _ *client.Rekor) (map[string]RekorPubKey, if err != nil { return nil, fmt.Errorf("error reading alternate Rekor public key file: %w", err) } - extra, err := PemToECDSAKey(raw) - if err != nil { - return nil, fmt.Errorf("error converting PEM to ECDSAKey: %w", err) - } - keyID, err := getLogID(extra) - if err != nil { - return nil, fmt.Errorf("error generating log ID: %w", err) + if err := publicKeys.AddRekorPubKey(raw, tuf.Active); err != nil { + return nil, fmt.Errorf("AddRekorPubKey: %w", err) } - publicKeys[keyID] = RekorPubKey{PubKey: extra, Status: tuf.Active} } else { tufClient, err := tuf.NewFromEnv(ctx) if err != nil { @@ -119,23 +120,32 @@ func GetRekorPubs(ctx context.Context, _ *client.Rekor) (map[string]RekorPubKey, return nil, err } for _, t := range targets { - rekorPubKey, err := PemToECDSAKey(t.Target) - if err != nil { - return nil, fmt.Errorf("pem to ecdsa: %w", err) - } - keyID, err := getLogID(rekorPubKey) - if err != nil { - return nil, fmt.Errorf("error generating log ID: %w", err) + if err := publicKeys.AddRekorPubKey(t.Target, t.Status); err != nil { + return nil, fmt.Errorf("AddRekorPubKey: %w", err) } - publicKeys[keyID] = RekorPubKey{PubKey: rekorPubKey, Status: t.Status} } } - if len(publicKeys) == 0 { + if len(publicKeys.Keys) == 0 { return nil, errors.New("none of the Rekor public keys have been found") } - return publicKeys, nil + return &publicKeys, nil +} + +// rekorPubsFromClient returns a RekorPubKey keyed by the log ID from the Rekor client. +// NOTE: This **must not** be used in the verification path, but may be used in the +// sign path to validate return responses are consistent from Rekor. +func rekorPubsFromClient(rekorClient *client.Rekor) (*TrustedRekorPubKeys, error) { + publicKeys := NewTrustedRekorPubKeys() + pubOK, err := rekorClient.Pubkey.GetPublicKey(nil) + if err != nil { + return nil, fmt.Errorf("unable to fetch rekor public key from rekor: %w", err) + } + if err := publicKeys.AddRekorPubKey([]byte(pubOK.Payload), tuf.Active); err != nil { + return nil, fmt.Errorf("constructRekorPubKey: %w", err) + } + return &publicKeys, nil } // TLogUpload will upload the signature, public key and payload to the transparency log. @@ -174,7 +184,11 @@ func doUpload(ctx context.Context, rekorClient *client.Rekor, pe models.Proposed if err != nil { return nil, err } - return e, VerifyTLogEntry(ctx, nil, e) + rekorPubsFromAPI, err := rekorPubsFromClient(rekorClient) + if err != nil { + return nil, err + } + return e, VerifyTLogEntryOffline(e, rekorPubsFromAPI) } return nil, err } @@ -390,13 +404,17 @@ func FindTlogEntry(ctx context.Context, rekorClient *client.Rekor, return results, nil } -// VerityTLogEntry verifies a TLog entry. -// The argument *client.Rekor is unused and may be nil. -func VerifyTLogEntry(ctx context.Context, _ *client.Rekor, e *models.LogEntryAnon) error { +// VerifyTLogEntryOffline verifies a TLog entry against a map of trusted rekorPubKeys indexed +// by log id. +func VerifyTLogEntryOffline(e *models.LogEntryAnon, rekorPubKeys *TrustedRekorPubKeys) error { if e.Verification == nil || e.Verification.InclusionProof == nil { return errors.New("inclusion proof not provided") } + if rekorPubKeys == nil || rekorPubKeys.Keys == nil { + return errors.New("no trusted rekor public keys provided") + } + hashes := [][]byte{} for _, h := range e.Verification.InclusionProof.Hashes { hb, _ := hex.DecodeString(h) @@ -424,12 +442,7 @@ func VerifyTLogEntry(ctx context.Context, _ *client.Rekor, e *models.LogEntryAno LogID: *e.LogID, } - rekorPubKeys, err := GetRekorPubs(ctx, nil) - if err != nil { - return fmt.Errorf("unable to fetch Rekor public keys: %w", err) - } - - pubKey, ok := rekorPubKeys[payload.LogID] + pubKey, ok := rekorPubKeys.Keys[payload.LogID] if !ok { return errors.New("rekor log public key not found for payload") } @@ -442,3 +455,22 @@ func VerifyTLogEntry(ctx context.Context, _ *client.Rekor, e *models.LogEntryAno } return nil } + +func NewTrustedRekorPubKeys() TrustedRekorPubKeys { + return TrustedRekorPubKeys{Keys: make(map[string]RekorPubKey, 0)} +} + +// constructRekorPubkey returns a log ID and RekorPubKey from a given +// byte-array representing the PEM-encoded Rekor key and a status. +func (t *TrustedRekorPubKeys) AddRekorPubKey(pemBytes []byte, status tuf.StatusKind) error { + pubKey, err := PemToECDSAKey(pemBytes) + if err != nil { + return err + } + keyID, err := getLogID(pubKey) + if err != nil { + return err + } + t.Keys[keyID] = RekorPubKey{PubKey: pubKey, Status: status} + return nil +} diff --git a/pkg/cosign/tlog_test.go b/pkg/cosign/tlog_test.go index f6a60bdac71..68cda11d76d 100644 --- a/pkg/cosign/tlog_test.go +++ b/pkg/cosign/tlog_test.go @@ -20,15 +20,15 @@ import ( ) func TestGetRekorPubKeys(t *testing.T) { - keys, err := GetRekorPubs(context.Background(), nil) + keys, err := GetRekorPubs(context.Background()) if err != nil { t.Errorf("Unexpected error calling GetRekorPubs, expected nil: %v", err) } - if len(keys) == 0 { + if len(keys.Keys) == 0 { t.Errorf("expected 1 or more keys, got 0") } // check that the mapping of key digest to key is correct - for logID, key := range keys { + for logID, key := range keys.Keys { expectedLogID, err := getLogID(key.PubKey) if err != nil { t.Fatalf("unexpected error generated log ID: %v", err) diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 7ed36f0cf96..298528606ba 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -81,8 +81,13 @@ type CheckOpts struct { // ClaimVerifier, if provided, verifies claims present in the oci.Signature. ClaimVerifier func(sig oci.Signature, imageDigest v1.Hash, annotations map[string]interface{}) error - // RekorClient, if set, is used to use to verify signatures and public keys. + // RekorClient, if set, is used to make online tlog calls use to verify signatures and public keys. RekorClient *client.Rekor + // TrustedRekorPubKeys, if set, is used to validate signatures on log entries from Rekor. + // It is a map from log id to RekorPubKey. + // Note: The RekorPubKey values contains information like status along with the + // raw public key information. + RekorPubKeys *TrustedRekorPubKeys // SigVerifier is used to verify signatures. SigVerifier signature.Verifier @@ -445,8 +450,8 @@ func ValidateAndUnpackCertWithChain(cert *x509.Certificate, chain []*x509.Certif return ValidateAndUnpackCert(cert, co) } -func tlogValidateEntry(ctx context.Context, client *client.Rekor, sig oci.Signature, pem []byte) ( - *models.LogEntryAnon, error) { +func tlogValidateEntry(ctx context.Context, client *client.Rekor, rekorPubKeys *TrustedRekorPubKeys, + sig oci.Signature, pem []byte) (*models.LogEntryAnon, error) { b64sig, err := sig.Base64Signature() if err != nil { return nil, err @@ -469,7 +474,7 @@ func tlogValidateEntry(ctx context.Context, client *client.Rekor, sig oci.Signat entryVerificationErrs := make([]string, 0) for _, e := range tlogEntries { entry := e - if err := VerifyTLogEntry(ctx, nil, &entry); err != nil { + if err := VerifyTLogEntryOffline(&entry, rekorPubKeys); err != nil { entryVerificationErrs = append(entryVerificationErrs, err.Error()) continue } @@ -623,7 +628,7 @@ func verifyInternal(ctx context.Context, sig oci.Signature, h v1.Hash, var acceptableRFC3161Time, acceptableRekorBundleTime *time.Time // Timestamps for the signature we accept, or nil if not applicable. if co.TSACerts != nil { - acceptableRFC3161Timestamp, err := VerifyRFC3161Timestamp(ctx, sig, co.TSACerts) + acceptableRFC3161Timestamp, err := VerifyRFC3161Timestamp(sig, co.TSACerts) if err != nil { return false, fmt.Errorf("unable to verify RFC3161 timestamp bundle: %w", err) } @@ -633,7 +638,7 @@ func verifyInternal(ctx context.Context, sig oci.Signature, h v1.Hash, } if !co.SkipTlogVerify { - bundleVerified, err = VerifyBundle(ctx, sig, co, co.RekorClient) + bundleVerified, err = VerifyBundle(sig, co) if err != nil { return false, fmt.Errorf("error verifying bundle: %w", err) } @@ -662,7 +667,7 @@ func verifyInternal(ctx context.Context, sig oci.Signature, h v1.Hash, return false, err } - e, err := tlogValidateEntry(ctx, co.RekorClient, sig, pemBytes) + e, err := tlogValidateEntry(ctx, co.RekorClient, co.RekorPubKeys, sig, pemBytes) if err != nil { return false, err } @@ -952,7 +957,9 @@ func getBundleIntegratedTime(sig oci.Signature) (time.Time, error) { return time.Unix(bundle.Payload.IntegratedTime, 0), nil } -func VerifyBundle(ctx context.Context, sig oci.Signature, co *CheckOpts, rekorClient *client.Rekor) (bool, error) { +// This verifies an offline bundle contained in the sig against the trusted +// Rekor publicKeys. +func VerifyBundle(sig oci.Signature, co *CheckOpts) (bool, error) { bundle, err := sig.Bundle() if err != nil { return false, err @@ -960,6 +967,10 @@ func VerifyBundle(ctx context.Context, sig oci.Signature, co *CheckOpts, rekorCl return false, nil } + if co.RekorPubKeys == nil || co.RekorPubKeys.Keys == nil { + return false, errors.New("no trusted rekor public keys provided") + } + if err := compareSigs(bundle.Payload.Body.(string), sig); err != nil { return false, err } @@ -968,14 +979,9 @@ func VerifyBundle(ctx context.Context, sig oci.Signature, co *CheckOpts, rekorCl return false, err } - publicKeys, err := GetRekorPubs(ctx, nil) - if err != nil { - return false, fmt.Errorf("retrieving rekor public key: %w", err) - } - - pubKey, ok := publicKeys[bundle.Payload.LogID] + pubKey, ok := co.RekorPubKeys.Keys[bundle.Payload.LogID] if !ok { - return false, &VerificationError{"rekor log public key not found for payload"} + return false, &VerificationError{"verifying bundle: rekor log public key not found for payload"} } err = VerifySET(bundle.Payload, bundle.SignedEntryTimestamp, pubKey.PubKey) if err != nil { @@ -1006,12 +1012,12 @@ func VerifyBundle(ctx context.Context, sig oci.Signature, co *CheckOpts, rekorCl // VerifyRFC3161Timestamp verifies that the timestamp in untrustedSig is correctly signed, and if so, // returns the timestamp value. -// It returns (nil, nil) if there is no timestamp; or (nil, err) if there is an invalid timestamp. -func VerifyRFC3161Timestamp(ctx context.Context, sig oci.Signature, tsaCerts *x509.CertPool) (*timestamp.Timestamp, error) { - bundle, err := sig.RFC3161Timestamp() +// It returns (nil, nil) if there is no timestamp, or (nil, err) if there is an invalid timestamp. +func VerifyRFC3161Timestamp(sig oci.Signature, tsaCerts *x509.CertPool) (*timestamp.Timestamp, error) { + ts, err := sig.RFC3161Timestamp() if err != nil { return nil, err - } else if bundle == nil { + } else if ts == nil { return nil, nil } @@ -1020,21 +1026,28 @@ func VerifyRFC3161Timestamp(ctx context.Context, sig oci.Signature, tsaCerts *x5 return nil, fmt.Errorf("reading base64signature: %w", 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 nil, 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 nil, 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 nil, fmt.Errorf("unable to verify TimestampResponse: %w", err) } - acceptedTimestamp := bundle + acceptedTimestamp := ts // FIXME: tsaverification.VerifyTimestampResponse has done this parsing; we shouldn’t parse again. return timestamp.ParseResponse(acceptedTimestamp.SignedRFC3161Timestamp) diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index b3b6d208ad4..e7cfa9acc44 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -54,6 +54,7 @@ import ( "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/options" + "github.com/sigstore/sigstore/pkg/tuf" tsaclient "github.com/sigstore/timestamp-authority/pkg/client" "github.com/sigstore/timestamp-authority/pkg/server" "github.com/spf13/viper" @@ -253,18 +254,23 @@ func TestVerifyImageSignatureWithNoChain(t *testing.T) { entry, _ := rtypes.UnmarshalEntry(pe[0]) leaf, _ := entry.Canonicalize(ctx) rekorBundle := CreateTestBundle(ctx, t, sv, leaf) + pemBytes, _ := cryptoutils.MarshalPublicKeyToPEM(sv.Public()) + rekorPubKeys := NewTrustedRekorPubKeys() + rekorPubKeys.AddRekorPubKey(pemBytes, tuf.Active) opts := []static.Option{static.WithCertChain(pemLeaf, []byte{}), static.WithBundle(rekorBundle)} ociSig, _ := static.NewSignature(payload, base64.StdEncoding.EncodeToString(signature), opts...) - // TODO(asraa): Re-enable passing test when Rekor public keys can be set in CheckOpts, - // instead of relying on the singleton TUF instance. - verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{RootCerts: rootPool, IgnoreSCT: true}) - if err == nil { - t.Fatalf("expected error due to custom Rekor public key") + verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, + &CheckOpts{ + RootCerts: rootPool, + IgnoreSCT: true, + RekorPubKeys: &rekorPubKeys}) + if err != nil { + t.Fatalf("unexpected error %v", err) } - if verified == true { - t.Fatalf("expected verified=false, got verified=true") + if verified == false { + t.Fatalf("expected verified=true, got verified=false") } } @@ -392,6 +398,10 @@ func uuid(e models.LogEntryAnon) string { // This test ensures that image signature validation fails properly if we are // using a SigVerifier with Rekor. +// In other words, we require checking against RekorPubKeys when verifying +// image signature. +// This could be made more robust with supplying a mismatched trusted RekorPubKeys +// rather than none. // See https://github.com/sigstore/cosign/issues/1816 for more details. func TestVerifyImageSignatureWithSigVerifierAndRekor(t *testing.T) { sv, privKey, err := signature.NewDefaultECDSASignerVerifier() @@ -415,14 +425,10 @@ func TestVerifyImageSignatureWithSigVerifierAndRekor(t *testing.T) { if _, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{ SigVerifier: sv, RekorClient: mClient, - }); err == nil || !strings.Contains(err.Error(), "verifying inclusion proof") { - // TODO(wlynch): This is a weak test, since this is really failing because - // there is no inclusion proof for the Rekor entry rather than failing to - // validate the Rekor public key itself. At the very least this ensures - // that we're hitting tlog validation during signature checking, - // but we should look into improving this once there is an in-memory - // Rekor client that is capable of performing inclusion proof validation - // in unit tests. + }); err == nil || !strings.Contains(err.Error(), "no valid tlog entries found no trusted rekor public keys provided") { + // This is failing to validate the Rekor public key itself. + // At the very least this ensures + // that we're hitting tlog validation during signature checking. t.Fatalf("expected error while verifying signature, got %s", err) } } @@ -465,7 +471,7 @@ func TestVerifyImageSignatureWithSigVerifierAndTSA(t *testing.T) { SigVerifier: sv, TSACerts: tsaCertPool, SkipTlogVerify: true, - }); err != nil || bundleVerified { // Rekor bundle should not be verified with timestamp + }); err != nil || bundleVerified { // bundle is not verified since there's no Rekor bundle t.Fatalf("unexpected error while verifying signature, got %v", err) } } @@ -516,7 +522,7 @@ func TestVerifyImageSignatureWithSigVerifierAndRekorTSA(t *testing.T) { SigVerifier: sv, TSACerts: tsaCertPool, RekorClient: mClient, - }); err == nil || !strings.Contains(err.Error(), "verifying inclusion proof") { + }); err == nil || !strings.Contains(err.Error(), "no trusted rekor public keys provided") { // TODO(wlynch): This is a weak test, since this is really failing because // there is no inclusion proof for the Rekor entry rather than failing to // validate the Rekor public key itself. At the very least this ensures @@ -1287,3 +1293,94 @@ 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 + ts, err := VerifyRFC3161Timestamp(ociSig, pool) + if err != nil { + t.Fatalf("unexpected error verifying timestamp with signature: %v", err) + } + if err := CheckExpiry(leafCert, ts.Time); err != nil { + t.Fatalf("unexpected error using time from timestamp to verify certificate: %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) + } +} diff --git a/release/cloudbuild.yaml b/release/cloudbuild.yaml index c657a9d3e12..e0942ed6eee 100644 --- a/release/cloudbuild.yaml +++ b/release/cloudbuild.yaml @@ -39,10 +39,10 @@ steps: - TUF_ROOT=/tmp args: - 'verify' - - 'ghcr.io/gythialy/golang-cross:v1.19.3-0@sha256:1072190e76d68f455f1bedb7430a633916b6629a722c42246037ac518fdb0ff2' + - 'ghcr.io/gythialy/golang-cross:v1.19.4-0@sha256:53ee894818ac14377996a6fe7c8fe6156d018a20f82aaf69f2519fc45d897bec' # maybe we can build our own image and use that to be more in a safe side -- name: ghcr.io/gythialy/golang-cross:v1.19.3-0@sha256:1072190e76d68f455f1bedb7430a633916b6629a722c42246037ac518fdb0ff2 +- name: ghcr.io/gythialy/golang-cross:v1.19.4-0@sha256:53ee894818ac14377996a6fe7c8fe6156d018a20f82aaf69f2519fc45d897bec entrypoint: /bin/sh dir: "go/src/sigstore/cosign" env: @@ -65,7 +65,7 @@ steps: gcloud auth configure-docker \ && make release -- name: ghcr.io/gythialy/golang-cross:v1.19.3-0@sha256:1072190e76d68f455f1bedb7430a633916b6629a722c42246037ac518fdb0ff2 +- name: ghcr.io/gythialy/golang-cross:v1.19.4-0@sha256:53ee894818ac14377996a6fe7c8fe6156d018a20f82aaf69f2519fc45d897bec entrypoint: 'bash' dir: "go/src/sigstore/cosign" env: 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)