// // Copyright 2021 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 signature import ( "bytes" "encoding/base64" "errors" "fmt" "io" "strings" "testing" "github.com/google/go-cmp/cmp" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/go-containerregistry/pkg/v1/types" "github.com/sigstore/cosign/v2/pkg/cosign/bundle" ) func mustDecode(s string) []byte { b, err := base64.StdEncoding.DecodeString(s) if err != nil { panic(err.Error()) } return b } func TestSignature(t *testing.T) { layer, err := random.Layer(300 /* byteSize */, types.DockerLayer) if err != nil { t.Fatalf("random.Layer() = %v", err) } digest, err := layer.Digest() if err != nil { t.Fatalf("Digest() = %v", err) } 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: "just payload and signature", l: &sigLayer{ Layer: layer, desc: v1.Descriptor{ Digest: digest, Annotations: map[string]string{ sigkey: "blah", }, }, }, wantSig: "blah", }, { name: "with empty other keys", l: &sigLayer{ Layer: layer, desc: v1.Descriptor{ Digest: digest, Annotations: map[string]string{ sigkey: "blah", certkey: "", chainkey: "", BundleKey: "", }, }, }, wantSig: "blah", }, { name: "missing signature", l: &sigLayer{ Layer: layer, desc: v1.Descriptor{ Digest: digest, }, }, wantSigErr: fmt.Errorf("signature layer %s is missing %q annotation", digest, sigkey), }, { name: "min plus bad bundle", l: &sigLayer{ Layer: layer, desc: v1.Descriptor{ Digest: digest, Annotations: map[string]string{ sigkey: "blah", BundleKey: `}`, }, }, }, wantSig: "blah", wantBundleErr: errors.New(`unmarshaling bundle: invalid character '}' looking for beginning of value`), }, { name: "min plus bad cert", l: &sigLayer{ Layer: layer, desc: v1.Descriptor{ Digest: digest, Annotations: map[string]string{ sigkey: "blah", certkey: `GARBAGE`, }, }, }, wantSig: "blah", wantCertErr: errors.New(`error during PEM decoding`), }, { name: "min plus bad chain", l: &sigLayer{ Layer: layer, desc: v1.Descriptor{ Digest: digest, Annotations: map[string]string{ sigkey: "blah", chainkey: `GARBAGE`, }, }, }, wantSig: "blah", wantChainErr: errors.New(`error during PEM decoding`), }, { name: "min plus bundle", l: &sigLayer{ Layer: layer, desc: v1.Descriptor{ Digest: digest, Annotations: map[string]string{ sigkey: "blah", // This was extracted from gcr.io/distroless/static:nonroot on 2021/09/16. // The Body has been removed for brevity BundleKey: `{"SignedEntryTimestamp":"MEUCIQClUkUqZNf+6dxBc/pxq22JIluTB7Kmip1G0FIF5E0C1wIgLqXm+IM3JYW/P/qjMZSXW+J8bt5EOqNfe3R+0A9ooFE=","Payload":{"body":"REMOVED","integratedTime":1631646761,"logIndex":693591,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}`, }, }, }, wantSig: "blah", wantBundle: &bundle.RekorBundle{ SignedEntryTimestamp: mustDecode("MEUCIQClUkUqZNf+6dxBc/pxq22JIluTB7Kmip1G0FIF5E0C1wIgLqXm+IM3JYW/P/qjMZSXW+J8bt5EOqNfe3R+0A9ooFE="), Payload: bundle.RekorPayload{ Body: "REMOVED", IntegratedTime: 1631646761, LogIndex: 693591, LogID: "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", }, }, }, { name: "min plus good cert", l: &sigLayer{ Layer: layer, desc: v1.Descriptor{ Digest: digest, Annotations: map[string]string{ sigkey: "blah", // This was extracted from gcr.io/distroless/static:nonroot on 2021/09/16 certkey: ` -----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----- `, }, }, }, wantSig: "blah", wantCert: true, }, { name: "min plus bad chain", l: &sigLayer{ Layer: layer, desc: v1.Descriptor{ Digest: digest, Annotations: map[string]string{ sigkey: "blah", // This was extracted from gcr.io/distroless/static:nonroot on 2021/09/16 chainkey: ` -----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----- `, }, }, }, wantSig: "blah", wantChain: 1, }} for _, test := range tests { t.Run(test.name, func(t *testing.T) { b, err := test.l.Payload() switch { case (err != nil) != (test.wantPayloadErr != nil): t.Errorf("Payload() = %v, wanted %v", err, test.wantPayloadErr) case (err != nil) && (test.wantPayloadErr != nil) && err.Error() != test.wantPayloadErr.Error(): t.Errorf("Payload() = %v, wanted %v", err, test.wantPayloadErr) case err == nil: if got, _, err := v1.SHA256(bytes.NewBuffer(b)); err != nil { t.Errorf("v1.SHA256() = %v", err) } else if want := digest; want != got { t.Errorf("v1.SHA256() = %v, wanted %v", got, want) } } switch got, err := test.l.Base64Signature(); { case (err != nil) != (test.wantSigErr != nil): t.Errorf("Base64Signature() = %v, wanted %v", err, test.wantSigErr) case (err != nil) && (test.wantSigErr != nil) && err.Error() != test.wantSigErr.Error(): t.Errorf("Base64Signature() = %v, wanted %v", err, test.wantSigErr) case got != test.wantSig: t.Errorf("Base64Signature() = %v, wanted %v", got, test.wantSig) } switch got, err := test.l.Cert(); { case (err != nil) != (test.wantCertErr != nil): t.Errorf("Cert() = %v, wanted %v", err, test.wantCertErr) case (err != nil) && (test.wantCertErr != nil) && err.Error() != test.wantCertErr.Error(): t.Errorf("Cert() = %v, wanted %v", err, test.wantCertErr) case (got != nil) != test.wantCert: t.Errorf("Cert() = %v, wanted cert? %v", got, test.wantCert) } switch got, err := test.l.Chain(); { case (err != nil) != (test.wantChainErr != nil): t.Errorf("Chain() = %v, wanted %v", err, test.wantChainErr) case (err != nil) && (test.wantChainErr != nil) && err.Error() != test.wantChainErr.Error(): t.Errorf("Chain() = %v, wanted %v", err, test.wantChainErr) case len(got) != test.wantChain: t.Errorf("Chain() = %v, wanted chain of length %d", got, test.wantChain) } switch got, err := test.l.Bundle(); { case (err != nil) != (test.wantBundleErr != nil): t.Errorf("Bundle() = %v, wanted %v", err, test.wantBundleErr) case (err != nil) && (test.wantBundleErr != nil) && err.Error() != test.wantBundleErr.Error(): t.Errorf("Bundle() = %v, wanted %v", err, test.wantBundleErr) case !cmp.Equal(got, test.wantBundle): t.Errorf("Bundle() %s", cmp.Diff(got, test.wantBundle)) } }) } } func TestSignatureWithTSAAnnotation(t *testing.T) { layer, err := random.Layer(300 /* byteSize */, types.DockerLayer) if err != nil { t.Fatalf("random.Layer() = %v", err) } digest, err := layer.Digest() if err != nil { t.Fatalf("Digest() = %v", err) } tests := []struct { name string l *sigLayer env map[string]string wantPayloadErr error wantSig string wantSigErr error wantCert bool wantCertErr error wantChain int wantChainErr error wantBundle *bundle.RFC3161Timestamp wantBundleErr error }{{ name: "just payload and signature", l: &sigLayer{ Layer: layer, desc: v1.Descriptor{ Digest: digest, Annotations: map[string]string{ sigkey: "blah", }, }, }, wantSig: "blah", }, { name: "with empty other keys", l: &sigLayer{ Layer: layer, desc: v1.Descriptor{ Digest: digest, Annotations: map[string]string{ sigkey: "blah", certkey: "", chainkey: "", RFC3161TimestampKey: "", }, }, }, wantSig: "blah", }, { name: "missing signature", l: &sigLayer{ Layer: layer, desc: v1.Descriptor{ Digest: digest, }, }, wantSigErr: fmt.Errorf("signature layer %s is missing %q annotation", digest, sigkey), }, { name: "min plus bad RFC3161 timestamp bundle", l: &sigLayer{ Layer: layer, desc: v1.Descriptor{ Digest: digest, Annotations: map[string]string{ sigkey: "blah", RFC3161TimestampKey: `}`, }, }, }, wantSig: "blah", wantBundleErr: errors.New(`unmarshaling RFC3161 timestamp bundle: invalid character '}' looking for beginning of value`), }, { name: "min plus bad cert", l: &sigLayer{ Layer: layer, desc: v1.Descriptor{ Digest: digest, Annotations: map[string]string{ sigkey: "blah", certkey: `GARBAGE`, }, }, }, wantSig: "blah", wantCertErr: errors.New(`error during PEM decoding`), }, { name: "min plus bad chain", l: &sigLayer{ Layer: layer, desc: v1.Descriptor{ Digest: digest, Annotations: map[string]string{ sigkey: "blah", chainkey: `GARBAGE`, }, }, }, wantSig: "blah", wantChainErr: errors.New(`error during PEM decoding`), }, { name: "min plus RFC3161 timestamp bundle", l: &sigLayer{ Layer: layer, desc: v1.Descriptor{ Digest: digest, Annotations: map[string]string{ sigkey: "tsa blah", // This was extracted from gcr.io/distroless/static:nonroot on 2021/09/16. // The Body has been removed for brevity RFC3161TimestampKey: `{"SignedRFC3161Timestamp":"MEUCIQClUkUqZNf+6dxBc/pxq22JIluTB7Kmip1G0FIF5E0C1wIgLqXm+IM3JYW/P/qjMZSXW+J8bt5EOqNfe3R+0A9ooFE=","Payload":{"body":"REMOVED","integratedTime":1631646761,"logIndex":693591,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}`, }, }, }, wantSig: "tsa blah", wantBundle: &bundle.RFC3161Timestamp{ SignedRFC3161Timestamp: mustDecode("MEUCIQClUkUqZNf+6dxBc/pxq22JIluTB7Kmip1G0FIF5E0C1wIgLqXm+IM3JYW/P/qjMZSXW+J8bt5EOqNfe3R+0A9ooFE="), }, }, { name: "payload size exceeds default limit", l: &sigLayer{ Layer: &mockLayer{size: 134217728 + 42}, // 128MiB + 42 bytes }, wantPayloadErr: errors.New("size of layer (134217770) exceeded the limit (134217728)"), }, { name: "payload size exceeds overridden limit", l: &sigLayer{ Layer: &mockLayer{size: 1000000000 + 42}, // 1GB + 42 bytes }, env: map[string]string{"COSIGN_MAX_ATTACHMENT_SIZE": "1GB"}, wantPayloadErr: errors.New("size of layer (1000000042) exceeded the limit (1000000000)"), }, { name: "payload size is within overridden limit", l: &sigLayer{ Layer: layer, desc: v1.Descriptor{ Digest: digest, Annotations: map[string]string{ sigkey: "blah", }, }, }, env: map[string]string{"COSIGN_MAX_ATTACHMENT_SIZE": "5KB"}, wantSig: "blah", }} for _, test := range tests { t.Run(test.name, func(t *testing.T) { for k, v := range test.env { t.Setenv(k, v) } b, err := test.l.Payload() switch { case (err != nil) != (test.wantPayloadErr != nil): t.Errorf("Payload() = %v, wanted %v", err, test.wantPayloadErr) case (err != nil) && (test.wantPayloadErr != nil) && err.Error() != test.wantPayloadErr.Error(): t.Errorf("Payload() = %v, wanted %v", err, test.wantPayloadErr) case err == nil: if got, _, err := v1.SHA256(bytes.NewBuffer(b)); err != nil { t.Errorf("v1.SHA256() = %v", err) } else if want := digest; want != got { t.Errorf("v1.SHA256() = %v, wanted %v", got, want) } } if err != nil { return } switch got, err := test.l.Base64Signature(); { case (err != nil) != (test.wantSigErr != nil): t.Errorf("Base64Signature() = %v, wanted %v", err, test.wantSigErr) case (err != nil) && (test.wantSigErr != nil) && err.Error() != test.wantSigErr.Error(): t.Errorf("Base64Signature() = %v, wanted %v", err, test.wantSigErr) case got != test.wantSig: t.Errorf("Base64Signature() = %v, wanted %v", got, test.wantSig) } switch got, err := test.l.Cert(); { case (err != nil) != (test.wantCertErr != nil): t.Errorf("Cert() = %v, wanted %v", err, test.wantCertErr) case (err != nil) && (test.wantCertErr != nil) && err.Error() != test.wantCertErr.Error(): t.Errorf("Cert() = %v, wanted %v", err, test.wantCertErr) case (got != nil) != test.wantCert: t.Errorf("Cert() = %v, wanted cert? %v", got, test.wantCert) } switch got, err := test.l.Chain(); { case (err != nil) != (test.wantChainErr != nil): t.Errorf("Chain() = %v, wanted %v", err, test.wantChainErr) case (err != nil) && (test.wantChainErr != nil) && err.Error() != test.wantChainErr.Error(): t.Errorf("Chain() = %v, wanted %v", err, test.wantChainErr) case len(got) != test.wantChain: t.Errorf("Chain() = %v, wanted chain of length %d", got, test.wantChain) } switch got, err := test.l.RFC3161Timestamp(); { case (err != nil) != (test.wantBundleErr != nil): t.Errorf("RFC3161Timestamp() = %v, wanted %v", err, test.wantBundleErr) case (err != nil) && (test.wantBundleErr != nil) && err.Error() != test.wantBundleErr.Error(): t.Errorf("RFC3161Timestamp() = %v, wanted %v", err, test.wantBundleErr) case !cmp.Equal(got, test.wantBundle): t.Errorf("RFC3161Timestamp() %s", cmp.Diff(got, test.wantBundle)) } }) } } type mockLayer struct { size int64 } func (m *mockLayer) Size() (int64, error) { return m.size, nil } func (m *mockLayer) Compressed() (io.ReadCloser, error) { return io.NopCloser(strings.NewReader("data")), nil } func (m *mockLayer) Digest() (v1.Hash, error) { panic("not implemented") } func (m *mockLayer) DiffID() (v1.Hash, error) { panic("not implemented") } func (m *mockLayer) Uncompressed() (io.ReadCloser, error) { panic("not implemented") } func (m *mockLayer) MediaType() (types.MediaType, error) { panic("not implemented") }