Repository: sigstore/cosign
Pull request #1305 (2022-01-12 21:51:26)

Provide certificate flags to all verify commands
Small refactor to provide the cert and cert-email
flags to all verify commands. cert-email will be
optionally used to when verifying a Fulcio certificate
to pin the cert identity. This refactor makes it easier
to add additional cert validations.

Also removed Fulcio flags from attestation verification, as they were unused.

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

<!--
Thanks for opening a pull request!

Please remember to:
- mention any issue(s) that this PR closes using a closing keyword as well as the issue number, such as "Closes #XYZ" or "Resolves sigstore/repo-name#XYZ", cf.
  [documentation](https://docs.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)
- ensure your commits are signed-off, as sigstore uses the [DCO](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin) using `git commit -s`, or `git commit -s --amend` if you want to amend already existing commits
- lastly, ensure there are no merge commits!
Thank you :)
-->

#### Summary
<!--
A description of what this pull request does, as well as QA test steps (if applicable and if not already added to the Jira ticket).
-->

#### Ticket Link
<!--
If this pull request addresses a Help Wanted ticket, please link the relevant GitHub issue, e.g.

  Fixes https://github.com/sigstore/YYYYYY/issues/XXXXX

-->

#### Release Note
<!--
Add a release note for each of the following conditions:

* Config changes (additions, deletions, updates)
* API additions—new endpoint, new response fields, or newly accepted request parameters
* Database changes (any)
* Websocket additions or changes
* Anything noteworthy to a Mattermost instance administrator (err on the side of over-communicating)
* New features and improvements, including behavioural changes, UI changes and CLI changes
* Bug fixes and fixes of previous known issues
* Deprecation warnings, breaking changes, or compatibility notes

If no release notes are required write NONE. Use past-tense.

-->
```release-note
* Added support for providing a local or hosted certificate to verify attestations
* All verify commands now can optionally validate the Fulcio certificate email
```



Patch:
diff --git a/cmd/cosign/cli/dockerfile.go b/cmd/cosign/cli/dockerfile.go
index 8a61bad3..70fe8322 100644
--- a/cmd/cosign/cli/dockerfile.go
+++ b/cmd/cosign/cli/dockerfile.go
@@ -88,7 +88,8 @@ Shell-like variables in the Dockerfile's FROM lines will be substituted with val
 					RegistryOptions: o.Registry,
 					CheckClaims:     o.CheckClaims,
 					KeyRef:          o.Key,
-					CertEmail:       o.CertEmail,
+					CertRef:         o.CertVerify.Cert,
+					CertEmail:       o.CertVerify.CertEmail,
 					Sk:              o.SecurityKey.Use,
 					Slot:            o.SecurityKey.Slot,
 					Output:          o.Output,
diff --git a/cmd/cosign/cli/manifest.go b/cmd/cosign/cli/manifest.go
index 7c662eff..65cbcc43 100644
--- a/cmd/cosign/cli/manifest.go
+++ b/cmd/cosign/cli/manifest.go
@@ -83,7 +83,8 @@ against the transparency log.`,
 					RegistryOptions: o.Registry,
 					CheckClaims:     o.CheckClaims,
 					KeyRef:          o.Key,
-					CertEmail:       o.CertEmail,
+					CertRef:         o.CertVerify.Cert,
+					CertEmail:       o.CertVerify.CertEmail,
 					Sk:              o.SecurityKey.Use,
 					Slot:            o.SecurityKey.Slot,
 					Output:          o.Output,
diff --git a/cmd/cosign/cli/options/certificate.go b/cmd/cosign/cli/options/certificate.go
new file mode 100644
index 00000000..2335e660
--- /dev/null
+++ b/cmd/cosign/cli/options/certificate.go
@@ -0,0 +1,36 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package options
+
+import (
+	"github.com/spf13/cobra"
+)
+
+// CertVerifyOptions is the wrapper for certificate verification.
+type CertVerifyOptions struct {
+	Cert      string
+	CertEmail string
+}
+
+var _ Interface = (*RekorOptions)(nil)
+
+// AddFlags implements Interface
+func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) {
+	cmd.Flags().StringVar(&o.Cert, "cert", "",
+		"path to the public certificate")
+
+	cmd.Flags().StringVar(&o.CertEmail, "cert-email", "",
+		"the email expected in a valid Fulcio certificate")
+}
diff --git a/cmd/cosign/cli/options/verify.go b/cmd/cosign/cli/options/verify.go
index 0d76aee3..88ab3f77 100644
--- a/cmd/cosign/cli/options/verify.go
+++ b/cmd/cosign/cli/options/verify.go
@@ -22,17 +22,15 @@ import (
 // VerifyOptions is the top level wrapper for the `verify` command.
 type VerifyOptions struct {
 	Key          string
-	Cert         string
-	CertEmail    string // TODO: merge into fulcio option as read mode?
 	CheckClaims  bool
 	Attachment   string
 	Output       string
 	SignatureRef string
 	LocalImage   bool
 
-	SecurityKey SecurityKeyOptions
-	Rekor       RekorOptions
-	// TODO: this seems like it should have the Fulcio options.
+	SecurityKey     SecurityKeyOptions
+	CertVerify      CertVerifyOptions
+	Rekor           RekorOptions
 	Registry        RegistryOptions
 	SignatureDigest SignatureDigestOptions
 	AnnotationOptions
@@ -44,6 +42,7 @@ var _ Interface = (*VerifyOptions)(nil)
 func (o *VerifyOptions) AddFlags(cmd *cobra.Command) {
 	o.SecurityKey.AddFlags(cmd)
 	o.Rekor.AddFlags(cmd)
+	o.CertVerify.AddFlags(cmd)
 	o.Registry.AddFlags(cmd)
 	o.SignatureDigest.AddFlags(cmd)
 	o.AnnotationOptions.AddFlags(cmd)
@@ -51,12 +50,6 @@ func (o *VerifyOptions) AddFlags(cmd *cobra.Command) {
 	cmd.Flags().StringVar(&o.Key, "key", "",
 		"path to the public key file, KMS URI or Kubernetes Secret")
 
-	cmd.Flags().StringVar(&o.Cert, "cert", "",
-		"path to the public certificate")
-
-	cmd.Flags().StringVar(&o.CertEmail, "cert-email", "",
-		"the email expected in a valid fulcio cert")
-
 	cmd.Flags().BoolVar(&o.CheckClaims, "check-claims", true,
 		"whether to check the claims found")
 
@@ -81,7 +74,7 @@ type VerifyAttestationOptions struct {
 
 	SecurityKey SecurityKeyOptions
 	Rekor       RekorOptions
-	Fulcio      FulcioOptions // TODO: the original command did not use id token, mistake?
+	CertVerify  CertVerifyOptions
 	Registry    RegistryOptions
 	Predicate   PredicateRemoteOptions
 	Policies    []string
@@ -94,7 +87,7 @@ var _ Interface = (*VerifyAttestationOptions)(nil)
 func (o *VerifyAttestationOptions) AddFlags(cmd *cobra.Command) {
 	o.SecurityKey.AddFlags(cmd)
 	o.Rekor.AddFlags(cmd)
-	o.Fulcio.AddFlags(cmd)
+	o.CertVerify.AddFlags(cmd)
 	o.Registry.AddFlags(cmd)
 	o.Predicate.AddFlags(cmd)
 
@@ -117,10 +110,10 @@ func (o *VerifyAttestationOptions) AddFlags(cmd *cobra.Command) {
 // VerifyBlobOptions is the top level wrapper for the `verify blob` command.
 type VerifyBlobOptions struct {
 	Key       string
-	Cert      string
 	Signature string
 
 	SecurityKey SecurityKeyOptions
+	CertVerify  CertVerifyOptions
 	Rekor       RekorOptions
 	Registry    RegistryOptions
 }
@@ -131,14 +124,12 @@ var _ Interface = (*VerifyBlobOptions)(nil)
 func (o *VerifyBlobOptions) AddFlags(cmd *cobra.Command) {
 	o.SecurityKey.AddFlags(cmd)
 	o.Rekor.AddFlags(cmd)
+	o.CertVerify.AddFlags(cmd)
 	o.Registry.AddFlags(cmd)
 
 	cmd.Flags().StringVar(&o.Key, "key", "",
 		"path to the public key file, KMS URI or Kubernetes Secret")
 
-	cmd.Flags().StringVar(&o.Cert, "cert", "",
-		"path to the public certificate")
-
 	cmd.Flags().StringVar(&o.Signature, "signature", "",
 		"signature content or path or remote URL")
 }
diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go
index 5cbd0679..550f4cb1 100644
--- a/cmd/cosign/cli/sign/sign.go
+++ b/cmd/cosign/cli/sign/sign.go
@@ -205,7 +205,9 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko KeyO
 
 	var s icos.Signer
 	s = ipayload.NewSigner(sv)
-	s = ifulcio.NewSigner(s, sv.Cert, sv.Chain)
+	if sv.Cert != nil {
+		s = ifulcio.NewSigner(s, sv.Cert, sv.Chain)
+	}
 	if ShouldUploadToTlog(ctx, digest, force, ko.RekorURL) {
 		rClient, err := rekor.NewClient(ko.RekorURL)
 		if err != nil {
diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go
index cd0e2559..90f66f19 100644
--- a/cmd/cosign/cli/verify.go
+++ b/cmd/cosign/cli/verify.go
@@ -90,8 +90,8 @@ against the transparency log.`,
 				RegistryOptions: o.Registry,
 				CheckClaims:     o.CheckClaims,
 				KeyRef:          o.Key,
-				CertRef:         o.Cert,
-				CertEmail:       o.CertEmail,
+				CertRef:         o.CertVerify.Cert,
+				CertEmail:       o.CertVerify.CertEmail,
 				Sk:              o.SecurityKey.Use,
 				Slot:            o.SecurityKey.Slot,
 				Output:          o.Output,
@@ -159,12 +159,13 @@ against the transparency log.`,
 			v := verify.VerifyAttestationCommand{
 				RegistryOptions: o.Registry,
 				CheckClaims:     o.CheckClaims,
+				CertRef:         o.CertVerify.Cert,
+				CertEmail:       o.CertVerify.CertEmail,
 				KeyRef:          o.Key,
 				Sk:              o.SecurityKey.Use,
 				Slot:            o.SecurityKey.Slot,
 				Output:          o.Output,
 				RekorURL:        o.Rekor.URL,
-				FulcioURL:       o.Fulcio.URL,
 				PredicateType:   o.Predicate.Type,
 				Policies:        o.Policies,
 				LocalImage:      o.LocalImage,
@@ -236,7 +237,8 @@ The blob may be specified as a path to a file or - for stdin.`,
 				Slot:     o.SecurityKey.Slot,
 				RekorURL: o.Rekor.URL,
 			}
-			if err := verify.VerifyBlobCmd(cmd.Context(), ko, o.Cert, o.Signature, args[0]); err != nil {
+			if err := verify.VerifyBlobCmd(cmd.Context(), ko, o.CertVerify.Cert,
+				o.CertVerify.CertEmail, o.Signature, args[0]); err != nil {
 				return errors.Wrapf(err, "verifying blob %s", args)
 			}
 			return nil
diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go
index cebc3624..b38bca25 100644
--- a/cmd/cosign/cli/verify/verify_blob.go
+++ b/cmd/cosign/cli/verify/verify_blob.go
@@ -54,8 +54,8 @@ func isb64(data []byte) bool {
 }
 
 // nolint
-func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, sigRef, blobRef string) error {
-	var pubKey signature.Verifier
+func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, sigRef, blobRef string) error {
+	var verifier signature.Verifier
 	var cert *x509.Certificate
 
 	if !options.OneOf(ko.KeyRef, ko.Sk, certRef) && !options.EnableExperimental() {
@@ -75,11 +75,11 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, sigRef, blobRe
 	// Keys are optional!
 	switch {
 	case ko.KeyRef != "":
-		pubKey, err = sigs.PublicKeyFromKeyRef(ctx, ko.KeyRef)
+		verifier, err = sigs.PublicKeyFromKeyRef(ctx, ko.KeyRef)
 		if err != nil {
 			return errors.Wrap(err, "loading public key")
 		}
-		pkcs11Key, ok := pubKey.(*pkcs11key.Key)
+		pkcs11Key, ok := verifier.(*pkcs11key.Key)
 		if ok {
 			defer pkcs11Key.Close()
 		}
@@ -89,7 +89,7 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, sigRef, blobRe
 			return errors.Wrap(err, "opening piv token")
 		}
 		defer sk.Close()
-		pubKey, err = sk.Verifier()
+		verifier, err = sk.Verifier()
 		if err != nil {
 			return errors.Wrap(err, "loading public key from token")
 		}
@@ -98,7 +98,7 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, sigRef, blobRe
 		if err != nil {
 			return err
 		}
-		pubKey, err = signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256)
+		verifier, err = signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256)
 		if err != nil {
 			return err
 		}
@@ -126,25 +126,25 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, sigRef, blobRe
 		if err != nil {
 			return err
 		}
+
+		co := &cosign.CheckOpts{
+			RootCerts: fulcio.GetRoots(),
+			CertEmail: certEmail,
+		}
 		cert = certs[0]
-		pubKey, err = signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256)
+		verifier, err = cosign.ValidateAndUnpackCert(cert, co)
 		if err != nil {
 			return err
 		}
 	}
 
 	// verify the signature
-	if err := pubKey.VerifySignature(bytes.NewReader([]byte(sig)), bytes.NewReader(blobBytes)); err != nil {
-		return err
-	}
-
-	// verify the cert
-	if err := verifyCert(cert); err != nil {
+	if err := verifier.VerifySignature(bytes.NewReader([]byte(sig)), bytes.NewReader(blobBytes)); err != nil {
 		return err
 	}
 
 	// verify the rekor entry
-	if err := verifyRekorEntry(ctx, ko, pubKey, cert, b64sig, blobBytes); err != nil {
+	if err := verifyRekorEntry(ctx, ko, verifier, cert, b64sig, blobBytes); err != nil {
 		return err
 	}
 
@@ -196,22 +196,6 @@ func payloadBytes(blobRef string) ([]byte, error) {
 	return blobBytes, nil
 }
 
-func verifyCert(cert *x509.Certificate) error {
-	if cert == nil {
-		return nil
-	}
-	if err := cosign.TrustedCert(cert, fulcio.GetRoots()); err != nil {
-		return err
-	}
-	fmt.Fprintln(os.Stderr, "Certificate is trusted by Fulcio Root CA")
-	fmt.Fprintln(os.Stderr, "Email:", cert.EmailAddresses)
-	for _, uri := range cert.URIs {
-		fmt.Fprintf(os.Stderr, "URI: %s://%s%s\n", uri.Scheme, uri.Host, uri.Path)
-	}
-	fmt.Fprintln(os.Stderr, "Issuer: ", sigs.CertIssuerExtension(cert))
-	return nil
-}
-
 func verifyRekorEntry(ctx context.Context, ko sign.KeyOpts, pubKey signature.Verifier, cert *x509.Certificate, b64sig string, blobBytes []byte) error {
 	if !options.EnableExperimental() {
 		return nil
diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md
index d2438ba3..12aa7bb2 100644
--- a/doc/cosign_dockerfile_verify.md
+++ b/doc/cosign_dockerfile_verify.md
@@ -58,7 +58,7 @@ cosign dockerfile verify [flags]
       --attachment-tag-prefix [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName]   optional custom prefix to use for attached image tags. Attachment images are tagged as: [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName]
       --base-image-only                                                                          only verify the base image (the last FROM image in the Dockerfile)
       --cert string                                                                              path to the public certificate
-      --cert-email string                                                                        the email expected in a valid fulcio cert
+      --cert-email string                                                                        the email expected in a valid Fulcio certificate
       --check-claims                                                                             whether to check the claims found (default true)
   -h, --help                                                                                     help for verify
       --k8s-keychain                                                                             whether to use the kubernetes keychain instead of the default keychain (supports workload identity).
diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md
index e1704fdb..8c954459 100644
--- a/doc/cosign_manifest_verify.md
+++ b/doc/cosign_manifest_verify.md
@@ -52,7 +52,7 @@ cosign manifest verify [flags]
       --attachment string                                                                        related image attachment to sign (sbom), default none
       --attachment-tag-prefix [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName]   optional custom prefix to use for attached image tags. Attachment images are tagged as: [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName]
       --cert string                                                                              path to the public certificate
-      --cert-email string                                                                        the email expected in a valid fulcio cert
+      --cert-email string                                                                        the email expected in a valid Fulcio certificate
       --check-claims                                                                             whether to check the claims found (default true)
   -h, --help                                                                                     help for verify
       --k8s-keychain                                                                             whether to use the kubernetes keychain instead of the default keychain (supports workload identity).
diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md
index 5769df3c..c4e0bf6c 100644
--- a/doc/cosign_verify-blob.md
+++ b/doc/cosign_verify-blob.md
@@ -64,6 +64,7 @@ cosign verify-blob [flags]
       --allow-insecure-registry                                                                  whether to allow insecure connections to registries. Don't use this for anything but testing
       --attachment-tag-prefix [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName]   optional custom prefix to use for attached image tags. Attachment images are tagged as: [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName]
       --cert string                                                                              path to the public certificate
+      --cert-email string                                                                        the email expected in a valid Fulcio certificate
   -h, --help                                                                                     help for verify-blob
       --k8s-keychain                                                                             whether to use the kubernetes keychain instead of the default keychain (supports workload identity).
       --key string                                                                               path to the public key file, KMS URI or Kubernetes Secret
diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md
index 683f8c06..8deb69a2 100644
--- a/doc/cosign_verify.md
+++ b/doc/cosign_verify.md
@@ -65,7 +65,7 @@ cosign verify [flags]
       --attachment string                                                                        related image attachment to sign (sbom), default none
       --attachment-tag-prefix [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName]   optional custom prefix to use for attached image tags. Attachment images are tagged as: [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName]
       --cert string                                                                              path to the public certificate
-      --cert-email string                                                                        the email expected in a valid fulcio cert
+      --cert-email string                                                                        the email expected in a valid Fulcio certificate
       --check-claims                                                                             whether to check the claims found (default true)
   -h, --help                                                                                     help for verify
       --k8s-keychain                                                                             whether to use the kubernetes keychain instead of the default keychain (supports workload identity).
diff --git a/go.mod b/go.mod
index 8be609b0..f07cea8a 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.16
 
 require (
 	cloud.google.com/go/storage v1.18.2
-	cuelang.org/go v0.4.0
+	cuelang.org/go v0.4.1
 	github.com/ThalesIgnite/crypto11 v1.2.5
 	github.com/cyberphone/json-canonicalization v0.0.0-20210823021906-dc406ceaf94b
 	github.com/go-openapi/runtime v0.21.0
diff --git a/go.sum b/go.sum
index e89e139e..a7734f12 100644
--- a/go.sum
+++ b/go.sum
@@ -91,8 +91,8 @@ contrib.go.opencensus.io/exporter/zipkin v0.1.2/go.mod h1:mP5xM3rrgOjpn79MM8fZbj
 contrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE=
 contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE=
 contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA=
-cuelang.org/go v0.4.0 h1:GLJblw6m2WGGCA3k1v6Wbk9gTOt2qto48ahO2MmSd6I=
-cuelang.org/go v0.4.0/go.mod h1:tz/edkPi+T37AZcb5GlPY+WJkL6KiDlDVupKwL3vvjs=
+cuelang.org/go v0.4.1 h1:rxG2cyZzdAymrNICI0L/wagK2EitoR8Xsiiq2D7qkaQ=
+cuelang.org/go v0.4.1/go.mod h1:P09/R4UfAEzLkV9DXxwlxQnIZbkaT4uIhiEgs6Vsz2Q=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/AdaLogics/go-fuzz-headers v0.0.0-20211102141018-f7be0cbad29c/go.mod h1:WpB7kf89yJUETZxQnP1kgYPNwlT2jjdDYUCoxVggM3g=
@@ -1011,6 +1011,7 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLe
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg=
 github.com/google/rpmpack v0.0.0-20210518075352-dc539ef4f2ea/go.mod h1:+y9lKiqDhR4zkLl+V9h4q0rdyrYVsWWm6LLCQP33DIk=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
 github.com/google/trillian v1.3.14-0.20210409160123-c5ea3abd4a41/go.mod h1:1dPv0CUjNQVFEDuAUFhZql16pw/VlPgaX8qj+g5pVzQ=
 github.com/google/trillian v1.3.14-0.20210511103300-67b5f349eefa/go.mod h1:s4jO3Ai4NSvxucdvqUHON0bCqJyoya32eNw6XJwsmNc=
@@ -2686,7 +2687,6 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/pkg/cosign/fulcio/signer.go b/internal/pkg/cosign/fulcio/signer.go
index 18ba60c0..3dbfb3ac 100644
--- a/internal/pkg/cosign/fulcio/signer.go
+++ b/internal/pkg/cosign/fulcio/signer.go
@@ -20,6 +20,7 @@ import (
 	"io"
 
 	"github.com/sigstore/cosign/internal/pkg/cosign"
+	"github.com/sigstore/cosign/pkg/cosign/tuf"
 	"github.com/sigstore/cosign/pkg/oci"
 	"github.com/sigstore/cosign/pkg/oci/mutate"
 )
@@ -41,8 +42,13 @@ func (fs *signerWrapper) Sign(ctx context.Context, payload io.Reader) (oci.Signa
 		return nil, nil, err
 	}
 
+	timestamp, err := tuf.GetTimestamp(ctx)
+	if err != nil {
+		return nil, nil, err
+	}
+
 	// TODO(dekkagaijin): move the fulcio SignerVerifier logic here
-	newSig, err := mutate.Signature(sig, mutate.WithCertChain(fs.cert, fs.chain))
+	newSig, err := mutate.Signature(sig, mutate.WithCertChain(fs.cert, fs.chain), mutate.WithTimestamp(timestamp))
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/internal/pkg/cosign/rekor/mock_rekor_client.go b/internal/pkg/cosign/rekor/mock_rekor_client.go
new file mode 100644
index 00000000..23facc2f
--- /dev/null
+++ b/internal/pkg/cosign/rekor/mock_rekor_client.go
@@ -0,0 +1,64 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package rekor
+
+import (
+	"github.com/go-openapi/runtime"
+	"github.com/sigstore/rekor/pkg/generated/client/entries"
+	"github.com/sigstore/rekor/pkg/generated/models"
+)
+
+// Client that implements entries.ClientService for Rekor
+// To use:
+// var mClient client.Rekor
+// mClient.entries = &MockEntriesClient{}
+type MockEntriesClient struct {
+}
+
+func (m *MockEntriesClient) CreateLogEntry(params *entries.CreateLogEntryParams, opts ...entries.ClientOption) (*entries.CreateLogEntryCreated, error) {
+	return &entries.CreateLogEntryCreated{
+		ETag:     "",
+		Location: "",
+		Payload: map[string]models.LogEntryAnon{
+			"sdf": {
+				Attestation:    &models.LogEntryAnonAttestation{},
+				Body:           nil,
+				IntegratedTime: new(int64),
+				LogID:          new(string),
+				LogIndex:       new(int64),
+				Verification:   &models.LogEntryAnonVerification{},
+			},
+		},
+	}, nil
+}
+
+// TODO: Implement mock
+func (m *MockEntriesClient) GetLogEntryByIndex(params *entries.GetLogEntryByIndexParams, opts ...entries.ClientOption) (*entries.GetLogEntryByIndexOK, error) {
+	return nil, nil
+}
+
+// TODO: Implement mock
+func (m *MockEntriesClient) GetLogEntryByUUID(params *entries.GetLogEntryByUUIDParams, opts ...entries.ClientOption) (*entries.GetLogEntryByUUIDOK, error) {
+	return nil, nil
+}
+
+// TODO: Implement mock
+func (m *MockEntriesClient) SearchLogQuery(params *entries.SearchLogQueryParams, opts ...entries.ClientOption) (*entries.SearchLogQueryOK, error) {
+	return nil, nil
+}
+
+// TODO: Implement mock
+func (m *MockEntriesClient) SetTransport(transport runtime.ClientTransport) {
+}
diff --git a/internal/pkg/cosign/rekor/signer.go b/internal/pkg/cosign/rekor/signer.go
index 9293797d..51b185ef 100644
--- a/internal/pkg/cosign/rekor/signer.go
+++ b/internal/pkg/cosign/rekor/signer.go
@@ -25,6 +25,7 @@ import (
 	"github.com/sigstore/cosign/internal/pkg/cosign"
 	cosignv1 "github.com/sigstore/cosign/pkg/cosign"
 	cbundle "github.com/sigstore/cosign/pkg/cosign/bundle"
+	"github.com/sigstore/cosign/pkg/cosign/tuf"
 	"github.com/sigstore/cosign/pkg/oci"
 	"github.com/sigstore/cosign/pkg/oci/mutate"
 
@@ -96,7 +97,12 @@ func (rs *signerWrapper) Sign(ctx context.Context, payload io.Reader) (oci.Signa
 		return nil, nil, err
 	}
 
-	newSig, err := mutate.Signature(sig, mutate.WithBundle(bundle))
+	timestamp, err := tuf.GetTimestamp(ctx)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	newSig, err := mutate.Signature(sig, mutate.WithBundle(bundle), mutate.WithTimestamp(timestamp))
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/pkg/cosign/tuf/client.go b/pkg/cosign/tuf/client.go
index 8ca95d1b..124133c0 100644
--- a/pkg/cosign/tuf/client.go
+++ b/pkg/cosign/tuf/client.go
@@ -45,16 +45,12 @@ const (
 type TUF struct {
 	client  *client.Client
 	targets targetImpl
-	close   func() error
+	local   client.LocalStore
 }
 
-// We have to close the local storage passed into the tuf.Client object, but tuf.Client doesn't expose a
-// Close method. So we capture the method of the inner local storage and close that.
+// Close closes the local TUF store. Should only be called once per client.
 func (t *TUF) Close() error {
-	if t.close != nil {
-		return t.close()
-	}
-	return nil
+	return t.local.Close()
 }
 
 func NewFromEnv(ctx context.Context) (*TUF, error) {
@@ -103,9 +99,8 @@ func New(ctx context.Context, remote client.RemoteStore, cacheRoot string) (*TUF
 		t.targets = newFileImpl()
 	}
 
+	t.local = local
 	t.client = client.NewClient(local, remote)
-	// Capture the Close method on the local storage object so we can close it.
-	t.close = local.Close
 	trustedMeta, err := local.GetMeta()
 	if err != nil {
 		return nil, errors.Wrap(err, "getting trusted meta")
@@ -114,7 +109,7 @@ func New(ctx context.Context, remote client.RemoteStore, cacheRoot string) (*TUF
 	// We have our local store, whether it was embedded or not!
 	// Now check to see if it needs to be updated.
 	trustedTimestamp, ok := trustedMeta["timestamp.json"]
-	if ok && !isExpiredMetadata(trustedTimestamp) {
+	if ok && !isExpiredTimestamp(trustedTimestamp) {
 		return t, nil
 	}
 
@@ -179,6 +174,7 @@ func Initialize(remote client.RemoteStore, root []byte) error {
 	if err := c.Init(rootKeys, rootThreshold); err != nil {
 		return errors.Wrap(err, "initializing root")
 	}
+	// Timestamp does not need to be saved in memory on Initialize
 	if err := updateMetadataAndDownloadTargets(c, newFileImpl()); err != nil {
 		return errors.Wrap(err, "updating local metadata and targets")
 	}
@@ -208,6 +204,18 @@ func (t *TUF) GetTarget(name string) ([]byte, error) {
 	return targetBytes, nil
 }
 
+func (t *TUF) GetTimestamp() ([]byte, error) {
+	trustedMeta, err := t.local.GetMeta()
+	if err != nil {
+		return nil, errors.Wrap(err, "getting trusted meta")
+	}
+	timestamp, ok := trustedMeta["timestamp.json"]
+	if !ok || len(timestamp) == 0 {
+		return nil, errors.New("unable to get TUF timestamp")
+	}
+	return timestamp, nil
+}
+
 func localStore(cacheRoot string) (client.LocalStore, error) {
 	local, err := tuf_leveldbstore.FileLocalStore(cacheRoot)
 	if err != nil {
@@ -233,24 +241,18 @@ func embeddedLocalStore() (client.LocalStore, error) {
 //go:embed repository
 var embeddedRootRepo embed.FS
 
-var isExpiredMetadata = func(metadata []byte) bool {
+var isExpiredTimestamp = func(metadata []byte) bool {
 	s := &data.Signed{}
 	if err := json.Unmarshal(metadata, s); err != nil {
 		return true
 	}
-	sm := &signedMeta{}
+	sm := &data.Timestamp{}
 	if err := json.Unmarshal(s.Signed, sm); err != nil {
 		return true
 	}
 	return time.Until(sm.Expires) <= 0
 }
 
-type signedMeta struct {
-	Type    string    `json:"_type"`
-	Expires time.Time `json:"expires"`
-	Version int       `json:"version"`
-}
-
 func getRootKeys(rootFileBytes []byte) ([]*data.PublicKey, int, error) {
 	store := gtuf.MemoryStore(map[string]json.RawMessage{"root.json": rootFileBytes}, nil)
 	repo, err := gtuf.NewRepo(store)
diff --git a/pkg/cosign/tuf/timestamp.go b/pkg/cosign/tuf/timestamp.go
new file mode 100644
index 00000000..57c35aaa
--- /dev/null
+++ b/pkg/cosign/tuf/timestamp.go
@@ -0,0 +1,47 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tuf
+
+import (
+	"context"
+	"encoding/json"
+
+	"github.com/pkg/errors"
+	"github.com/theupdateframework/go-tuf/data"
+)
+
+// GetTimestamp fetches the TUF timestamp metadata to be bundled
+// with the OCI signature.
+func GetTimestamp(ctx context.Context) (*Timestamp, error) {
+	tuf, err := NewFromEnv(ctx)
+	if err != nil {
+		return nil, err
+	}
+	defer tuf.Close()
+	tsBytes, err := tuf.GetTimestamp()
+	if err != nil {
+		return nil, err
+	}
+	var timestamp Timestamp
+	if err := json.Unmarshal(tsBytes, &timestamp); err != nil {
+		return nil, errors.Wrap(err, "unable to unmarshal timestamp")
+	}
+	return &timestamp, nil
+}
+
+type Timestamp struct {
+	Signatures []data.Signature `json:"signatures"`
+	Signed     data.Timestamp   `json:"signed"`
+}
diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go
index fbc4866e..1dcf110c 100644
--- a/pkg/cosign/verify.go
+++ b/pkg/cosign/verify.go
@@ -136,7 +136,9 @@ func verifyOCIAttestation(_ context.Context, verifier signature.Verifier, att pa
 	return err
 }
 
-func validateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Verifier, error) {
+// ValidateAndUnpackCert creates a Verifier from a certificate. Veries that the certificate
+// chains up to a trusted root. Optionally verifies the subject of the certificate.
+func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Verifier, error) {
 	verifier, err := signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256)
 	if err != nil {
 		return nil, errors.Wrap(err, "invalid certificate found on signature")
@@ -315,7 +317,7 @@ func verifySignatures(ctx context.Context, sigs oci.Signatures, h v1.Hash, co *C
 				if cert == nil {
 					return errors.New("no certificate found on signature")
 				}
-				verifier, err = validateAndUnpackCert(cert, co)
+				verifier, err = ValidateAndUnpackCert(cert, co)
 				if err != nil {
 					return err
 				}
@@ -490,7 +492,7 @@ func verifyImageAttestations(ctx context.Context, atts oci.Signatures, h v1.Hash
 				if cert == nil {
 					return errors.New("no certificate found on attestation")
 				}
-				verifier, err = validateAndUnpackCert(cert, co)
+				verifier, err = ValidateAndUnpackCert(cert, co)
 				if err != nil {
 					return err
 				}
diff --git a/pkg/oci/internal/signature/layer.go b/pkg/oci/internal/signature/layer.go
index e042ce1a..e84cbd28 100644
--- a/pkg/oci/internal/signature/layer.go
+++ b/pkg/oci/internal/signature/layer.go
@@ -25,15 +25,17 @@ import (
 	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/pkg/errors"
 	"github.com/sigstore/cosign/pkg/cosign/bundle"
+	"github.com/sigstore/cosign/pkg/cosign/tuf"
 	"github.com/sigstore/cosign/pkg/oci"
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
 )
 
 const (
-	sigkey    = "dev.cosignproject.cosign/signature"
-	certkey   = "dev.sigstore.cosign/certificate"
-	chainkey  = "dev.sigstore.cosign/chain"
-	BundleKey = "dev.sigstore.cosign/bundle"
+	sigkey       = "dev.cosignproject.cosign/signature"
+	certkey      = "dev.sigstore.cosign/certificate"
+	chainkey     = "dev.sigstore.cosign/chain"
+	BundleKey    = "dev.sigstore.cosign/bundle"
+	TimestampKey = "dev.sigstore.cosign/timestamp"
 )
 
 type sigLayer struct {
@@ -116,3 +118,16 @@ func (s *sigLayer) Bundle() (*bundle.RekorBundle, error) {
 	}
 	return &b, nil
 }
+
+// Timestamp implements oci.Signature
+func (s *sigLayer) Timestamp() (*tuf.Timestamp, error) {
+	timestamp := s.desc.Annotations[TimestampKey]
+	if timestamp == "" {
+		return nil, nil
+	}
+	var ts tuf.Timestamp
+	if err := json.Unmarshal([]byte(timestamp), &ts); err != nil {
+		return nil, errors.Wrap(err, "unmarshaling timestamp")
+	}
+	return &ts, nil
+}
diff --git a/pkg/oci/mutate/options.go b/pkg/oci/mutate/options.go
index 0c19f3da..7d55ea94 100644
--- a/pkg/oci/mutate/options.go
+++ b/pkg/oci/mutate/options.go
@@ -18,6 +18,7 @@ package mutate
 import (
 	"github.com/google/go-containerregistry/pkg/v1/types"
 	"github.com/sigstore/cosign/pkg/cosign/bundle"
+	"github.com/sigstore/cosign/pkg/cosign/tuf"
 	"github.com/sigstore/cosign/pkg/oci"
 )
 
@@ -65,6 +66,7 @@ type signatureOpts struct {
 	cert        []byte
 	chain       []byte
 	mediaType   types.MediaType
+	timestamp   *tuf.Timestamp
 }
 
 type SignatureOption func(*signatureOpts)
@@ -98,6 +100,13 @@ func WithMediaType(mediaType types.MediaType) SignatureOption {
 	}
 }
 
+// WithTimestamp specifies the new Timestamp the Signature should have.
+func WithTimestamp(timestamp *tuf.Timestamp) SignatureOption {
+	return func(so *signatureOpts) {
+		so.timestamp = timestamp
+	}
+}
+
 func makeSignatureOption(opts ...SignatureOption) *signatureOpts {
 	so := &signatureOpts{}
 	for _, opt := range opts {
diff --git a/pkg/oci/mutate/signature.go b/pkg/oci/mutate/signature.go
index dd2a7ab6..49ed98b2 100644
--- a/pkg/oci/mutate/signature.go
+++ b/pkg/oci/mutate/signature.go
@@ -24,6 +24,7 @@ import (
 	"github.com/google/go-containerregistry/pkg/v1/types"
 	"github.com/pkg/errors"
 	"github.com/sigstore/cosign/pkg/cosign/bundle"
+	"github.com/sigstore/cosign/pkg/cosign/tuf"
 	"github.com/sigstore/cosign/pkg/oci"
 	"github.com/sigstore/cosign/pkg/oci/static"
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
@@ -37,6 +38,7 @@ type sigWrapper struct {
 	cert        *x509.Certificate
 	chain       []*x509.Certificate
 	mediaType   types.MediaType
+	timestamp   *tuf.Timestamp
 }
 
 var _ v1.Layer = (*sigWrapper)(nil)
@@ -92,6 +94,14 @@ func (sw *sigWrapper) Bundle() (*bundle.RekorBundle, error) {
 	return sw.wrapped.Bundle()
 }
 
+// Timestamp implements oci.Signature.
+func (sw *sigWrapper) Timestamp() (*tuf.Timestamp, error) {
+	if sw.timestamp != nil {
+		return sw.timestamp, nil
+	}
+	return sw.wrapped.Timestamp()
+}
+
 // MediaType implements v1.Layer
 func (sw *sigWrapper) MediaType() (types.MediaType, error) {
 	if sw.mediaType != "" {
@@ -159,6 +169,15 @@ func Signature(original oci.Signature, opts ...SignatureOption) (oci.Signature,
 		newAnn[static.BundleAnnotationKey] = string(b)
 	}
 
+	if so.timestamp != nil {
+		newSig.timestamp = so.timestamp
+		t, err := json.Marshal(so.timestamp)
+		if err != nil {
+			return nil, err
+		}
+		newAnn[static.TimestampAnnotationKey] = string(t)
+	}
+
 	if so.cert != nil {
 		var cert *x509.Certificate
 		var chain []*x509.Certificate
diff --git a/pkg/oci/signatures.go b/pkg/oci/signatures.go
index e66b7c6c..c791d88e 100644
--- a/pkg/oci/signatures.go
+++ b/pkg/oci/signatures.go
@@ -20,6 +20,7 @@ import (
 
 	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/sigstore/cosign/pkg/cosign/bundle"
+	"github.com/sigstore/cosign/pkg/cosign/tuf"
 )
 
 // Signatures represents a set of signatures that are associated with a particular
@@ -59,4 +60,9 @@ type Signature interface {
 	// Bundle fetches the optional metadata that records the ephemeral
 	// Fulcio key in the transparency log.
 	Bundle() (*bundle.RekorBundle, error)
+
+	// Timestamp fetches the optional TUF timestamp metadata that
+	// records when the signature was generated. This can be used
+	// to find the TUF targets used to generate the signature.
+	Timestamp() (*tuf.Timestamp, error)
 }
diff --git a/pkg/oci/static/options.go b/pkg/oci/static/options.go
index e00b6cdb..03dc863a 100644
--- a/pkg/oci/static/options.go
+++ b/pkg/oci/static/options.go
@@ -20,6 +20,7 @@ import (
 
 	"github.com/google/go-containerregistry/pkg/v1/types"
 	"github.com/sigstore/cosign/pkg/cosign/bundle"
+	"github.com/sigstore/cosign/pkg/cosign/tuf"
 	ctypes "github.com/sigstore/cosign/pkg/types"
 )
 
@@ -33,6 +34,7 @@ type options struct {
 	Cert            []byte
 	Chain           []byte
 	Annotations     map[string]string
+	Timestamp       *tuf.Timestamp
 }
 
 func makeOptions(opts ...Option) (*options, error) {
@@ -59,6 +61,14 @@ func makeOptions(opts ...Option) (*options, error) {
 		o.Annotations[BundleAnnotationKey] = string(b)
 	}
 
+	if o.Timestamp != nil {
+		t, err := json.Marshal(o.Timestamp)
+		if err != nil {
+			return nil, err
+		}
+		o.Annotations[TimestampAnnotationKey] = string(t)
+	}
+
 	return o, nil
 }
 
@@ -97,3 +107,10 @@ func WithCertChain(cert, chain []byte) Option {
 		o.Chain = chain
 	}
 }
+
+// WithTimestamp sets the TUF timestamp to attach to the signature
+func WithTimestamp(t *tuf.Timestamp) Option {
+	return func(o *options) {
+		o.Timestamp = t
+	}
+}
diff --git a/pkg/oci/static/signature.go b/pkg/oci/static/signature.go
index 882ff87e..466f1eba 100644
--- a/pkg/oci/static/signature.go
+++ b/pkg/oci/static/signature.go
@@ -23,6 +23,7 @@ import (
 	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/go-containerregistry/pkg/v1/types"
 	"github.com/sigstore/cosign/pkg/cosign/bundle"
+	"github.com/sigstore/cosign/pkg/cosign/tuf"
 	"github.com/sigstore/cosign/pkg/oci"
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
 )
@@ -32,6 +33,7 @@ const (
 	CertificateAnnotationKey = "dev.sigstore.cosign/certificate"
 	ChainAnnotationKey       = "dev.sigstore.cosign/chain"
 	BundleAnnotationKey      = "dev.sigstore.cosign/bundle"
+	TimestampAnnotationKey   = "dev.sigstore.cosign/timestamp"
 )
 
 // NewSignature constructs a new oci.Signature from the provided options.
@@ -110,6 +112,11 @@ func (l *staticLayer) Bundle() (*bundle.RekorBundle, error) {
 	return l.opts.Bundle, nil
 }
 
+// Timestamp implements oci.Signature
+func (l *staticLayer) Timestamp() (*tuf.Timestamp, error) {
+	return l.opts.Timestamp, nil
+}
+
 // Digest implements v1.Layer
 func (l *staticLayer) Digest() (v1.Hash, error) {
 	h, _, err := v1.SHA256(bytes.NewReader(l.b))


Test patch:
diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go
index ced90f9a..b836999b 100644
--- a/cmd/cosign/cli/verify/verify_attestation.go
+++ b/cmd/cosign/cli/verify/verify_attestation.go
@@ -17,6 +17,8 @@ package verify
 
 import (
 	"context"
+	"crypto"
+	"crypto/ecdsa"
 	"encoding/base64"
 	"encoding/json"
 	"flag"
@@ -30,6 +32,7 @@ import (
 	"github.com/sigstore/cosign/pkg/cosign/pkcs11key"
 	"github.com/sigstore/cosign/pkg/cosign/rego"
 	"github.com/sigstore/cosign/pkg/oci"
+	"github.com/sigstore/sigstore/pkg/signature"
 
 	"github.com/sigstore/cosign/cmd/cosign/cli/fulcio"
 	"github.com/sigstore/cosign/cmd/cosign/cli/options"
@@ -45,11 +48,12 @@ import (
 type VerifyAttestationCommand struct {
 	options.RegistryOptions
 	CheckClaims   bool
+	CertRef       string
+	CertEmail     string
 	KeyRef        string
 	Sk            bool
 	Slot          string
 	Output        string
-	FulcioURL     string
 	RekorURL      string
 	PredicateType string
 	Policies      []string
@@ -62,7 +66,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
 		return flag.ErrHelp
 	}
 
-	if !options.OneOf(c.KeyRef, c.Sk) && !options.EnableExperimental() {
+	if !options.OneOf(c.KeyRef, c.Sk, c.CertRef) && !options.EnableExperimental() {
 		return &options.KeyParseError{}
 	}
 
@@ -72,6 +76,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
 	}
 	co := &cosign.CheckOpts{
 		RegistryClientOpts: ociremoteOpts,
+		CertEmail:          c.CertEmail,
 	}
 	if c.CheckClaims {
 		co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier
@@ -89,7 +94,8 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
 	keyRef := c.KeyRef
 
 	// Keys are optional!
-	if keyRef != "" {
+	switch {
+	case keyRef != "":
 		co.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, keyRef)
 		if err != nil {
 			return errors.Wrap(err, "loading public key")
@@ -98,7 +104,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
 		if ok {
 			defer pkcs11Key.Close()
 		}
-	} else if c.Sk {
+	case c.Sk:
 		sk, err := pivkey.GetKeyWithSlot(c.Slot)
 		if err != nil {
 			return errors.Wrap(err, "opening piv token")
@@ -108,6 +114,15 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
 		if err != nil {
 			return errors.Wrap(err, "initializing piv token verifier")
 		}
+	case c.CertRef != "":
+		cert, err := loadCertFromFileOrURL(c.CertRef)
+		if err != nil {
+			return errors.Wrap(err, "loading certificate from reference")
+		}
+		co.SigVerifier, err = signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256)
+		if err != nil {
+			return errors.Wrap(err, "creating certificate verifier")
+		}
 	}
 
 	for _, imageRef := range images {
diff --git a/doc/cosign_verify-attestation.md b/doc/cosign_verify-attestation.md
index 1af6cf66..3ffea92a 100644
--- a/doc/cosign_verify-attestation.md
+++ b/doc/cosign_verify-attestation.md
@@ -55,11 +55,10 @@ cosign verify-attestation [flags]
 ```
       --allow-insecure-registry                                                                  whether to allow insecure connections to registries. Don't use this for anything but testing
       --attachment-tag-prefix [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName]   optional custom prefix to use for attached image tags. Attachment images are tagged as: [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName]
+      --cert string                                                                              path to the public certificate
+      --cert-email string                                                                        the email expected in a valid Fulcio certificate
       --check-claims                                                                             whether to check the claims found (default true)
-      --fulcio-url string                                                                        [EXPERIMENTAL] address of sigstore PKI server (default "https://v1.fulcio.sigstore.dev")
   -h, --help                                                                                     help for verify-attestation
-      --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).
       --k8s-keychain                                                                             whether to use the kubernetes keychain instead of the default keychain (supports workload identity).
       --key string                                                                               path to the public key file, KMS URI or Kubernetes Secret
       --local-image                                                                              whether the specified image is a path to an image saved locally via 'cosign save'
diff --git a/internal/pkg/cosign/fulcio/signer_test.go b/internal/pkg/cosign/fulcio/signer_test.go
new file mode 100644
index 00000000..53077cd2
--- /dev/null
+++ b/internal/pkg/cosign/fulcio/signer_test.go
@@ -0,0 +1,140 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package fulcio
+
+import (
+	"bytes"
+	"context"
+	"crypto"
+	"encoding/base64"
+	"strings"
+	"testing"
+
+	"github.com/sigstore/cosign/internal/pkg/cosign/payload"
+	"github.com/sigstore/cosign/pkg/cosign"
+	"github.com/sigstore/sigstore/pkg/signature"
+)
+
+var (
+	testCertBytes = []byte(`
+-----BEGIN CERTIFICATE-----
+MIICjzCCAhSgAwIBAgITV2heiswW9YldtVEAu98QxDO8TTAKBggqhkjOPQQDAzAq
+MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIx
+MDkxNDE5MTI0MFoXDTIxMDkxNDE5MzIzOVowADBZMBMGByqGSM49AgEGCCqGSM49
+AwEHA0IABMF1AWZcfvubslc4ABNnvGbRjm6GWVHxrJ1RRthTHMCE4FpFmiHQBfGt
+6n80DqszGj77Whb35O33+Dal4Y2po+CjggFBMIIBPTAOBgNVHQ8BAf8EBAMCB4Aw
+EwYDVR0lBAwwCgYIKwYBBQUHAwMwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU340G
+3G1ozVNmFC5TBFV0yNuouvowHwYDVR0jBBgwFoAUyMUdAEGaJCkyUSTrDa5K7UoG
+0+wwgY0GCCsGAQUFBwEBBIGAMH4wfAYIKwYBBQUHMAKGcGh0dHA6Ly9wcml2YXRl
+Y2EtY29udGVudC02MDNmZTdlNy0wMDAwLTIyMjctYmY3NS1mNGY1ZTgwZDI5NTQu
+c3RvcmFnZS5nb29nbGVhcGlzLmNvbS9jYTM2YTFlOTYyNDJiOWZjYjE0Ni9jYS5j
+cnQwOAYDVR0RAQH/BC4wLIEqa2V5bGVzc0BkaXN0cm9sZXNzLmlhbS5nc2Vydmlj
+ZWFjY291bnQuY29tMAoGCCqGSM49BAMDA2kAMGYCMQDcH9cdkxW6ugsbPHqX9qrM
+wlMaprcwnlktS3+5xuABr5icuqwrB/Fj5doFtS7AnM0CMQD9MjSaUmHFFF7zoLMx
+uThR1Z6JuA21HwxtL3GyJ8UQZcEPOlTBV593HrSAwBhiCoY=
+-----END CERTIFICATE-----
+`)
+	testChainBytes = []byte(`
+-----BEGIN CERTIFICATE-----
+MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAq
+MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIx
+MDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUu
+ZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSy
+A7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0Jcas
+taRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6Nm
+MGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE
+FMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2u
+Su1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJx
+Ve/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uup
+Hr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==
+-----END CERTIFICATE-----
+`)
+)
+
+func mustGetNewSigner(t *testing.T) signature.Signer {
+	t.Helper()
+	priv, err := cosign.GeneratePrivateKey()
+	if err != nil {
+		t.Fatalf("cosign.GeneratePrivateKey() failed: %v", err)
+	}
+	s, err := signature.LoadECDSASignerVerifier(priv, crypto.SHA256)
+	if err != nil {
+		t.Fatalf("signature.LoadECDSASignerVerifier(key, crypto.SHA256) failed: %v", err)
+	}
+	return s
+}
+
+func TestSigner(t *testing.T) {
+	// Need real cert and chain
+	payloadSigner := payload.NewSigner(mustGetNewSigner(t))
+	testSigner := NewSigner(payloadSigner, testCertBytes, testChainBytes)
+
+	testPayload := "test payload"
+
+	ociSig, pub, err := testSigner.Sign(context.Background(), strings.NewReader(testPayload))
+	if err != nil {
+		t.Fatalf("Sign() returned error: %v", err)
+	}
+
+	// Verify that the OCI signature contains a cert, chain and timestamp.
+	cert, err := ociSig.Cert()
+	if err != nil {
+		t.Fatalf("ociSig.Cert() returned error: %v", err)
+	}
+	if cert == nil {
+		t.Fatal("ociSig.Cert() missing certificate, got nil")
+	}
+	chain, err := ociSig.Chain()
+	if err != nil {
+		t.Fatalf("ociSig.Chain() returned error: %v", err)
+	}
+	if len(chain) != 1 {
+		t.Fatalf("ociSig.Chain() expected to be of length 1, got %d", len(chain))
+	}
+	if chain[0] == nil {
+		t.Fatal("ociSig.Chain()[0] missing certificate, got nil")
+	}
+	timestamp, err := ociSig.Timestamp()
+	if err != nil {
+		t.Fatalf("ociSig.Timestamp() returned error: %v", err)
+	}
+	if timestamp == nil {
+		t.Fatal("ociSig.Timestamp() missing TUF timestamp, got nil")
+	}
+
+	// Verify that the wrapped signer was called.
+	verifier, err := signature.LoadVerifier(pub, crypto.SHA256)
+	if err != nil {
+		t.Fatalf("signature.LoadVerifier(pub) returned error: %v", err)
+	}
+	b64Sig, err := ociSig.Base64Signature()
+	if err != nil {
+		t.Fatalf("ociSig.Base64Signature() returned error: %v", err)
+	}
+	sig, err := base64.StdEncoding.DecodeString(b64Sig)
+	if err != nil {
+		t.Fatalf("base64.StdEncoding.DecodeString(b64Sig) returned error: %v", err)
+	}
+	gotPayload, err := ociSig.Payload()
+	if err != nil {
+		t.Fatalf("ociSig.Payload() returned error: %v", err)
+	}
+	if string(gotPayload) != testPayload {
+		t.Errorf("ociSig.Payload() returned %q, wanted %q", string(gotPayload), testPayload)
+	}
+	if err = verifier.VerifySignature(bytes.NewReader(sig), bytes.NewReader(gotPayload)); err != nil {
+		t.Errorf("VerifySignature() returned error: %v", err)
+	}
+}
diff --git a/internal/pkg/cosign/rekor/signer_test.go b/internal/pkg/cosign/rekor/signer_test.go
new file mode 100644
index 00000000..89f6e03e
--- /dev/null
+++ b/internal/pkg/cosign/rekor/signer_test.go
@@ -0,0 +1,93 @@
+// Copyright 2022 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package rekor
+
+import (
+	"bytes"
+	"context"
+	"crypto"
+	"encoding/base64"
+	"strings"
+	"testing"
+
+	"github.com/sigstore/cosign/internal/pkg/cosign/payload"
+	"github.com/sigstore/cosign/pkg/cosign"
+	"github.com/sigstore/rekor/pkg/generated/client"
+	"github.com/sigstore/sigstore/pkg/signature"
+)
+
+func mustGetNewSigner(t *testing.T) signature.Signer {
+	t.Helper()
+	priv, err := cosign.GeneratePrivateKey()
+	if err != nil {
+		t.Fatalf("cosign.GeneratePrivateKey() failed: %v", err)
+	}
+	s, err := signature.LoadECDSASignerVerifier(priv, crypto.SHA256)
+	if err != nil {
+		t.Fatalf("signature.LoadECDSASignerVerifier(key, crypto.SHA256) failed: %v", err)
+	}
+	return s
+}
+
+func TestSigner(t *testing.T) {
+	// Need real cert and chain
+	payloadSigner := payload.NewSigner(mustGetNewSigner(t))
+
+	// Mock out Rekor client
+	var mClient client.Rekor
+	mClient.Entries = &MockEntriesClient{}
+
+	testSigner := NewSigner(payloadSigner, &mClient)
+
+	testPayload := "test payload"
+
+	ociSig, pub, err := testSigner.Sign(context.Background(), strings.NewReader(testPayload))
+	if err != nil {
+		t.Fatalf("Sign() returned error: %v", err)
+	}
+
+	// Verify that the OCI signature contains a timestamp.
+	timestamp, err := ociSig.Timestamp()
+	if err != nil {
+		t.Fatalf("ociSig.Timestamp() returned error: %v", err)
+	}
+	if timestamp == nil {
+		t.Fatal("ociSig.Timestamp() missing TUF timestamp, got nil")
+	}
+
+	// Verify that the wrapped signer was called.
+	verifier, err := signature.LoadVerifier(pub, crypto.SHA256)
+	if err != nil {
+		t.Fatalf("signature.LoadVerifier(pub) returned error: %v", err)
+	}
+	b64Sig, err := ociSig.Base64Signature()
+	if err != nil {
+		t.Fatalf("ociSig.Base64Signature() returned error: %v", err)
+	}
+	sig, err := base64.StdEncoding.DecodeString(b64Sig)
+	if err != nil {
+		t.Fatalf("base64.StdEncoding.DecodeString(b64Sig) returned error: %v", err)
+	}
+	gotPayload, err := ociSig.Payload()
+	if err != nil {
+		t.Fatalf("ociSig.Payload() returned error: %v", err)
+	}
+	if string(gotPayload) != testPayload {
+		t.Errorf("ociSig.Payload() returned %q, wanted %q", string(gotPayload), testPayload)
+	}
+	if err = verifier.VerifySignature(bytes.NewReader(sig), bytes.NewReader(gotPayload)); err != nil {
+		t.Errorf("VerifySignature() returned error: %v", err)
+	}
+}
diff --git a/pkg/cosign/tuf/client_test.go b/pkg/cosign/tuf/client_test.go
index 46d07922..bfdd2b16 100644
--- a/pkg/cosign/tuf/client_test.go
+++ b/pkg/cosign/tuf/client_test.go
@@ -39,7 +39,7 @@ func TestNewFromEnv(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	checkTargets(t, tuf)
+	checkTargetsAndMeta(t, tuf)
 	tuf.Close()
 
 	// Now try with expired targets
@@ -49,7 +49,7 @@ func TestNewFromEnv(t *testing.T) {
 		t.Fatal(err)
 	}
 	tuf.Close()
-	checkTargets(t, tuf)
+	checkTargetsAndMeta(t, tuf)
 
 	// Now let's explicitly make a root.
 	remote, err := GcsRemoteStore(ctx, DefaultRemoteRoot, nil, nil)
@@ -68,7 +68,7 @@ func TestNewFromEnv(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	checkTargets(t, tuf)
+	checkTargetsAndMeta(t, tuf)
 	tuf.Close()
 }
 
@@ -86,7 +86,7 @@ func TestNoCache(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	checkTargets(t, tuf)
+	checkTargetsAndMeta(t, tuf)
 	tuf.Close()
 
 	if l := dirLen(t, td); l != 0 {
@@ -129,10 +129,10 @@ func TestCache(t *testing.T) {
 	if l := dirLen(t, td); l == 0 {
 		t.Errorf("expected filesystem writes, got %d entries", l)
 	}
-	checkTargets(t, tuf)
+	checkTargetsAndMeta(t, tuf)
 }
 
-func checkTargets(t *testing.T, tuf *TUF) {
+func checkTargetsAndMeta(t *testing.T, tuf *TUF) {
 	// Check the targets
 	t.Helper()
 	for _, target := range targets {
@@ -145,6 +145,13 @@ func checkTargets(t *testing.T, tuf *TUF) {
 	if _, err := tuf.GetTarget("invalid"); err == nil {
 		t.Error("expected error reading target, got nil")
 	}
+
+	// Check the TUF timestamp metadata
+	if ts, err := tuf.GetTimestamp(); err != nil {
+		t.Error("expected no error reading timestamp, got err")
+	} else if len(ts) == 0 {
+		t.Errorf("expected timestamp length of %d, got 0", len(ts))
+	}
 }
 
 func dirLen(t *testing.T, td string) int {
@@ -157,11 +164,11 @@ func dirLen(t *testing.T, td string) int {
 }
 
 func forceExpiration(t *testing.T, expire bool) {
-	oldIsExpiredMetadata := isExpiredMetadata
-	isExpiredMetadata = func(_ []byte) bool {
+	oldIsExpiredTimestamp := isExpiredTimestamp
+	isExpiredTimestamp = func(_ []byte) bool {
 		return expire
 	}
 	t.Cleanup(func() {
-		isExpiredMetadata = oldIsExpiredMetadata
+		isExpiredTimestamp = oldIsExpiredTimestamp
 	})
 }
diff --git a/pkg/oci/internal/signature/layer_test.go b/pkg/oci/internal/signature/layer_test.go
index a3ea0d80..10618dde 100644
--- a/pkg/oci/internal/signature/layer_test.go
+++ b/pkg/oci/internal/signature/layer_test.go
@@ -20,6 +20,7 @@ import (
 	"encoding/base64"
 	"fmt"
 	"testing"
+	"time"
 
 	"github.com/google/go-cmp/cmp"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -27,6 +28,8 @@ import (
 	"github.com/google/go-containerregistry/pkg/v1/types"
 	"github.com/pkg/errors"
 	"github.com/sigstore/cosign/pkg/cosign/bundle"
+	"github.com/sigstore/cosign/pkg/cosign/tuf"
+	"github.com/theupdateframework/go-tuf/data"
 )
 
 func mustDecode(s string) []byte {
@@ -46,19 +49,22 @@ func TestSignature(t *testing.T) {
 	if err != nil {
 		t.Fatalf("Digest() = %v", err)
 	}
+	ts, _ := time.Parse(time.RFC3339, "2022-01-15T00:39:22Z")
 
 	tests := []struct {
-		name           string
-		l              *sigLayer
-		wantPayloadErr error
-		wantSig        string
-		wantSigErr     error
-		wantCert       bool
-		wantCertErr    error
-		wantChain      int
-		wantChainErr   error
-		wantBundle     *bundle.RekorBundle
-		wantBundleErr  error
+		name             string
+		l                *sigLayer
+		wantPayloadErr   error
+		wantSig          string
+		wantSigErr       error
+		wantCert         bool
+		wantCertErr      error
+		wantChain        int
+		wantChainErr     error
+		wantBundle       *bundle.RekorBundle
+		wantBundleErr    error
+		wantTimestamp    *tuf.Timestamp
+		wantTimestampErr error
 	}{{
 		name: "just payload and signature",
 		l: &sigLayer{
@@ -78,10 +84,11 @@ func TestSignature(t *testing.T) {
 			desc: v1.Descriptor{
 				Digest: digest,
 				Annotations: map[string]string{
-					sigkey:    "blah",
-					certkey:   "",
-					chainkey:  "",
-					BundleKey: "",
+					sigkey:       "blah",
+					certkey:      "",
+					chainkey:     "",
+					BundleKey:    "",
+					TimestampKey: "",
 				},
 			},
 		},
@@ -109,6 +116,20 @@ func TestSignature(t *testing.T) {
 		},
 		wantSig:       "blah",
 		wantBundleErr: errors.New(`unmarshaling bundle: invalid character '}' looking for beginning of value`),
+	}, {
+		name: "min plus bad timestamp",
+		l: &sigLayer{
+			Layer: layer,
+			desc: v1.Descriptor{
+				Digest: digest,
+				Annotations: map[string]string{
+					sigkey:       "blah",
+					TimestampKey: `}`,
+				},
+			},
+		},
+		wantSig:          "blah",
+		wantTimestampErr: errors.New(`unmarshaling timestamp: invalid character '}' looking for beginning of value`),
 	}, {
 		name: "min plus bad cert",
 		l: &sigLayer{
@@ -161,6 +182,45 @@ func TestSignature(t *testing.T) {
 				LogID:          "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
 			},
 		},
+	}, {
+		name: "min plus timestamp",
+		l: &sigLayer{
+			Layer: layer,
+			desc: v1.Descriptor{
+				Digest: digest,
+				Annotations: map[string]string{
+					sigkey:       "blah",
+					TimestampKey: `{"signatures":[{"keyid":"b6710623a30c010738e64c5209d367df1c0a18cf90e6ab5292fb01680f83453d","sig":"3046022100926cd1a5a90539f3efa97390293180132413c7d30d94399c220a8a9aa9907e6e0221009e07b0e207f76dd45caeab87258553ddcf83fc7db6dfbbd4678d18f8c3517023"}],"signed":{"_type":"timestamp","spec_version":"1.0","version":8,"expires":"2022-01-15T00:39:22Z","meta":{"snapshot.json":{"length":1658,"hashes":{"sha256":"95e5b6822e0c3a9924f2f906c0b75e09246ad6d37078806085a273fddd079679","sha512":"4b1df9f2cc2d052bee185554ded7c526e283d4fab8388557a7b684c4ce0efb28c196e33a5140e7de9de99b2f5f37a7b2503617c2ff220168c5b7a79340675acf"},"version":8}}}}`,
+				},
+			},
+		},
+		wantSig: "blah",
+		wantTimestamp: &tuf.Timestamp{
+			Signatures: []data.Signature{
+				{
+					KeyID:     "b6710623a30c010738e64c5209d367df1c0a18cf90e6ab5292fb01680f83453d",
+					Signature: []byte{48, 70, 2, 33, 0, 146, 108, 209, 165, 169, 5, 57, 243, 239, 169, 115, 144, 41, 49, 128, 19, 36, 19, 199, 211, 13, 148, 57, 156, 34, 10, 138, 154, 169, 144, 126, 110, 2, 33, 0, 158, 7, 176, 226, 7, 247, 109, 212, 92, 174, 171, 135, 37, 133, 83, 221, 207, 131, 252, 125, 182, 223, 187, 212, 103, 141, 24, 248, 195, 81, 112, 35},
+				},
+			},
+			Signed: data.Timestamp{
+				Type:        "timestamp",
+				SpecVersion: "1.0",
+				Version:     8,
+				Expires:     ts,
+				Meta: map[string]data.TimestampFileMeta{
+					"snapshot.json": {
+						FileMeta: data.FileMeta{
+							Length: 1658,
+							Hashes: map[string]data.HexBytes{
+								"sha256": []byte{149, 229, 182, 130, 46, 12, 58, 153, 36, 242, 249, 6, 192, 183, 94, 9, 36, 106, 214, 211, 112, 120, 128, 96, 133, 162, 115, 253, 221, 7, 150, 121},
+								"sha512": []byte{75, 29, 249, 242, 204, 45, 5, 43, 238, 24, 85, 84, 222, 215, 197, 38, 226, 131, 212, 250, 184, 56, 133, 87, 167, 182, 132, 196, 206, 14, 251, 40, 193, 150, 227, 58, 81, 64, 231, 222, 157, 233, 155, 47, 95, 55, 167, 178, 80, 54, 23, 194, 255, 34, 1, 104, 197, 183, 167, 147, 64, 103, 90, 207},
+							},
+						},
+						Version: 8,
+					},
+				},
+			},
+		},
 	}, {
 		name: "min plus good cert",
 		l: &sigLayer{
@@ -275,6 +335,15 @@ Hr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==
 			case !cmp.Equal(got, test.wantBundle):
 				t.Errorf("Bundle() %s", cmp.Diff(got, test.wantBundle))
 			}
+
+			switch got, err := test.l.Timestamp(); {
+			case (err != nil) != (test.wantTimestampErr != nil):
+				t.Errorf("Timestamp() = %v, wanted %v", err, test.wantTimestampErr)
+			case (err != nil) && (test.wantTimestampErr != nil) && err.Error() != test.wantTimestampErr.Error():
+				t.Errorf("Timestamp() = %v, wanted %v", err, test.wantTimestampErr)
+			case !cmp.Equal(got, test.wantTimestamp):
+				t.Errorf("Timestamp() %s", cmp.Diff(got, test.wantTimestamp))
+			}
 		})
 	}
 }
diff --git a/pkg/oci/mutate/signature_test.go b/pkg/oci/mutate/signature_test.go
index 1e17e9bc..7f26f82f 100644
--- a/pkg/oci/mutate/signature_test.go
+++ b/pkg/oci/mutate/signature_test.go
@@ -22,8 +22,10 @@ import (
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-containerregistry/pkg/v1/types"
 	"github.com/sigstore/cosign/pkg/cosign/bundle"
+	"github.com/sigstore/cosign/pkg/cosign/tuf"
 	"github.com/sigstore/cosign/pkg/oci"
 	"github.com/sigstore/cosign/pkg/oci/static"
+	"github.com/theupdateframework/go-tuf/data"
 )
 
 var (
@@ -311,6 +313,40 @@ func TestSignatureWithBundle(t *testing.T) {
 	assertSignaturesEqual(t, expectedSig, newSig)
 }
 
+func TestSignatureWithTimestamp(t *testing.T) {
+	payload := "this is the TestSignatureWithTimestamp content!"
+	b64sig := "b64 content2="
+	timestamp := &tuf.Timestamp{
+		Signatures: []data.Signature{
+			{
+				KeyID: "b6710623a30c010738e64c5209d367df1c0a18cf90e6ab5292fb01680f83453d",
+			},
+		},
+		Signed: data.Timestamp{
+			Type:        "timestamp",
+			SpecVersion: "1.0",
+			Version:     8,
+			Meta: map[string]data.TimestampFileMeta{
+				"snapshot.json": {
+					FileMeta: data.FileMeta{
+						Length: 1658,
+					},
+					Version: 8,
+				},
+			},
+		},
+	}
+	originalSig := mustCreateSignature(t, []byte(payload), b64sig)
+	expectedSig := mustCreateSignature(t, []byte(payload), b64sig, static.WithTimestamp(timestamp))
+
+	newSig, err := Signature(originalSig, WithTimestamp(timestamp))
+	if err != nil {
+		t.Fatalf("Signature(WithTimestamp()) returned error: %v", err)
+	}
+
+	assertSignaturesEqual(t, expectedSig, newSig)
+}
+
 func TestSignatureWithCertChain(t *testing.T) {
 	payload := "this is the TestSignatureWithCertChain content!"
 	b64sig := "b64 content3="
@@ -358,6 +394,26 @@ func TestSignatureWithEverything(t *testing.T) {
 			LogID:          "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
 		},
 	}
+	timestamp := &tuf.Timestamp{
+		Signatures: []data.Signature{
+			{
+				KeyID: "b6710623a30c010738e64c5209d367df1c0a18cf90e6ab5292fb01680f83453d",
+			},
+		},
+		Signed: data.Timestamp{
+			Type:        "timestamp",
+			SpecVersion: "1.0",
+			Version:     8,
+			Meta: map[string]data.TimestampFileMeta{
+				"snapshot.json": {
+					FileMeta: data.FileMeta{
+						Length: 1658,
+					},
+					Version: 8,
+				},
+			},
+		},
+	}
 	mediaType := types.MediaType("test/media.type")
 
 	originalSig := mustCreateSignature(t, []byte(payload), b64sig)
@@ -365,12 +421,14 @@ func TestSignatureWithEverything(t *testing.T) {
 	expectedSig := mustCreateSignature(t, []byte(payload), b64sig,
 		static.WithAnnotations(annotations),
 		static.WithBundle(b),
+		static.WithTimestamp(timestamp),
 		static.WithCertChain(testCertBytes, testChainBytes),
 		static.WithLayerMediaType(mediaType))
 
 	newSig, err := Signature(originalSig,
 		WithAnnotations(annotations),
 		WithBundle(b),
+		WithTimestamp(timestamp),
 		WithCertChain(testCertBytes, testChainBytes),
 		WithMediaType(mediaType))
 
diff --git a/pkg/oci/static/options_test.go b/pkg/oci/static/options_test.go
index 63600c79..ca708106 100644
--- a/pkg/oci/static/options_test.go
+++ b/pkg/oci/static/options_test.go
@@ -22,11 +22,13 @@ import (
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-containerregistry/pkg/v1/types"
 	"github.com/sigstore/cosign/pkg/cosign/bundle"
+	"github.com/sigstore/cosign/pkg/cosign/tuf"
 	ctypes "github.com/sigstore/cosign/pkg/types"
 )
 
 func TestOptions(t *testing.T) {
 	bundle := &bundle.RekorBundle{}
+	timestamp := &tuf.Timestamp{}
 
 	tests := []struct {
 		name string
@@ -91,6 +93,17 @@ func TestOptions(t *testing.T) {
 			},
 			Bundle: bundle,
 		},
+	}, {
+		name: "with timestamp",
+		opts: []Option{WithTimestamp(timestamp)},
+		want: &options{
+			LayerMediaType:  ctypes.SimpleSigningMediaType,
+			ConfigMediaType: types.OCIConfigJSON,
+			Annotations: map[string]string{
+				TimestampAnnotationKey: `{"signatures":null,"signed":{"_type":"","spec_version":"","version":0,"expires":"0001-01-01T00:00:00Z","meta":null}}`,
+			},
+			Timestamp: timestamp,
+		},
 	}}
 
 	for _, test := range tests {
diff --git a/pkg/oci/static/signature_test.go b/pkg/oci/static/signature_test.go
index 1ef5becc..affdbf50 100644
--- a/pkg/oci/static/signature_test.go
+++ b/pkg/oci/static/signature_test.go
@@ -20,11 +20,14 @@ import (
 	"io"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/google/go-cmp/cmp"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/go-containerregistry/pkg/v1/types"
 	"github.com/sigstore/cosign/pkg/cosign/bundle"
+	"github.com/sigstore/cosign/pkg/cosign/tuf"
+	"github.com/theupdateframework/go-tuf/data"
 )
 
 func TestNewSignatureBasic(t *testing.T) {
@@ -130,6 +133,14 @@ func TestNewSignatureBasic(t *testing.T) {
 		if gotBundle != nil {
 			t.Errorf("Bundle() = %#v, wanted nil", gotBundle)
 		}
+
+		gotTimestamp, err := l.Timestamp()
+		if err != nil {
+			t.Fatalf("Timestamp() = %v", err)
+		}
+		if gotTimestamp != nil {
+			t.Errorf("Timestamp() = %#v, wanted nil", gotTimestamp)
+		}
 	})
 
 	t.Run("check annotations", func(t *testing.T) {
@@ -168,6 +179,12 @@ func TestNewSignatureBasic(t *testing.T) {
 			t.Errorf("Bundle() = %#v, wanted nil", got)
 		}
 
+		if got, err := l.Timestamp(); err != nil {
+			t.Fatalf("Timestamp() = %v", err)
+		} else if got != nil {
+			t.Errorf("Timestamp() = %#v, wanted nil", got)
+		}
+
 		if got, err := l.Cert(); err != nil {
 			t.Fatalf("Cert() = %v", err)
 		} else if got != nil {
@@ -284,6 +301,14 @@ func TestNewAttestationBasic(t *testing.T) {
 		if gotBundle != nil {
 			t.Errorf("Bundle() = %#v, wanted nil", gotBundle)
 		}
+
+		gotTimestamp, err := l.Timestamp()
+		if err != nil {
+			t.Fatalf("Timestamp() = %v", err)
+		}
+		if gotTimestamp != nil {
+			t.Errorf("Timestamp() = %#v, wanted nil", gotTimestamp)
+		}
 	})
 
 	t.Run("check annotations", func(t *testing.T) {
@@ -322,6 +347,12 @@ func TestNewAttestationBasic(t *testing.T) {
 			t.Errorf("Bundle() = %#v, wanted nil", got)
 		}
 
+		if got, err := l.Timestamp(); err != nil {
+			t.Fatalf("Timestamp() = %v", err)
+		} else if got != nil {
+			t.Errorf("Timestamp() = %#v, wanted nil", got)
+		}
+
 		if got, err := l.Cert(); err != nil {
 			t.Fatalf("Cert() = %v", err)
 		} else if got != nil {
@@ -336,9 +367,10 @@ func TestNewAttestationBasic(t *testing.T) {
 	})
 }
 
-func TestNewSignatureCertChainAndBundle(t *testing.T) {
+func TestNewSignatureCertChainBundleAndTimestamp(t *testing.T) {
 	payload := "this is the other content!"
 	b64sig := "b64 content="
+	ts, _ := time.Parse(time.RFC3339, "2022-01-15T00:39:22Z")
 
 	// This was extracted from gcr.io/distroless/static:nonroot on 2021/09/16
 	var (
@@ -384,10 +416,36 @@ Hr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==
 				LogID:          "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
 			},
 		}
+		timestamp = &tuf.Timestamp{
+			Signatures: []data.Signature{
+				{
+					KeyID:     "b6710623a30c010738e64c5209d367df1c0a18cf90e6ab5292fb01680f83453d",
+					Signature: []byte{48, 70, 2, 33, 0, 146, 108, 209, 165, 169, 5, 57, 243, 239, 169, 115, 144, 41, 49, 128, 19, 36, 19, 199, 211, 13, 148, 57, 156, 34, 10, 138, 154, 169, 144, 126, 110, 2, 33, 0, 158, 7, 176, 226, 7, 247, 109, 212, 92, 174, 171, 135, 37, 133, 83, 221, 207, 131, 252, 125, 182, 223, 187, 212, 103, 141, 24, 248, 195, 81, 112, 35},
+				},
+			},
+			Signed: data.Timestamp{
+				Type:        "timestamp",
+				SpecVersion: "1.0",
+				Version:     8,
+				Expires:     ts,
+				Meta: map[string]data.TimestampFileMeta{
+					"snapshot.json": {
+						FileMeta: data.FileMeta{
+							Length: 1658,
+							Hashes: map[string]data.HexBytes{
+								"sha256": []byte{149, 229, 182, 130, 46, 12, 58, 153, 36, 242, 249, 6, 192, 183, 94, 9, 36, 106, 214, 211, 112, 120, 128, 96, 133, 162, 115, 253, 221, 7, 150, 121},
+								"sha512": []byte{75, 29, 249, 242, 204, 45, 5, 43, 238, 24, 85, 84, 222, 215, 197, 38, 226, 131, 212, 250, 184, 56, 133, 87, 167, 182, 132, 196, 206, 14, 251, 40, 193, 150, 227, 58, 81, 64, 231, 222, 157, 233, 155, 47, 95, 55, 167, 178, 80, 54, 23, 194, 255, 34, 1, 104, 197, 183, 167, 147, 64, 103, 90, 207},
+							},
+						},
+						Version: 8,
+					},
+				},
+			},
+		}
 	)
 
 	l, err := NewSignature([]byte(payload), b64sig,
-		WithCertChain(cert, chain), WithBundle(b))
+		WithCertChain(cert, chain), WithBundle(b), WithTimestamp(timestamp))
 	if err != nil {
 		t.Fatalf("NewSignature() = %v", err)
 	}
@@ -415,6 +473,12 @@ Hr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==
 			t.Errorf("Bundle() = %#v, wanted %#v", got, b)
 		}
 
+		if got, err := l.Timestamp(); err != nil {
+			t.Fatalf("Timestamp() = %v", err)
+		} else if got != timestamp {
+			t.Errorf("Timestamp() = %#v, wanted %#v", got, timestamp)
+		}
+
 		if got, err := l.Cert(); err != nil {
 			t.Fatalf("Cert() = %v", err)
 		} else if got == nil {
@@ -435,7 +499,8 @@ Hr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==
 			ChainAnnotationKey:       string(chain),
 			// This was extracted from gcr.io/distroless/static:nonroot on 2021/09/16.
 			// The Body has been removed for brevity
-			BundleAnnotationKey: `{"SignedEntryTimestamp":"MEUCIQClUkUqZNf+6dxBc/pxq22JIluTB7Kmip1G0FIF5E0C1wIgLqXm+IM3JYW/P/qjMZSXW+J8bt5EOqNfe3R+0A9ooFE=","Payload":{"body":"REMOVED","integratedTime":1631646761,"logIndex":693591,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}`,
+			BundleAnnotationKey:    `{"SignedEntryTimestamp":"MEUCIQClUkUqZNf+6dxBc/pxq22JIluTB7Kmip1G0FIF5E0C1wIgLqXm+IM3JYW/P/qjMZSXW+J8bt5EOqNfe3R+0A9ooFE=","Payload":{"body":"REMOVED","integratedTime":1631646761,"logIndex":693591,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}`,
+			TimestampAnnotationKey: `{"signatures":[{"keyid":"b6710623a30c010738e64c5209d367df1c0a18cf90e6ab5292fb01680f83453d","sig":"3046022100926cd1a5a90539f3efa97390293180132413c7d30d94399c220a8a9aa9907e6e0221009e07b0e207f76dd45caeab87258553ddcf83fc7db6dfbbd4678d18f8c3517023"}],"signed":{"_type":"timestamp","spec_version":"1.0","version":8,"expires":"2022-01-15T00:39:22Z","meta":{"snapshot.json":{"length":1658,"hashes":{"sha256":"95e5b6822e0c3a9924f2f906c0b75e09246ad6d37078806085a273fddd079679","sha512":"4b1df9f2cc2d052bee185554ded7c526e283d4fab8388557a7b684c4ce0efb28c196e33a5140e7de9de99b2f5f37a7b2503617c2ff220168c5b7a79340675acf"},"version":8}}}}`,
 		}
 		got, err := l.Annotations()
 		if err != nil {
diff --git a/test/e2e_test.go b/test/e2e_test.go
index 7a403c35..0d48719e 100644
--- a/test/e2e_test.go
+++ b/test/e2e_test.go
@@ -449,8 +449,8 @@ func TestSignBlob(t *testing.T) {
 		KeyRef: pubKeyPath2,
 	}
 	// Verify should fail on a bad input
-	mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "", "badsig", blob), t)
-	mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "", "badsig", blob), t)
+	mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "badsig", blob), t)
+	mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "badsig", blob), t)
 
 	// Now sign the blob with one key
 	ko := sign.KeyOpts{
@@ -462,8 +462,8 @@ func TestSignBlob(t *testing.T) {
 		t.Fatal(err)
 	}
 	// Now verify should work with that one, but not the other
-	must(cliverify.VerifyBlobCmd(ctx, ko1, "", string(sig), bp), t)
-	mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "", string(sig), bp), t)
+	must(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, string(sig), bp), t)
+	mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, string(sig), bp), t)
 }
 
 func TestGenerate(t *testing.T) {