diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 5cbd0679bb1..550f4cb175c 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/internal/pkg/cosign/fulcio/signer.go b/internal/pkg/cosign/fulcio/signer.go index 18ba60c0828..3dbfb3ac8be 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/fulcio/signer_test.go b/internal/pkg/cosign/fulcio/signer_test.go new file mode 100644 index 00000000000..53077cd2687 --- /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/mock_rekor_client.go b/internal/pkg/cosign/rekor/mock_rekor_client.go new file mode 100644 index 00000000000..23facc2fb94 --- /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 9293797df0a..51b185ef0ed 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/internal/pkg/cosign/rekor/signer_test.go b/internal/pkg/cosign/rekor/signer_test.go new file mode 100644 index 00000000000..89f6e03ed1c --- /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.go b/pkg/cosign/tuf/client.go index 8ca95d1bb78..124133c076c 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/client_test.go b/pkg/cosign/tuf/client_test.go index 46d079225af..bfdd2b16c6a 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/cosign/tuf/timestamp.go b/pkg/cosign/tuf/timestamp.go new file mode 100644 index 00000000000..57c35aaa4b4 --- /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, ×tamp); err != nil { + return nil, errors.Wrap(err, "unable to unmarshal timestamp") + } + return ×tamp, nil +} + +type Timestamp struct { + Signatures []data.Signature `json:"signatures"` + Signed data.Timestamp `json:"signed"` +} diff --git a/pkg/oci/internal/signature/layer.go b/pkg/oci/internal/signature/layer.go index e042ce1a7db..e84cbd2870d 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/internal/signature/layer_test.go b/pkg/oci/internal/signature/layer_test.go index a3ea0d80353..10618dde9c0 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/options.go b/pkg/oci/mutate/options.go index 0c19f3da7f5..7d55ea942f3 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 dd2a7ab6808..49ed98b23a0 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/mutate/signature_test.go b/pkg/oci/mutate/signature_test.go index 1e17e9bcf0b..7f26f82fa6f 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/signatures.go b/pkg/oci/signatures.go index e66b7c6c9e1..c791d88ec9f 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 e00b6cdb4e5..03dc863a114 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/options_test.go b/pkg/oci/static/options_test.go index 63600c791ab..ca7081067c5 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.go b/pkg/oci/static/signature.go index 882ff87e7df..466f1eba99a 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)) diff --git a/pkg/oci/static/signature_test.go b/pkg/oci/static/signature_test.go index 1ef5beccb1d..affdbf5062a 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 {