diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 6f1452a458..3b15fcbc15 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -30,6 +30,7 @@ type copyOptions struct { removeSignatures bool // Do not copy signatures from the source image signByFingerprint string // Sign the image using a GPG key with the specified fingerprint signPassphraseFile string // Path pointing to a passphrase file when signing + signIdentity string // Identity of the signed image, must be a fully specified docker reference digestFile string // Write digest to this file format commonFlag.OptionalString // Force conversion of the image to a specified format quiet bool // Suppress output information when copying images @@ -81,6 +82,7 @@ See skopeo(1) section "IMAGE NAMES" for the expected format flags.BoolVar(&opts.removeSignatures, "remove-signatures", false, "Do not copy signatures from SOURCE-IMAGE") flags.StringVar(&opts.signByFingerprint, "sign-by", "", "Sign the image using a GPG key with the specified `FINGERPRINT`") flags.StringVar(&opts.signPassphraseFile, "sign-passphrase-file", "", "File that contains a passphrase for the --sign-by key") + flags.StringVar(&opts.signIdentity, "sign-identity", "", "Identity of signed image, must be a fully specified docker reference. Defaults to the target docker reference.") flags.StringVar(&opts.digestFile, "digestfile", "", "Write the digest of the pushed image to the specified file") flags.VarP(commonFlag.NewOptionalStringValue(&opts.format), "format", "f", `MANIFEST TYPE (oci, v2s1, or v2s2) to use in the destination (default is manifest type of source, with fallbacks)`) flags.StringSliceVar(&opts.encryptionKeys, "encryption-key", []string{}, "*Experimental* key with the encryption protocol to use needed to encrypt the image (e.g. jwe:/path/to/key.pem)") @@ -231,11 +233,20 @@ func (opts *copyOptions) run(args []string, stdout io.Writer) (retErr error) { return err } + var signIdentity reference.Named = nil + if opts.signIdentity != "" { + signIdentity, err = reference.ParseNamed(opts.signIdentity) + if err != nil { + return fmt.Errorf("Could not parse --sign-identity: %v", err) + } + } + return retry.RetryIfNecessary(ctx, func() error { manifestBytes, err := copy.Image(ctx, policyContext, destRef, srcRef, ©.Options{ RemoveSignatures: opts.removeSignatures, SignBy: opts.signByFingerprint, SignPassphrase: passphrase, + SignIdentity: signIdentity, ReportWriter: stdout, SourceCtx: sourceCtx, DestinationCtx: destinationCtx, diff --git a/completions/bash/skopeo b/completions/bash/skopeo index 91d2ad9eea..2b0ab6340c 100644 --- a/completions/bash/skopeo +++ b/completions/bash/skopeo @@ -43,6 +43,7 @@ _skopeo_copy() { --multi-arch --sign-by --sign-passphrase-file + --sign-identity --src-creds --screds --src-cert-dir --src-tls-verify diff --git a/docs/skopeo-copy.1.md b/docs/skopeo-copy.1.md index f70273b838..97e00fc910 100644 --- a/docs/skopeo-copy.1.md +++ b/docs/skopeo-copy.1.md @@ -70,7 +70,7 @@ MANIFEST TYPE (oci, v2s1, or v2s2) to use in the destination (default is manifes Print usage statement -**--multi-arch** +**--multi-arch** _option_ Control what is copied if _source-image_ refers to a multi-architecture image. Default is system. @@ -89,14 +89,18 @@ Suppress output information when copying images. Do not copy signatures, if any, from _source-image_. Necessary when copying a signed image to a destination which does not support signatures. -**--sign-by**=_key-id_ +**--sign-by** _key-id_ Add a signature using that key ID for an image name corresponding to _destination-image_ -**--sign-passphrase-file**=_path_ +**--sign-passphrase-file** _path_ The passphare to use when signing with the key ID from `--sign-by`. Only the first line will be read. A passphrase stored in a file is of questionable security if other users can read this file. Do not use this option if at all avoidable. +**--sign-identity** _reference_ + +The identity to use when signing the image. The identity must be a fully specified docker reference. If the identity is not specified, the target docker reference will be used. + **--src-shared-blob-dir** _directory_ Directory to use to share blobs across OCI repositories. diff --git a/integration/copy_test.go b/integration/copy_test.go index cd3bda8fe2..c8e28c81d5 100644 --- a/integration/copy_test.go +++ b/integration/copy_test.go @@ -969,20 +969,6 @@ func (s *CopySuite) TestCopyAtomicExtension(c *check.C) { assertDirImagesAreEqual(c, filepath.Join(topDir, "dirDA"), filepath.Join(topDir, "dirDD")) } -// copyWithSignedIdentity creates a copy of an unsigned image, adding a signature for an unrelated identity -// This should be easier than using standalone-sign. -func copyWithSignedIdentity(c *check.C, src, dest, signedIdentity, signBy, registriesDir string) { - topDir := c.MkDir() - - signingDir := filepath.Join(topDir, "signing-temp") - assertSkopeoSucceeds(c, "", "copy", "--src-tls-verify=false", src, "dir:"+signingDir) - c.Logf("%s", combinedOutputOfCommand(c, "ls", "-laR", signingDir)) - assertSkopeoSucceeds(c, "^$", "standalone-sign", "-o", filepath.Join(signingDir, "signature-1"), - filepath.Join(signingDir, "manifest.json"), signedIdentity, signBy) - c.Logf("%s", combinedOutputOfCommand(c, "ls", "-laR", signingDir)) - assertSkopeoSucceeds(c, "", "--registries.d", registriesDir, "copy", "--dest-tls-verify=false", "dir:"+signingDir, dest) -} - // Both mirroring support in registries.conf, and mirrored remapIdentity support in policy.json func (s *CopySuite) TestCopyVerifyingMirroredSignatures(c *check.C) { const regPrefix = "docker://localhost:5006/myns/mirroring-" @@ -1026,10 +1012,12 @@ func (s *CopySuite) TestCopyVerifyingMirroredSignatures(c *check.C) { assertSkopeoFails(c, ".*Source image rejected: None of the signatures were accepted, reasons: Signature for identity localhost:5006/myns/mirroring-primary:direct is not accepted; Signature for identity localhost:5006/myns/mirroring-mirror:mirror-signed is not accepted.*", "--policy", policy, "--registries.d", registriesDir, "--registries-conf", "fixtures/registries.conf", "copy", "--src-tls-verify=false", regPrefix+"primary:mirror-signed", dirDest) + // Fail if we specify an unqualified identity + assertSkopeoFails(c, ".*Could not parse --sign-identity: repository name must be canonical.*", + "--registries.d", registriesDir, "copy", "--src-tls-verify=false", "--dest-tls-verify=false", "--sign-by=personal@example.com", "--sign-identity=this-is-not-fully-specified", regPrefix+"primary:unsigned", regPrefix+"mirror:primary-signed") + // Create a signature for mirroring-primary:primary-signed without pushing there. - copyWithSignedIdentity(c, regPrefix+"primary:unsigned", regPrefix+"mirror:primary-signed", - "localhost:5006/myns/mirroring-primary:primary-signed", "personal@example.com", - registriesDir) + assertSkopeoSucceeds(c, "", "--registries.d", registriesDir, "copy", "--src-tls-verify=false", "--dest-tls-verify=false", "--sign-by=personal@example.com", "--sign-identity=localhost:5006/myns/mirroring-primary:primary-signed", regPrefix+"primary:unsigned", regPrefix+"mirror:primary-signed") // Verify that a correctly signed image for the primary is accessible using the primary's reference assertSkopeoSucceeds(c, "", "--policy", policy, "--registries.d", registriesDir, "--registries-conf", "fixtures/registries.conf", "copy", "--src-tls-verify=false", regPrefix+"primary:primary-signed", dirDest) // … but verify that while it is accessible using the mirror location @@ -1044,9 +1032,7 @@ func (s *CopySuite) TestCopyVerifyingMirroredSignatures(c *check.C) { // … it is NOT accessible when requiring a signature … assertSkopeoFails(c, ".*Source image rejected: None of the signatures were accepted, reasons: Signature for identity localhost:5006/myns/mirroring-primary:direct is not accepted; Signature for identity localhost:5006/myns/mirroring-mirror:mirror-signed is not accepted; Signature for identity localhost:5006/myns/mirroring-primary:primary-signed is not accepted.*", "--policy", policy, "--registries.d", registriesDir, "--registries-conf", "fixtures/registries.conf", "copy", "--src-tls-verify=false", regPrefix+"remap:remapped", dirDest) // … until signed. - copyWithSignedIdentity(c, regPrefix+"remap:remapped", regPrefix+"remap:remapped", - "localhost:5006/myns/mirroring-primary:remapped", "personal@example.com", - registriesDir) + assertSkopeoSucceeds(c, "", "--registries.d", registriesDir, "copy", "--src-tls-verify=false", "--dest-tls-verify=false", "--sign-by=personal@example.com", "--sign-identity=localhost:5006/myns/mirroring-primary:remapped", regPrefix+"remap:remapped", regPrefix+"remap:remapped") assertSkopeoSucceeds(c, "", "--policy", policy, "--registries.d", registriesDir, "--registries-conf", "fixtures/registries.conf", "copy", "--src-tls-verify=false", regPrefix+"remap:remapped", dirDest) // To be extra clear about the semantics, verify that the signedPrefix (primary) location never exists // and only the remapped prefix (mirror) is accessed.