diff --git a/cmd/cosign/cli/download/signature.go b/cmd/cosign/cli/download/signature.go index d911bd0e6ebc..8abd5a226077 100644 --- a/cmd/cosign/cli/download/signature.go +++ b/cmd/cosign/cli/download/signature.go @@ -25,6 +25,7 @@ import ( "github.com/peterbourgon/ff/v3/ffcli" "github.com/sigstore/cosign/cmd/cosign/cli" + ociremote "github.com/sigstore/cosign/internal/oci/remote" "github.com/sigstore/cosign/pkg/cosign" ) @@ -53,12 +54,8 @@ func SignatureCmd(ctx context.Context, regOpts cli.RegistryOpts, imageRef string if err != nil { return err } - sigRepo, err := cli.TargetRepositoryForImage(ref) - if err != nil { - return err - } regClientOpts := regOpts.GetRegistryClientOpts(ctx) - signatures, err := cosign.FetchSignaturesForImage(ctx, ref, sigRepo, cosign.SignatureTagSuffix, regClientOpts...) + signatures, err := cosign.FetchSignaturesForImage(ctx, ref, ociremote.WithRemoteOptions(regClientOpts...)) if err != nil { return err } diff --git a/cmd/cosign/cli/util.go b/cmd/cosign/cli/util.go index 29d3041927e0..1921da0b510b 100644 --- a/cmd/cosign/cli/util.go +++ b/cmd/cosign/cli/util.go @@ -32,11 +32,12 @@ import ( "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/kms" + + oremote "github.com/sigstore/cosign/internal/oci/remote" ) const ( ExperimentalEnv = "COSIGN_EXPERIMENTAL" - repoEnv = "COSIGN_REPOSITORY" ) func EnableExperimental() bool { @@ -47,7 +48,7 @@ func EnableExperimental() bool { } func TargetRepositoryForImage(img name.Reference) (name.Repository, error) { - wantRepo := os.Getenv(repoEnv) + wantRepo := os.Getenv(oremote.RepoOverrideKey) if wantRepo == "" { return img.Context(), nil } diff --git a/cmd/cosign/cli/util_test.go b/cmd/cosign/cli/util_test.go index 5d90550a7c90..403f0e6a9c55 100644 --- a/cmd/cosign/cli/util_test.go +++ b/cmd/cosign/cli/util_test.go @@ -19,6 +19,7 @@ import ( "testing" "github.com/google/go-containerregistry/pkg/name" + ociremote "github.com/sigstore/cosign/internal/oci/remote" ) func TestTargetRepositoryForImage(t *testing.T) { @@ -68,8 +69,8 @@ func TestTargetRepositoryForImage(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { - os.Setenv(repoEnv, test.envRepo) - defer os.Unsetenv(repoEnv) + os.Setenv(ociremote.RepoOverrideKey, test.envRepo) + defer os.Unsetenv(ociremote.RepoOverrideKey) got, err := TargetRepositoryForImage(test.image) if err != nil { diff --git a/copasetic/main.go b/copasetic/main.go index dab27b9d02a4..17efa6d5e670 100644 --- a/copasetic/main.go +++ b/copasetic/main.go @@ -39,6 +39,7 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" + ociremote "github.com/sigstore/cosign/internal/oci/remote" "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/options" @@ -136,16 +137,12 @@ func main() { if err != nil { return nil, err } - sigRepo, err := cli.TargetRepositoryForImage(ref) - if err != nil { - return nil, err - } registryOpts := []remote.Option{ remote.WithAuthFromKeychain(authn.DefaultKeychain), remote.WithContext(bctx.Context), } - sps, err := cosign.FetchSignaturesForImage(bctx.Context, ref, sigRepo, cosign.SignatureTagSuffix, registryOpts...) + sps, err := cosign.FetchSignaturesForImage(bctx.Context, ref, ociremote.WithRemoteOptions(registryOpts...)) if err != nil { return nil, err } diff --git a/internal/oci/remote/options.go b/internal/oci/remote/options.go new file mode 100644 index 000000000000..75783df8af88 --- /dev/null +++ b/internal/oci/remote/options.go @@ -0,0 +1,118 @@ +// +// 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 ( + "os" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +const ( + SignatureTagSuffix = ".sig" + SBOMTagSuffix = ".sbom" + AttestationTagSuffix = ".att" + + RepoOverrideKey = "COSIGN_REPOSITORY" +) + +// Option is a functional option for remote operations. +type Option func(*options) error + +type options struct { + signatureSuffix string + attestationSuffix string + sbomSuffix string + targetRepository name.Repository + ropt []remote.Option +} + +func makeOptions(target name.Repository, opts ...Option) (*options, error) { + o := &options{ + signatureSuffix: SignatureTagSuffix, + attestationSuffix: AttestationTagSuffix, + sbomSuffix: SBOMTagSuffix, + targetRepository: target, + ropt: []remote.Option{ + remote.WithAuthFromKeychain(authn.DefaultKeychain), + // TODO(mattmoor): Incorporate user agent. + }, + } + + // Before applying options, allow the environment to override things. + if ro := os.Getenv(RepoOverrideKey); ro != "" { + repo, err := name.NewRepository(ro) + if err != nil { + return nil, err + } + o.targetRepository = repo + } + + for _, option := range opts { + if err := option(o); err != nil { + return nil, err + } + } + + return o, nil +} + +// WithSignatureSuffix is a functional option for overriding the default +// signature tag suffix. +func WithSignatureSuffix(suffix string) Option { + return func(o *options) error { + o.signatureSuffix = suffix + return nil + } +} + +// WithAttestationSuffix is a functional option for overriding the default +// attestation tag suffix. +func WithAttestationSuffix(suffix string) Option { + return func(o *options) error { + o.attestationSuffix = suffix + return nil + } +} + +// WithSBOMSuffix is a functional option for overriding the default +// SBOM tag suffix. +func WithSBOMSuffix(suffix string) Option { + return func(o *options) error { + o.sbomSuffix = suffix + return nil + } +} + +// WithRemoteOptions is a functional option for overriding the default +// remote options passed to GGCR. +func WithRemoteOptions(opts ...remote.Option) Option { + return func(o *options) error { + o.ropt = opts + return nil + } +} + +// WithTargetRepository is a functional option for overriding the default +// target repository hosting the signature and attestation tags. +func WithTargetRepository(repo name.Repository) Option { + return func(o *options) error { + o.targetRepository = repo + return nil + } +} diff --git a/internal/oci/remote/remote.go b/internal/oci/remote/remote.go index 7e5c305d315f..b05f299867ad 100644 --- a/internal/oci/remote/remote.go +++ b/internal/oci/remote/remote.go @@ -25,6 +25,7 @@ import ( "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/google/go-containerregistry/pkg/v1/types" "github.com/pkg/errors" "github.com/sigstore/cosign/internal/oci" "github.com/sigstore/sigstore/pkg/cryptoutils" @@ -37,8 +38,148 @@ const ( BundleKey = "dev.sigstore.cosign/bundle" ) -// This enables mocking for unit testing without faking an entire registry. -var remoteImage = remote.Image +// These enable mocking for unit testing without faking an entire registry. +var ( + remoteImage = remote.Image + remoteIndex = remote.Index +) + +// SignedEntity provides access to a remote reference, and its signatures. +// The SignedEntity will be one of SignedImage or SignedImageIndex. +func SignedEntity(ref name.Reference, options ...Option) (oci.SignedEntity, error) { + o, err := makeOptions(ref.Context(), options...) + if err != nil { + return nil, err + } + + got, err := remote.Get(ref, o.ropt...) + if err != nil { + return nil, err + } + + switch got.MediaType { + case types.OCIImageIndex, types.DockerManifestList: + ii, err := got.ImageIndex() + if err != nil { + return nil, err + } + return &index{ + v1Index: ii, + ref: ref.Context().Digest(got.Digest.String()), + opt: o, + }, nil + case types.OCIManifestSchema1, types.DockerManifestSchema2: + i, err := got.Image() + if err != nil { + return nil, err + } + return &image{ + Image: i, + opt: o, + }, nil + default: + return nil, fmt.Errorf("unknown mime type: %v", got.MediaType) + } +} + +// SignedImageIndex provides access to a remote index reference, and its signatures. +func SignedImageIndex(ref name.Reference, options ...Option) (oci.SignedImageIndex, error) { + o, err := makeOptions(ref.Context(), options...) + if err != nil { + return nil, err + } + ri, err := remoteIndex(ref, o.ropt...) + if err != nil { + return nil, err + } + return &index{ + v1Index: ri, + ref: ref, + opt: o, + }, nil +} + +// We alias ImageIndex so that we can inline it without the type +// name colliding with the name of a method it had to implement. +type v1Index v1.ImageIndex + +type index struct { + v1Index + ref name.Reference + opt *options +} + +var _ oci.SignedImageIndex = (*index)(nil) + +// signatures is a shared implementation of the oci.Signed* Signatures method. +func signatures(digestable interface{ Digest() (v1.Hash, error) }, o *options) (oci.Signatures, error) { + h, err := digestable.Digest() + if err != nil { + return nil, err + } + + // sha256:d34db33f -> sha256-d34db33f.suffix + tagStr := strings.ReplaceAll(h.String(), ":", "-") + o.signatureSuffix + ref := o.targetRepository.Tag(tagStr) + return Signatures(ref, o.ropt...) +} + +// Signatures implements oic.SignedImageIndex +func (i *index) Signatures() (oci.Signatures, error) { + return signatures(i, i.opt) +} + +// Attestations implements oic.SignedImageIndex +func (i *index) Attestations() (oci.Attestations, error) { + // TODO(mattmoor): allow accessing attestations. + return nil, errors.New("NYI") +} + +// Attestations implements oic.SignedImageIndex +func (i *index) SignedImage(v1.Hash) (oci.SignedImage, error) { + // TODO(mattmoor): allow accessing child images as SignedImage + return nil, errors.New("NYI") +} + +// Attestations implements oic.SignedImageIndex +func (i *index) SignedImageIndex(v1.Hash) (oci.SignedImageIndex, error) { + // TODO(mattmoor): allow accessing child indices as SignedImageIndex + return nil, errors.New("NYI") +} + +// SignedImage provides access to a remote image reference, and its signatures. +func SignedImage(ref name.Reference, options ...Option) (oci.SignedImage, error) { + o, err := makeOptions(ref.Context(), options...) + if err != nil { + return nil, err + } + ri, err := remote.Image(ref, o.ropt...) + if err != nil { + return nil, err + } + return &image{ + Image: ri, + opt: o, + }, nil +} + +type image struct { + v1.Image + opt *options +} + +var _ oci.SignedImage = (*image)(nil) + +// Signatures implements oic.SignedImage +func (i *image) Signatures() (oci.Signatures, error) { + return signatures(i, i.opt) +} + +// Attestations implements oic.SignedImage +func (i *image) Attestations() (oci.Attestations, error) { + // TODO(mattmoor): allow accessing attestations. + return nil, errors.New("NYI") +} // Signatures fetches the signatures image represented by the named reference. func Signatures(ref name.Reference, opts ...remote.Option) (oci.Signatures, error) { diff --git a/pkg/cosign/fetch.go b/pkg/cosign/fetch.go index c8a1282afd46..29bd08ba2658 100644 --- a/pkg/cosign/fetch.go +++ b/pkg/cosign/fetch.go @@ -23,7 +23,6 @@ import ( "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" "knative.dev/pkg/pool" @@ -67,19 +66,13 @@ func AttachedImageTag(repo name.Repository, digest v1.Hash, tagSuffix string) na return repo.Tag(tagStr) } -func FetchSignaturesForImage(ctx context.Context, signedImgRef name.Reference, sigRepo name.Repository, sigTagSuffix string, registryOpts ...remote.Option) ([]SignedPayload, error) { - // TODO(mattmoor): If signedImageRef is a digest, this is an unnecessary fetch. - signedImgDesc, err := remote.Get(signedImgRef, registryOpts...) +func FetchSignaturesForImage(ctx context.Context, ref name.Reference, opts ...ociremote.Option) ([]SignedPayload, error) { + simg, err := ociremote.SignedEntity(ref, opts...) if err != nil { return nil, err } - return FetchSignaturesForImageDigest(ctx, signedImgDesc.Descriptor.Digest, sigRepo, sigTagSuffix, registryOpts...) -} - -func FetchSignaturesForImageDigest(ctx context.Context, signedImageDigest v1.Hash, sigRepo name.Repository, sigTagSuffix string, registryOpts ...remote.Option) ([]SignedPayload, error) { - tag := AttachedImageTag(sigRepo, signedImageDigest, sigTagSuffix) - sigs, err := ociremote.Signatures(tag, registryOpts...) + sigs, err := simg.Signatures() if err != nil { return nil, errors.Wrap(err, "remote image") } diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 45141d10d1d0..6678520c18ee 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -34,6 +34,7 @@ import ( "github.com/pkg/errors" "github.com/sigstore/cosign/internal/oci" + ociremote "github.com/sigstore/cosign/internal/oci/remote" rekor "github.com/sigstore/rekor/pkg/client" "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/sigstore/pkg/cryptoutils" @@ -88,17 +89,21 @@ func Verify(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) ([] } h := signedImgDesc.Descriptor.Digest + opts := []ociremote.Option{ + ociremote.WithRemoteOptions(co.RegistryClientOpts...), + } + // These are all the signatures attached to our image that we know how to parse. - sigRepo := co.SignatureRepo - if (sigRepo == name.Repository{}) { - sigRepo = signedImgRef.Context() + if (co.SignatureRepo != name.Repository{}) { + opts = append(opts, ociremote.WithTargetRepository(co.SignatureRepo)) } - tagSuffix := SignatureTagSuffix if co.SigTagSuffixOverride != "" { - tagSuffix = co.SigTagSuffixOverride + opts = append(opts, ociremote.WithSignatureSuffix(co.SigTagSuffixOverride)) } - allSignatures, err := FetchSignaturesForImageDigest(ctx, h, sigRepo, tagSuffix, co.RegistryClientOpts...) + // TODO(mattmoor): If we change this code to interact with the SignedImage directly, + // then we could shed the `remote.Get` above. + allSignatures, err := FetchSignaturesForImage(ctx, signedImgRef, opts...) if err != nil { return nil, errors.Wrap(err, "fetching signatures") } diff --git a/test/e2e_test.go b/test/e2e_test.go index 60bce98aa9fe..21b679e72c80 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -45,6 +45,7 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli/download" "github.com/sigstore/cosign/cmd/cosign/cli/upload" sget "github.com/sigstore/cosign/cmd/sget/cli" + ociremote "github.com/sigstore/cosign/internal/oci/remote" "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/cosign/pkg/cosign/kubernetes" cremote "github.com/sigstore/cosign/pkg/cosign/remote" @@ -243,13 +244,8 @@ func TestDuplicateSign(t *testing.T) { // Signing again should work just fine... must(cli.SignCmd(ctx, ko, cli.RegistryOpts{}, nil, []string{imgName}, "", true, "", false, false, ""), t) - // but a duplicate signature should not be a uploaded - sigRepo, err := cli.TargetRepositoryForImage(ref) - if err != nil { - t.Fatalf("failed to get signature repository: %v", err) - } - signatures, err := cosign.FetchSignaturesForImage(ctx, ref, sigRepo, cosign.SignatureTagSuffix, registryClientOpts(ctx)...) + signatures, err := cosign.FetchSignaturesForImage(ctx, ref, ociremote.WithRemoteOptions(registryClientOpts(ctx)...)) if err != nil { t.Fatalf("failed to fetch signatures: %v", err) } @@ -500,12 +496,8 @@ func TestUploadDownload(t *testing.T) { } // Now download it! - sigRepo, err := cli.TargetRepositoryForImage(ref) - if err != nil { - t.Fatalf("failed to get signature repository: %v", err) - } regClientOpts := registryClientOpts(ctx) - signatures, err := cosign.FetchSignaturesForImage(ctx, ref, sigRepo, cosign.SignatureTagSuffix, regClientOpts...) + signatures, err := cosign.FetchSignaturesForImage(ctx, ref, ociremote.WithRemoteOptions(regClientOpts...)) if testCase.expectedErr { mustErr(err, t) } else {