From a868d651df462ec38b152bf56eff877918c55229 Mon Sep 17 00:00:00 2001 From: Matt Moore Date: Thu, 16 Sep 2021 18:39:05 -0700 Subject: [PATCH] Start to flesh out `Signatures` and `internal/oci/remote` (#699) Starting with `cosign.FetchSignaturesForImageDigest`, I am starting to peel out some of the existing logic, and try to define useful interfaces within `internal/oci.Signatures` and implement them in `internal/oci/remote.Signatures`. This is still somewhat rough in parts, so I suspect that as we build it up, we will iterates on parts of it. --- cmd/cosign/cli/sign.go | 7 +- internal/oci/remote/remote.go | 148 +++++++++++++ internal/oci/remote/remote_test.go | 334 +++++++++++++++++++++++++++++ internal/oci/signatures.go | 49 ++++- pkg/cosign/fetch.go | 73 ++----- pkg/cosign/keys.go | 6 +- pkg/cosign/remote/remote.go | 16 +- pkg/cosign/tlog.go | 4 +- pkg/cosign/verify.go | 4 +- 9 files changed, 560 insertions(+), 81 deletions(-) create mode 100644 internal/oci/remote/remote.go create mode 100644 internal/oci/remote/remote_test.go diff --git a/cmd/cosign/cli/sign.go b/cmd/cosign/cli/sign.go index e8cb2052e07..293533ad0ab 100644 --- a/cmd/cosign/cli/sign.go +++ b/cmd/cosign/cli/sign.go @@ -39,6 +39,7 @@ import ( "github.com/pkg/errors" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioverifier" + "github.com/sigstore/cosign/internal/oci" "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/cosign/pkg/cosign/pivkey" cremote "github.com/sigstore/cosign/pkg/cosign/remote" @@ -378,13 +379,13 @@ func SignCmd(ctx context.Context, ko KeyOpts, regOpts RegistryOpts, annotations return nil } -func bundle(entry *models.LogEntryAnon) *cremote.Bundle { +func bundle(entry *models.LogEntryAnon) *oci.Bundle { if entry.Verification == nil { return nil } - return &cremote.Bundle{ + return &oci.Bundle{ SignedEntryTimestamp: entry.Verification.SignedEntryTimestamp, - Payload: cremote.BundlePayload{ + Payload: oci.BundlePayload{ Body: entry.Body, IntegratedTime: *entry.IntegratedTime, LogIndex: *entry.LogIndex, diff --git a/internal/oci/remote/remote.go b/internal/oci/remote/remote.go new file mode 100644 index 00000000000..396eb0d58a6 --- /dev/null +++ b/internal/oci/remote/remote.go @@ -0,0 +1,148 @@ +// +// 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 remote + +import ( + "crypto/x509" + "encoding/json" + "fmt" + "io/ioutil" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/pkg/errors" + "github.com/sigstore/cosign/internal/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" +) + +// This enables mocking for unit testing without faking an entire registry. +var remoteImage = remote.Image + +// Signatures fetches the signatures image represented by the named reference. +func Signatures(ref name.Reference, opts ...remote.Option) (oci.Signatures, error) { + img, err := remoteImage(ref, opts...) + if err != nil { + return nil, err + } + return &sigs{ + Image: img, + }, nil +} + +type sigs struct { + v1.Image +} + +var _ oci.Signatures = (*sigs)(nil) + +// Get implements oci.Signatures +func (s *sigs) Get() ([]oci.Signature, error) { + m, err := s.Manifest() + if err != nil { + return nil, err + } + signatures := make([]oci.Signature, 0, len(m.Layers)) + for _, desc := range m.Layers { + signatures = append(signatures, &sigLayer{ + img: s, + desc: desc, + }) + } + return signatures, nil +} + +type sigLayer struct { + img *sigs + desc v1.Descriptor +} + +var _ oci.Signature = (*sigLayer)(nil) + +// Payload implements oci.Signature +func (s *sigLayer) Payload() ([]byte, error) { + l, err := s.img.LayerByDigest(s.desc.Digest) + if err != nil { + return nil, err + } + + // Compressed is a misnomer here, we just want the raw bytes from the registry. + r, err := l.Compressed() + if err != nil { + return nil, err + } + payload, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + return payload, nil +} + +// Base64Signature implements oci.Signature +func (s *sigLayer) Base64Signature() (string, error) { + b64sig, ok := s.desc.Annotations[sigkey] + if !ok { + return "", fmt.Errorf("signature layer %s is missing %q annotation", s.desc.Digest, sigkey) + } + return b64sig, nil +} + +// Cert implements oci.Signature +func (s *sigLayer) Cert() (*x509.Certificate, error) { + certPEM, ok := s.desc.Annotations[certkey] + if !ok { + return nil, nil + } + certs, err := cryptoutils.LoadCertificatesFromPEM(strings.NewReader(certPEM)) + if err != nil { + return nil, err + } + return certs[0], nil +} + +// Chain implements oci.Signature +func (s *sigLayer) Chain() ([]*x509.Certificate, error) { + chainPEM, ok := s.desc.Annotations[chainkey] + if !ok { + return nil, nil + } + certs, err := cryptoutils.LoadCertificatesFromPEM(strings.NewReader(chainPEM)) + if err != nil { + return nil, err + } + return certs, nil +} + +// Bundle implements oci.Signature +func (s *sigLayer) Bundle() (*oci.Bundle, error) { + bundle := s.desc.Annotations[BundleKey] + if bundle == "" { + return nil, nil + } + var b oci.Bundle + if err := json.Unmarshal([]byte(bundle), &b); err != nil { + return nil, errors.Wrap(err, "unmarshaling bundle") + } + return &b, nil +} diff --git a/internal/oci/remote/remote_test.go b/internal/oci/remote/remote_test.go new file mode 100644 index 00000000000..b4195344d00 --- /dev/null +++ b/internal/oci/remote/remote_test.go @@ -0,0 +1,334 @@ +// +// 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 remote + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/sigstore/cosign/internal/oci" + "github.com/sigstore/cosign/internal/oci/empty" +) + +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 *oci.Bundle + wantBundleErr error + }{{ + name: "just payload and signature", + l: &sigLayer{ + img: &sigs{ + Image: must(mutate.Append(empty.Image(), mutate.Addendum{Layer: layer})), + }, + desc: v1.Descriptor{ + Digest: digest, + Annotations: map[string]string{ + sigkey: "blah", + }, + }, + }, + wantSig: "blah", + }, { + name: "bad digest", + l: &sigLayer{ + img: &sigs{ + Image: must(mutate.Append(empty.Image(), mutate.Addendum{Layer: layer})), + }, + desc: v1.Descriptor{ + Digest: v1.Hash{Algorithm: "bad", Hex: "f00d"}, + Annotations: map[string]string{ + sigkey: "blah", + }, + }, + }, + wantPayloadErr: errors.New("unknown blob bad:f00d"), + wantSig: "blah", + }, { + name: "missing signature", + l: &sigLayer{ + img: &sigs{ + Image: must(mutate.Append(empty.Image(), mutate.Addendum{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{ + img: &sigs{ + Image: must(mutate.Append(empty.Image(), mutate.Addendum{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{ + img: &sigs{ + Image: must(mutate.Append(empty.Image(), mutate.Addendum{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{ + img: &sigs{ + Image: must(mutate.Append(empty.Image(), mutate.Addendum{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{ + img: &sigs{ + Image: must(mutate.Append(empty.Image(), mutate.Addendum{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: &oci.Bundle{ + SignedEntryTimestamp: mustDecode("MEUCIQClUkUqZNf+6dxBc/pxq22JIluTB7Kmip1G0FIF5E0C1wIgLqXm+IM3JYW/P/qjMZSXW+J8bt5EOqNfe3R+0A9ooFE="), + Payload: oci.BundlePayload{ + Body: "REMOVED", + IntegratedTime: 1631646761, + LogIndex: 693591, + LogID: "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", + }, + }, + }, { + name: "min plus good cert", + l: &sigLayer{ + img: &sigs{ + Image: must(mutate.Append(empty.Image(), mutate.Addendum{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{ + img: &sigs{ + Image: must(mutate.Append(empty.Image(), mutate.Addendum{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 TestSignatures(t *testing.T) { + ri := remote.Image + t.Cleanup(func() { + remoteImage = ri + }) + wantLayers := int64(27) + remoteImage = func(ref name.Reference, options ...remote.Option) (v1.Image, error) { + return random.Image(300 /* byteSize */, wantLayers) + } + + ref, err := name.ParseReference("gcr.io/distroless/static:nonroot") + if err != nil { + t.Fatalf("ParseRef() = %v", err) + } + + sigs, err := Signatures(ref) + if err != nil { + t.Fatalf("Signatures() = %v", err) + } + + if sl, err := sigs.Get(); err != nil { + t.Errorf("Get() = %v", err) + } else if got := int64(len(sl)); got != wantLayers { + t.Errorf("len(Get()) = %d, wanted %d", got, wantLayers) + } +} + +func must(img v1.Image, err error) v1.Image { + if err != nil { + panic(err.Error()) + } + return img +} + +func mustDecode(s string) []byte { + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + panic(err.Error()) + } + return b +} diff --git a/internal/oci/signatures.go b/internal/oci/signatures.go index d3c1972a951..cc1435dac2b 100644 --- a/internal/oci/signatures.go +++ b/internal/oci/signatures.go @@ -15,7 +15,12 @@ package oci -import v1 "github.com/google/go-containerregistry/pkg/v1" +import ( + "crypto/x509" + + "github.com/go-openapi/strfmt" + v1 "github.com/google/go-containerregistry/pkg/v1" +) // Signatures represents a set of signatures that are associated with a particular // v1.Image. @@ -25,4 +30,46 @@ type Signatures interface { // TODO(mattmoor): Accessors that build on `v1.Image` to provide // higher-level accessors for the signature data that is embedded // in the wrapped `v1.Image` + + // Get retrieves the list of signatures stored. + Get() ([]Signature, error) +} + +// Signature holds a single image signature. +type Signature interface { + // Payload fetches the opaque data that is being signed. + // This will always return data when there is no error. + Payload() ([]byte, error) + + // Base64Signature fetches the base64 encoded signature + // of the payload. This will always return data when + // there is no error. + Base64Signature() (string, error) + + // Cert fetches the optional public key from the key pair that + // was used to sign the payload. + Cert() (*x509.Certificate, error) + + // Chain fetches the optional "full certificate chain" rooted + // at a Fulcio CA, the leaf of which was used to sign the + // payload. + Chain() ([]*x509.Certificate, error) + + // Bundle fetches the optional metadata that records the ephemeral + // Fulcio key in the transparency log. + Bundle() (*Bundle, error) +} + +// Bundle holds metadata about recording a Signature's ephemeral key to +// a Rekor transparency log. +type Bundle struct { + SignedEntryTimestamp strfmt.Base64 + Payload BundlePayload +} + +type BundlePayload struct { + Body interface{} `json:"body"` + IntegratedTime int64 `json:"integratedTime"` + LogIndex int64 `json:"logIndex"` + LogID string `json:"logID"` } diff --git a/pkg/cosign/fetch.go b/pkg/cosign/fetch.go index 5efd60610d8..c8a1282afd4 100644 --- a/pkg/cosign/fetch.go +++ b/pkg/cosign/fetch.go @@ -16,11 +16,8 @@ package cosign import ( - "bytes" "context" "crypto/x509" - "encoding/json" - "io/ioutil" "runtime" "strings" @@ -30,8 +27,8 @@ import ( "github.com/pkg/errors" "knative.dev/pkg/pool" - cremote "github.com/sigstore/cosign/pkg/cosign/remote" - "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/cosign/internal/oci" + ociremote "github.com/sigstore/cosign/internal/oci/remote" ) type SignedPayload struct { @@ -39,7 +36,7 @@ type SignedPayload struct { Payload []byte Cert *x509.Certificate Chain []*x509.Certificate - Bundle *cremote.Bundle + Bundle *oci.Bundle bundleVerified bool } @@ -82,72 +79,38 @@ func FetchSignaturesForImage(ctx context.Context, signedImgRef name.Reference, s func FetchSignaturesForImageDigest(ctx context.Context, signedImageDigest v1.Hash, sigRepo name.Repository, sigTagSuffix string, registryOpts ...remote.Option) ([]SignedPayload, error) { tag := AttachedImageTag(sigRepo, signedImageDigest, sigTagSuffix) - sigImg, err := remote.Image(tag, registryOpts...) + sigs, err := ociremote.Signatures(tag, registryOpts...) if err != nil { return nil, errors.Wrap(err, "remote image") } - - m, err := sigImg.Manifest() + l, err := sigs.Get() if err != nil { - return nil, errors.Wrap(err, "manifest") + return nil, errors.Wrap(err, "fetching signatures") } g := pool.New(runtime.NumCPU()) - signatures := make([]SignedPayload, len(m.Layers)) - for i, desc := range m.Layers { - i, desc := i, desc - g.Go(func() error { - base64sig, ok := desc.Annotations[sigkey] - if !ok { - return nil - } - l, err := sigImg.LayerByDigest(desc.Digest) + signatures := make([]SignedPayload, len(l)) + for i, sig := range l { + i, sig := i, sig + g.Go(func() (err error) { + signatures[i].Payload, err = sig.Payload() if err != nil { return err } - - // Compressed is a misnomer here, we just want the raw bytes from the registry. - r, err := l.Compressed() + signatures[i].Base64Signature, err = sig.Base64Signature() if err != nil { return err } - payload, err := ioutil.ReadAll(r) + signatures[i].Cert, err = sig.Cert() if err != nil { return err } - sp := SignedPayload{ - Payload: payload, - Base64Signature: base64sig, - } - // We may have a certificate and chain - certPem := desc.Annotations[certkey] - if certPem != "" { - certs, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader([]byte(certPem))) - if err != nil { - return err - } - sp.Cert = certs[0] - } - chainPem := desc.Annotations[chainkey] - if chainPem != "" { - certs, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader([]byte(chainPem))) - if err != nil { - return err - } - sp.Chain = certs - } - - bundle := desc.Annotations[BundleKey] - if bundle != "" { - var b cremote.Bundle - if err := json.Unmarshal([]byte(bundle), &b); err != nil { - return errors.Wrap(err, "unmarshaling bundle") - } - sp.Bundle = &b + signatures[i].Chain, err = sig.Chain() + if err != nil { + return err } - - signatures[i] = sp - return nil + signatures[i].Bundle, err = sig.Bundle() + return err }) } if err := g.Wait(); err != nil { diff --git a/pkg/cosign/keys.go b/pkg/cosign/keys.go index 173791fc071..ab88e6a4987 100644 --- a/pkg/cosign/keys.go +++ b/pkg/cosign/keys.go @@ -28,6 +28,7 @@ import ( "github.com/pkg/errors" "github.com/theupdateframework/go-tuf/encrypted" + "github.com/sigstore/cosign/pkg/cosign/remote" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" ) @@ -35,10 +36,7 @@ import ( const ( PrivakeKeyPemType = "ENCRYPTED COSIGN PRIVATE KEY" - sigkey = "dev.cosignproject.cosign/signature" - certkey = "dev.sigstore.cosign/certificate" - chainkey = "dev.sigstore.cosign/chain" - BundleKey = "dev.sigstore.cosign/bundle" + BundleKey = remote.BundleKey ) type PassFunc func(bool) ([]byte, error) diff --git a/pkg/cosign/remote/remote.go b/pkg/cosign/remote/remote.go index bffa631b438..aad5b110701 100644 --- a/pkg/cosign/remote/remote.go +++ b/pkg/cosign/remote/remote.go @@ -22,7 +22,6 @@ import ( "io/ioutil" "net/http" - "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -32,6 +31,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/types" "github.com/pkg/errors" + "github.com/sigstore/cosign/internal/oci" "github.com/sigstore/cosign/internal/oci/empty" ctypes "github.com/sigstore/cosign/pkg/types" "github.com/sigstore/sigstore/pkg/signature" @@ -110,23 +110,11 @@ LayerLoop: return nil, nil } -type BundlePayload struct { - Body interface{} `json:"body"` - IntegratedTime int64 `json:"integratedTime"` - LogIndex int64 `json:"logIndex"` - LogID string `json:"logID"` -} - -type Bundle struct { - SignedEntryTimestamp strfmt.Base64 - Payload BundlePayload -} - type UploadOpts struct { Cert []byte Chain []byte DupeDetector signature.Verifier - Bundle *Bundle + Bundle *oci.Bundle AdditionalAnnotations map[string]string RemoteOpts []remote.Option MediaType string diff --git a/pkg/cosign/tlog.go b/pkg/cosign/tlog.go index 0f935f95513..b4882e370c3 100644 --- a/pkg/cosign/tlog.go +++ b/pkg/cosign/tlog.go @@ -31,7 +31,7 @@ import ( "github.com/google/trillian/merkle/rfc6962/hasher" "github.com/pkg/errors" - cremote "github.com/sigstore/cosign/pkg/cosign/remote" + "github.com/sigstore/cosign/internal/oci" "github.com/sigstore/cosign/pkg/cosign/tuf" "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/client/entries" @@ -225,7 +225,7 @@ func verifyTLogEntry(rekorClient *client.Rekor, uuid string) (*models.LogEntryAn return nil, errors.Wrap(err, "rekor public key pem to ecdsa") } - payload := cremote.BundlePayload{ + payload := oci.BundlePayload{ Body: e.Body, IntegratedTime: *e.IntegratedTime, LogIndex: *e.LogIndex, diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 9e10d17bdb1..45141d10d1d 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -33,7 +33,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/pkg/errors" - cremote "github.com/sigstore/cosign/pkg/cosign/remote" + "github.com/sigstore/cosign/internal/oci" rekor "github.com/sigstore/rekor/pkg/client" "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/sigstore/pkg/cryptoutils" @@ -288,7 +288,7 @@ func (sp *SignedPayload) VerifyBundle() (bool, error) { return true, nil } -func VerifySET(bundlePayload cremote.BundlePayload, signature []byte, pub *ecdsa.PublicKey) error { +func VerifySET(bundlePayload oci.BundlePayload, signature []byte, pub *ecdsa.PublicKey) error { contents, err := json.Marshal(bundlePayload) if err != nil { return errors.Wrap(err, "marshaling")