Skip to content

Commit

Permalink
Make cosign copy copy metadata attached to child images. (#1682)
Browse files Browse the repository at this point in the history
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 <mattmoor@chainguard.dev>
  • Loading branch information
mattmoor authored Mar 29, 2022
1 parent ba50ee0 commit b13c4bb
Showing 1 changed file with 62 additions and 21 deletions.
83 changes: 62 additions & 21 deletions cmd/cosign/cli/copy/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}

Expand All @@ -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 {
Expand Down

0 comments on commit b13c4bb

Please sign in to comment.