Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates to Timestamp signing and verification #2499

Merged
merged 8 commits into from
Dec 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cmd/cosign/cli/options/signblob.go
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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)
hectorj2f marked this conversation as resolved.
Show resolved Hide resolved
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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we do these checks before signing etc.?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a just-in-case check, since TimestampToRFC3161Timestamp might return nil if respBytes is empty for some reason.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Maybe both wouldn't be a bad idea, up to you

}
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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")
haydentherapper marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This basically removes tests that we had in place. I am not sure we want to change functionality and remove tests. How does it work if I only sign/verify a blob without using tlog ?

Copy link
Contributor Author

@haydentherapper haydentherapper Dec 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I plan to add some more tests in, but the presence of the bundle is just a way to get the signature and/or other timestamp. If you don't use the tlog, then you must provide the cert/sig the same way you currently do, via --certificate/--signature flags.

This PR should only be adding in tests, and refactoring tests based on the proposed changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With these changes, it actually requires less tests since this is only an additional flag that affects verification, rather than modifying codepaths that read in the cert/signature.

// * 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
}
Loading