Skip to content

Commit

Permalink
Updates to Timestamp signing and verification (#2499)
Browse files Browse the repository at this point in the history
* Updates to Timestamp signing and verification

* Switch to using the raw signature rather than base64 signature for OCI
  and blob signing
* For blob signing, write only the timestamp to disk, not the
  LocalSignedPayload (since that's already written with the bundle)
* For blob verification, expect only a timestamp in the file. If you
  don't pass a bundle, you'll need to also pass the signature by flag
* Some nits from the previous PR

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

* Add testing in verify, update e2e test

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

* Fix test

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

* Add missing setting of SkipTlogVerify

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

* Address comments

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

* Fix docs

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

* Address nits

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

* Fix e2e test

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>
  • Loading branch information
haydentherapper committed Dec 7, 2022
1 parent 980905e commit e9e9cb2
Show file tree
Hide file tree
Showing 16 changed files with 255 additions and 180 deletions.
6 changes: 3 additions & 3 deletions cmd/cosign/cli/options/signblob.go
Expand Up @@ -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{})
}
4 changes: 2 additions & 2 deletions cmd/cosign/cli/options/verify.go
Expand Up @@ -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.
Expand Down
3 changes: 1 addition & 2 deletions cmd/cosign/cli/policy_init.go
Expand Up @@ -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
}
}
Expand Down
37 changes: 20 additions & 17 deletions cmd/cosign/cli/sign/sign_blob.go
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions cmd/cosign/cli/verify.go
Expand Up @@ -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)
Expand Down
39 changes: 10 additions & 29 deletions cmd/cosign/cli/verify/verify_blob.go
Expand Up @@ -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"
Expand Down Expand Up @@ -73,16 +74,16 @@ 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.
if options.NOf(c.KeyRef, c.Sk, c.CertRef) > 1 {
return &options.KeyParseError{}
}

sig, err := base64signature(c.SigRef, c.BundlePath, c.RFC3161TimestampPath)
sig, err := base64signature(c.SigRef, c.BundlePath)
if err != nil {
return err
}
Expand Down Expand Up @@ -208,29 +209,15 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error {
opts = append(opts, static.WithBundle(b.Bundle))
}
if c.RFC3161TimestampPath != "" {
b, err := cosign.FetchLocalSignedPayloadFromPath(c.RFC3161TimestampPath)
var rfc3161Timestamp bundle.RFC3161Timestamp
ts, err := blob.LoadFileOrURL(c.RFC3161TimestampPath)
if err != nil {
return err
}
// Note: RFC3161 timestamp does not set the certificate.
// We have to condition on this because sign-blob may not output the signing
// key to the bundle when there is no tlog upload.
if b.Cert != "" {
// b.Cert can either be a certificate or public key
certBytes := []byte(b.Cert)
if isb64(certBytes) {
certBytes, _ = base64.StdEncoding.DecodeString(b.Cert)
}
cert, err = loadCertFromPEM(certBytes)
if err != nil {
// check if cert is actually a public key
co.SigVerifier, err = sigs.LoadPublicKeyRaw(certBytes, crypto.SHA256)
if err != nil {
return fmt.Errorf("loading verifier from rfc3161 timestamp bundle: %w", err)
}
}
if err := json.Unmarshal(ts, &rfc3161Timestamp); err != nil {
return err
}
opts = append(opts, static.WithRFC3161Timestamp(b.RFC3161Timestamp))
opts = append(opts, static.WithRFC3161Timestamp(&rfc3161Timestamp))
}
// Set an SCT if provided via the CLI.
if c.SCTRef != "" {
Expand Down Expand Up @@ -306,7 +293,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error {
}

// base64signature returns the base64 encoded signature
func base64signature(sigRef string, bundlePath, rfc3161TimestampPath string) (string, error) {
func base64signature(sigRef, bundlePath string) (string, error) {
var targetSig []byte
var err error
switch {
Expand All @@ -325,12 +312,6 @@ func base64signature(sigRef string, bundlePath, rfc3161TimestampPath string) (st
return "", err
}
targetSig = []byte(b.Base64Signature)
case rfc3161TimestampPath != "":
b, err := cosign.FetchLocalSignedPayloadFromPath(rfc3161TimestampPath)
if err != nil {
return "", err
}
targetSig = []byte(b.Base64Signature)
default:
return "", fmt.Errorf("missing flag '--signature'")
}
Expand Down
102 changes: 31 additions & 71 deletions cmd/cosign/cli/verify/verify_blob_test.go
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -550,6 +523,10 @@ func TestVerifyBlob(t *testing.T) {
expiredLeafPem, true)},
shouldErr: true,
},
// TODO: Add tests for TSA:
// * With or without bundle
// * Mismatched signature
// * Unexpired and expired certificate
}
for _, tt := range tts {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -1015,12 +992,6 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) {
// Create blob
blob := "someblob"

// Sign blob with private key
sig, err := signer.SignMessage(bytes.NewReader([]byte(blob)))
if err != nil {
t.Fatal(err)
}

// TODO: Replace with a full TSA mock client, related to https://github.com/sigstore/timestamp-authority/issues/146
viper.Set("timestamp-signer", "memory")
apiServer := server.NewRestAPIServer("localhost", 0, []string{"http"}, 10*time.Second, 10*time.Second)
Expand All @@ -1032,8 +1003,7 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) {
t.Error(err)
}

payloadSigner := payload.NewSigner(keyless.rekorSigner)

payloadSigner := payload.NewSigner(signer)
tsaSigner := tsa.NewSigner(payloadSigner, client)
var sigTSA oci.Signature
sigTSA, _, err = tsaSigner.Sign(context.Background(), bytes.NewReader([]byte(blob)))
Expand All @@ -1045,6 +1015,7 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) {
if err != nil {
t.Fatalf("unexpected error getting rfc3161 timestamp bundle: %v", err)
}
tsPath := writeTimestampFile(t, keyless.td, rfc3161Timestamp, "rfc3161TS.json")

chain, err := client.Timestamp.GetTimestampCertChain(nil)
if err != nil {
Expand All @@ -1058,8 +1029,16 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) {
defer os.Remove(tsaCertChainPath)

// Create bundle
b64Sig, err := sigTSA.Base64Signature()
if err != nil {
t.Fatal(err)
}
sig, err := base64.StdEncoding.DecodeString(b64Sig)
if err != nil {
t.Fatal(err)
}
entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig)
b := createRFC3161TimestampAndOrRekorBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry, rfc3161Timestamp.SignedRFC3161Timestamp)
b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry)
b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload)
bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json")
blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt")
Expand All @@ -1070,12 +1049,12 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) {
CertEmail: identity,
CertChain: os.Getenv("SIGSTORE_ROOT_FILE"),
SigRef: "", // Sig is fetched from bundle
KeyOpts: options.KeyOpts{BundlePath: bundlePath, TSACertChainPath: tsaCertChainPath},
KeyOpts: options.KeyOpts{BundlePath: bundlePath, TSACertChainPath: tsaCertChainPath, RFC3161TimestampPath: tsPath},
IgnoreSCT: true,
}
err = cmd.Exec(context.Background(), blobPath)
if err != nil {
t.Fatalf("expected success specifying the intermediates, got %v", err)
t.Fatalf("expected success verifying with timestamp, got %v", err)
}
})
t.Run("Explicit Fulcio chain with bundle in non-experimental mode", func(t *testing.T) {
Expand Down Expand Up @@ -1384,37 +1363,6 @@ func createBundle(_ *testing.T, sig []byte, certPem []byte, logID string, integr
return b
}

func createRFC3161TimestampAndOrRekorBundle(_ *testing.T, sig []byte, certPem []byte, logID string, integratedTime int64, rekorEntry string, rfc3161timestamp []byte) *cosign.LocalSignedPayload {
// Create bundle with:
// * Blob signature
// * Signing certificate
b := &cosign.LocalSignedPayload{
Base64Signature: base64.StdEncoding.EncodeToString(sig),
Cert: string(certPem),
}

if rekorEntry != "" {
// * Bundle with a payload and signature over the payload
b.Bundle = &bundle.RekorBundle{
SignedEntryTimestamp: []byte{},
Payload: bundle.RekorPayload{
LogID: logID,
IntegratedTime: integratedTime,
LogIndex: 1,
Body: rekorEntry,
},
}
}

if rfc3161timestamp != nil {
b.RFC3161Timestamp = &bundle.RFC3161Timestamp{
SignedRFC3161Timestamp: rfc3161timestamp,
}
}

return b
}

func createEntry(ctx context.Context, kind, apiVersion string, blobBytes, certBytes, sigBytes []byte) (types.EntryImpl, error) {
props := types.ArtifactProperties{
PublicKeyBytes: [][]byte{certBytes},
Expand Down Expand Up @@ -1475,3 +1423,15 @@ func writeBlobFile(t *testing.T, td string, blob string, name string) string {
}
return blobPath
}

func writeTimestampFile(t *testing.T, td string, ts *bundle.RFC3161Timestamp, name string) string {
jsonBundle, err := json.Marshal(ts)
if err != nil {
t.Fatal(err)
}
path := filepath.Join(td, name)
if err := os.WriteFile(path, jsonBundle, 0644); err != nil {
t.Fatal(err)
}
return path
}

0 comments on commit e9e9cb2

Please sign in to comment.