From b13c4bbb07a31cb7cd6bf4233869f96782100009 Mon Sep 17 00:00:00 2001 From: Matt Moore Date: Tue, 29 Mar 2022 16:11:46 -0700 Subject: [PATCH] Make `cosign copy` copy metadata attached to child images. (#1682) Previously, `cosign copy` would only copy metadata associated directly with the reference it was given, however, this is problematic for multi-architecture images like those produced by `ko` and `apko` because the SBOMs they produce are associated with the per-architecture images, but only SBOMs associated with the index will be copied. This change leverages the `walk` library to copy things at each level of the `oci.SignedEntity` we are given. Here is an example where I copy a `ko` built image where I signed the index: ``` $ go run ./cmd/cosign copy -f gcr.io/mattmoor-chainguard/cosign@sha256:71e2f842aec01d151a2630db3c2a6891536ffe273d17e7a8bff288845a7b0624 ghcr.io/mattmoor/cosign Copying gcr.io/mattmoor-chainguard/cosign:sha256-71e2f842aec01d151a2630db3c2a6891536ffe273d17e7a8bff288845a7b0624.sig to ghcr.io/mattmoor/cosign:sha256-71e2f842aec01d151a2630db3c2a6891536ffe273d17e7a8bff288845a7b0624.sig... Copying gcr.io/mattmoor-chainguard/cosign@sha256:71e2f842aec01d151a2630db3c2a6891536ffe273d17e7a8bff288845a7b0624 to ghcr.io/mattmoor/cosign:sha256:71e2f842aec01d151a2630db3c2a6891536ffe273d17e7a8bff288845a7b0624... Copying gcr.io/mattmoor-chainguard/cosign:sha256-70e7d4974d9ed3017706c38247b270f7a0b9fe77ae1d034c4c0bc5e214872700.sbom to ghcr.io/mattmoor/cosign:sha256-70e7d4974d9ed3017706c38247b270f7a0b9fe77ae1d034c4c0bc5e214872700.sbom... Copying gcr.io/mattmoor-chainguard/cosign@sha256:70e7d4974d9ed3017706c38247b270f7a0b9fe77ae1d034c4c0bc5e214872700 to ghcr.io/mattmoor/cosign:sha256:70e7d4974d9ed3017706c38247b270f7a0b9fe77ae1d034c4c0bc5e214872700... Copying gcr.io/mattmoor-chainguard/cosign:sha256-3b2e73aaa122fa1aded2164a506687510c82e788d7a5b510c998877ba78003e0.sbom to ghcr.io/mattmoor/cosign:sha256-3b2e73aaa122fa1aded2164a506687510c82e788d7a5b510c998877ba78003e0.sbom... Copying gcr.io/mattmoor-chainguard/cosign@sha256:3b2e73aaa122fa1aded2164a506687510c82e788d7a5b510c998877ba78003e0 to ghcr.io/mattmoor/cosign:sha256:3b2e73aaa122fa1aded2164a506687510c82e788d7a5b510c998877ba78003e0... ``` Notable is that both the signature and the per-architecture SBOMs are copied to the target repository. I refactored the existing logic a bit to be slightly less verbose in support of this. Signed-off-by: Matt Moore --- cmd/cosign/cli/copy/copy.go | 83 +++++++++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 21 deletions(-) diff --git a/cmd/cosign/cli/copy/copy.go b/cmd/cosign/cli/copy/copy.go index 186386dfeeb..cf2c733b618 100644 --- a/cmd/cosign/cli/copy/copy.go +++ b/cmd/cosign/cli/copy/copy.go @@ -16,65 +16,87 @@ package copy import ( "context" + "errors" "fmt" + "net/http" "os" "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/remote/transport" "github.com/sigstore/cosign/cmd/cosign/cli/options" + "github.com/sigstore/cosign/pkg/oci" ociremote "github.com/sigstore/cosign/pkg/oci/remote" + "github.com/sigstore/cosign/pkg/oci/walk" ) // CopyCmd implements the logic to copy the supplied container image and signatures. // nolint func CopyCmd(ctx context.Context, regOpts options.RegistryOptions, srcImg, dstImg string, sigOnly, force bool) error { - var refs []name.Reference // nolint: staticcheck srcRef, err := name.ParseReference(srcImg) if err != nil { return err } - - refs = append(refs, srcRef) + srcRepoRef := srcRef.Context() dstRef, err := name.ParseReference(dstImg) if err != nil { return err } + dstRepoRef := dstRef.Context() remoteOpts := regOpts.GetRegistryClientOpts(ctx) - sigSrcRef, err := ociremote.SignatureTag(srcRef, ociremote.WithRemoteOptions(remoteOpts...)) + root, err := ociremote.SignedEntity(srcRef, ociremote.WithRemoteOptions(remoteOpts...)) if err != nil { return err } - dstRepoRef := dstRef.Context() - sigDstRef := dstRepoRef.Tag(sigSrcRef.Identifier()) - if err := copyImage(sigSrcRef, sigDstRef, force, remoteOpts...); err != nil { - return err - } - - if !sigOnly { - attSrcRef, err := ociremote.AttestationTag(srcRef, ociremote.WithRemoteOptions(remoteOpts...)) + if err := walk.SignedEntity(ctx, root, func(ctx context.Context, se oci.SignedEntity) error { + // Both of the SignedEntity types implement Digest() + h, err := se.(interface{ Digest() (v1.Hash, error) }).Digest() if err != nil { return err } - refs = append(refs, attSrcRef) + srcDigest := srcRepoRef.Digest(h.String()) - sbomSrcRef, err := ociremote.SBOMTag(srcRef, ociremote.WithRemoteOptions(remoteOpts...)) - if err != nil { + // Copy signatures. + if err := copyTagImage(ociremote.SignatureTag, srcDigest, dstRepoRef, force, remoteOpts...); err != nil { return err } - refs = append(refs, sbomSrcRef) + if sigOnly { + return nil + } - for _, ref := range refs { - if err := copyImage(ref, dstRepoRef.Tag(ref.Identifier()), force, remoteOpts...); err != nil { - fmt.Fprintf(os.Stderr, "WARNING: %s tag could not be found for an image %s, it might not exist, continuing anyway\n", ref.Identifier(), srcImg) - } + // Copy attestations + if err := copyTagImage(ociremote.AttestationTag, srcDigest, dstRepoRef, force, remoteOpts...); err != nil { + return err } + + // Copy SBOMs + if err := copyTagImage(ociremote.SBOMTag, srcDigest, dstRepoRef, force, remoteOpts...); err != nil { + return err + } + + // Copy the entity itself. + if err := copyImage(srcDigest, dstRepoRef.Tag(srcDigest.Identifier()), force, remoteOpts...); err != nil { + return err + } + + return nil + }); err != nil { + return err + } + if sigOnly { + return nil } - return nil + // Now that everything has been copied over, update the tag. + h, err := root.(interface{ Digest() (v1.Hash, error) }).Digest() + if err != nil { + return err + } + return copyImage(srcRepoRef.Digest(h.String()), dstRef, force, remoteOpts...) } func descriptorsEqual(a, b *v1.Descriptor) bool { @@ -84,9 +106,27 @@ func descriptorsEqual(a, b *v1.Descriptor) bool { return a.Digest == b.Digest } +type tagMap func(name.Reference, ...ociremote.Option) (name.Tag, error) + +func copyTagImage(tm tagMap, srcDigest name.Digest, dstRepo name.Repository, overwrite bool, opts ...remote.Option) error { + src, err := tm(srcDigest, ociremote.WithRemoteOptions(opts...)) + if err != nil { + return err + } + return copyImage(src, dstRepo.Tag(src.Identifier()), overwrite, opts...) +} + func copyImage(src, dest name.Reference, overwrite bool, opts ...remote.Option) error { got, err := remote.Get(src, opts...) if err != nil { + var te *transport.Error + if errors.As(err, &te) && te.StatusCode == http.StatusNotFound { + // We do not treat 404s on the source image as errors because we are + // trying many flavors of tag (sig, sbom, att) and only a subset of + // these are likely to exist, especially when we're talking about a + // multi-arch image. + return nil + } return err } @@ -99,6 +139,7 @@ func copyImage(src, dest name.Reference, overwrite bool, opts ...remote.Option) } } + fmt.Fprintf(os.Stderr, "Copying %s to %s...\n", src, dest) if got.MediaType.IsIndex() { imgIdx, err := got.ImageIndex() if err != nil {