From c6f456bc90574f4180f3b990e8a4e216485e35b7 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 5 Mar 2025 15:11:37 +0100 Subject: [PATCH 1/3] cli/command/image: deprecate and internalize TrustedPush This function was only used by "docker trust sign"; inline the code and deprecate the function. This function has no known external consumers, so we should remove it on the first possible ocassion (which could be a minor release). Signed-off-by: Sebastiaan van Stijn --- cli/command/image/trust.go | 4 +++- cli/command/trust/sign.go | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/cli/command/image/trust.go b/cli/command/image/trust.go index 89d6e54eae..9fa19a0130 100644 --- a/cli/command/image/trust.go +++ b/cli/command/image/trust.go @@ -44,7 +44,9 @@ func newNotaryClient(cli command.Streams, imgRefAndAuth trust.ImageRefAndAuth) ( return trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), "pull") } -// TrustedPush handles content trust pushing of an image +// TrustedPush handles content trust pushing of an image. +// +// Deprecated: this function was only used internally and will be removed in the next release. func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, options image.PushOptions) error { responseBody, err := cli.Client().ImagePush(ctx, reference.FamiliarString(ref), options) if err != nil { diff --git a/cli/command/trust/sign.go b/cli/command/trust/sign.go index 1875e257b5..0b913e323b 100644 --- a/cli/command/trust/sign.go +++ b/cli/command/trust/sign.go @@ -8,6 +8,7 @@ import ( "sort" "strings" + "github.com/distribution/reference" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/image" @@ -98,10 +99,15 @@ func runSignImage(ctx context.Context, dockerCLI command.Cli, options signOption if err != nil { return err } - return image.TrustedPush(ctx, dockerCLI, imgRefAndAuth.RepoInfo(), imgRefAndAuth.Reference(), *imgRefAndAuth.AuthConfig(), imagetypes.PushOptions{ + responseBody, err := dockerCLI.Client().ImagePush(ctx, reference.FamiliarString(imgRefAndAuth.Reference()), imagetypes.PushOptions{ RegistryAuth: encodedAuth, PrivilegeFunc: requestPrivilege, }) + if err != nil { + return err + } + defer responseBody.Close() + return image.PushTrustedReference(ctx, dockerCLI, imgRefAndAuth.RepoInfo(), imgRefAndAuth.Reference(), authConfig, responseBody) default: return err } From d80436021c21c26b492f0014511f13f41d8b42d9 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 5 Mar 2025 15:35:31 +0100 Subject: [PATCH 2/3] cli/command/image: deprecate PushTrustedReference, move to trust This function was shared between "trust" "image" and "plugin" packages, all of which needed the trust package, so move it there instead. Signed-off-by: Sebastiaan van Stijn --- cli/command/image/push.go | 4 +- cli/command/image/trust.go | 118 ++---------------------------- cli/command/plugin/push.go | 4 +- cli/command/trust/sign.go | 2 +- cli/trust/trust_push.go | 143 +++++++++++++++++++++++++++++++++++++ 5 files changed, 155 insertions(+), 116 deletions(-) create mode 100644 cli/trust/trust_push.go diff --git a/cli/command/image/push.go b/cli/command/image/push.go index 19e5c8a8f6..65b562a45f 100644 --- a/cli/command/image/push.go +++ b/cli/command/image/push.go @@ -139,8 +139,8 @@ To push the complete multi-platform image, remove the --platform flag. defer responseBody.Close() if !opts.untrusted { - // TODO PushTrustedReference currently doesn't respect `--quiet` - return PushTrustedReference(ctx, dockerCli, repoInfo, ref, authConfig, responseBody) + // TODO pushTrustedReference currently doesn't respect `--quiet` + return pushTrustedReference(ctx, dockerCli, repoInfo, ref, authConfig, responseBody) } if opts.quiet { diff --git a/cli/command/image/trust.go b/cli/command/image/trust.go index 9fa19a0130..aef21d47d7 100644 --- a/cli/command/image/trust.go +++ b/cli/command/image/trust.go @@ -3,17 +3,14 @@ package image import ( "context" "encoding/hex" - "encoding/json" "fmt" "io" - "sort" "github.com/distribution/reference" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/internal/jsonstream" "github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/trust" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/image" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/registry" @@ -55,120 +52,19 @@ func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.Reposi defer responseBody.Close() - return PushTrustedReference(ctx, cli, repoInfo, ref, authConfig, responseBody) + return trust.PushTrustedReference(ctx, cli, repoInfo, ref, authConfig, responseBody, command.UserAgent()) } // PushTrustedReference pushes a canonical reference to the trust server. // -//nolint:gocyclo +// Deprecated: use [trust.PushTrustedReference] instead. this function was only used internally and will be removed in the next release. func PushTrustedReference(ctx context.Context, ioStreams command.Streams, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader) error { - // If it is a trusted push we would like to find the target entry which match the - // tag provided in the function and then do an AddTarget later. - notaryTarget := &client.Target{} - // Count the times of calling for handleTarget, - // if it is called more that once, that should be considered an error in a trusted push. - cnt := 0 - handleTarget := func(msg jsonstream.JSONMessage) { - cnt++ - if cnt > 1 { - // handleTarget should only be called once. This will be treated as an error. - return - } + return pushTrustedReference(ctx, ioStreams, repoInfo, ref, authConfig, in) +} - var pushResult types.PushResult - err := json.Unmarshal(*msg.Aux, &pushResult) - if err == nil && pushResult.Tag != "" { - if dgst, err := digest.Parse(pushResult.Digest); err == nil { - h, err := hex.DecodeString(dgst.Hex()) - if err != nil { - notaryTarget = nil - return - } - notaryTarget.Name = pushResult.Tag - notaryTarget.Hashes = data.Hashes{string(dgst.Algorithm()): h} - notaryTarget.Length = int64(pushResult.Size) - } - } - } - - var tag string - switch x := ref.(type) { - case reference.Canonical: - return errors.New("cannot push a digest reference") - case reference.NamedTagged: - tag = x.Tag() - default: - // We want trust signatures to always take an explicit tag, - // otherwise it will act as an untrusted push. - if err := jsonstream.Display(ctx, in, ioStreams.Out()); err != nil { - return err - } - _, _ = fmt.Fprintln(ioStreams.Err(), "No tag specified, skipping trust metadata push") - return nil - } - - if err := jsonstream.Display(ctx, in, ioStreams.Out(), jsonstream.WithAuxCallback(handleTarget)); err != nil { - return err - } - - if cnt > 1 { - return errors.Errorf("internal error: only one call to handleTarget expected") - } - - if notaryTarget == nil { - return errors.Errorf("no targets found, provide a specific tag in order to sign it") - } - - _, _ = fmt.Fprintln(ioStreams.Out(), "Signing and pushing trust metadata") - - repo, err := trust.GetNotaryRepository(ioStreams.In(), ioStreams.Out(), command.UserAgent(), repoInfo, &authConfig, "push", "pull") - if err != nil { - return errors.Wrap(err, "error establishing connection to trust repository") - } - - // get the latest repository metadata so we can figure out which roles to sign - _, err = repo.ListTargets() - - switch err.(type) { - case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist: - keys := repo.GetCryptoService().ListKeys(data.CanonicalRootRole) - var rootKeyID string - // always select the first root key - if len(keys) > 0 { - sort.Strings(keys) - rootKeyID = keys[0] - } else { - rootPublicKey, err := repo.GetCryptoService().Create(data.CanonicalRootRole, "", data.ECDSAKey) - if err != nil { - return err - } - rootKeyID = rootPublicKey.ID() - } - - // Initialize the notary repository with a remotely managed snapshot key - if err := repo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil { - return trust.NotaryError(repoInfo.Name.Name(), err) - } - _, _ = fmt.Fprintf(ioStreams.Out(), "Finished initializing %q\n", repoInfo.Name.Name()) - err = repo.AddTarget(notaryTarget, data.CanonicalTargetsRole) - case nil: - // already initialized and we have successfully downloaded the latest metadata - err = trust.AddToAllSignableRoles(repo, notaryTarget) - default: - return trust.NotaryError(repoInfo.Name.Name(), err) - } - - if err == nil { - err = repo.Publish() - } - - if err != nil { - err = errors.Wrapf(err, "failed to sign %s:%s", repoInfo.Name.Name(), tag) - return trust.NotaryError(repoInfo.Name.Name(), err) - } - - _, _ = fmt.Fprintf(ioStreams.Out(), "Successfully signed %s:%s\n", repoInfo.Name.Name(), tag) - return nil +// pushTrustedReference pushes a canonical reference to the trust server. +func pushTrustedReference(ctx context.Context, ioStreams command.Streams, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader) error { + return trust.PushTrustedReference(ctx, ioStreams, repoInfo, ref, authConfig, in, command.UserAgent()) } // trustedPull handles content trust pulling of an image diff --git a/cli/command/plugin/push.go b/cli/command/plugin/push.go index c8758a9ef1..ca05f9dd8c 100644 --- a/cli/command/plugin/push.go +++ b/cli/command/plugin/push.go @@ -6,8 +6,8 @@ import ( "github.com/distribution/reference" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/internal/jsonstream" + "github.com/docker/cli/cli/trust" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/registry" "github.com/pkg/errors" @@ -66,7 +66,7 @@ func runPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error defer responseBody.Close() if !opts.untrusted { - return image.PushTrustedReference(ctx, dockerCli, repoInfo, named, authConfig, responseBody) + return trust.PushTrustedReference(ctx, dockerCli, repoInfo, named, authConfig, responseBody, command.UserAgent()) } return jsonstream.Display(ctx, responseBody, dockerCli.Out()) diff --git a/cli/command/trust/sign.go b/cli/command/trust/sign.go index 0b913e323b..df97f04d32 100644 --- a/cli/command/trust/sign.go +++ b/cli/command/trust/sign.go @@ -107,7 +107,7 @@ func runSignImage(ctx context.Context, dockerCLI command.Cli, options signOption return err } defer responseBody.Close() - return image.PushTrustedReference(ctx, dockerCLI, imgRefAndAuth.RepoInfo(), imgRefAndAuth.Reference(), authConfig, responseBody) + return trust.PushTrustedReference(ctx, dockerCLI, imgRefAndAuth.RepoInfo(), imgRefAndAuth.Reference(), authConfig, responseBody, command.UserAgent()) default: return err } diff --git a/cli/trust/trust_push.go b/cli/trust/trust_push.go new file mode 100644 index 0000000000..63fbdf932d --- /dev/null +++ b/cli/trust/trust_push.go @@ -0,0 +1,143 @@ +package trust + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "sort" + + "github.com/distribution/reference" + "github.com/docker/cli/cli/internal/jsonstream" + "github.com/docker/cli/cli/streams" + "github.com/docker/docker/api/types" + registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/registry" + "github.com/opencontainers/go-digest" + "github.com/pkg/errors" + "github.com/theupdateframework/notary/client" + "github.com/theupdateframework/notary/tuf/data" +) + +// Streams is an interface which exposes the standard input and output streams. +// +// Same interface as [github.com/docker/cli/cli/command.Streams] but defined here to prevent a circular import. +type Streams interface { + In() *streams.In + Out() *streams.Out + Err() *streams.Out +} + +// PushTrustedReference pushes a canonical reference to the trust server. +// +//nolint:gocyclo +func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader, userAgent string) error { + // If it is a trusted push we would like to find the target entry which match the + // tag provided in the function and then do an AddTarget later. + notaryTarget := &client.Target{} + // Count the times of calling for handleTarget, + // if it is called more that once, that should be considered an error in a trusted push. + cnt := 0 + handleTarget := func(msg jsonstream.JSONMessage) { + cnt++ + if cnt > 1 { + // handleTarget should only be called once. This will be treated as an error. + return + } + + var pushResult types.PushResult + err := json.Unmarshal(*msg.Aux, &pushResult) + if err == nil && pushResult.Tag != "" { + if dgst, err := digest.Parse(pushResult.Digest); err == nil { + h, err := hex.DecodeString(dgst.Hex()) + if err != nil { + notaryTarget = nil + return + } + notaryTarget.Name = pushResult.Tag + notaryTarget.Hashes = data.Hashes{string(dgst.Algorithm()): h} + notaryTarget.Length = int64(pushResult.Size) + } + } + } + + var tag string + switch x := ref.(type) { + case reference.Canonical: + return errors.New("cannot push a digest reference") + case reference.NamedTagged: + tag = x.Tag() + default: + // We want trust signatures to always take an explicit tag, + // otherwise it will act as an untrusted push. + if err := jsonstream.Display(ctx, in, ioStreams.Out()); err != nil { + return err + } + _, _ = fmt.Fprintln(ioStreams.Err(), "No tag specified, skipping trust metadata push") + return nil + } + + if err := jsonstream.Display(ctx, in, ioStreams.Out(), jsonstream.WithAuxCallback(handleTarget)); err != nil { + return err + } + + if cnt > 1 { + return errors.Errorf("internal error: only one call to handleTarget expected") + } + + if notaryTarget == nil { + return errors.Errorf("no targets found, provide a specific tag in order to sign it") + } + + _, _ = fmt.Fprintln(ioStreams.Out(), "Signing and pushing trust metadata") + + repo, err := GetNotaryRepository(ioStreams.In(), ioStreams.Out(), userAgent, repoInfo, &authConfig, "push", "pull") + if err != nil { + return errors.Wrap(err, "error establishing connection to trust repository") + } + + // get the latest repository metadata so we can figure out which roles to sign + _, err = repo.ListTargets() + + switch err.(type) { + case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist: + keys := repo.GetCryptoService().ListKeys(data.CanonicalRootRole) + var rootKeyID string + // always select the first root key + if len(keys) > 0 { + sort.Strings(keys) + rootKeyID = keys[0] + } else { + rootPublicKey, err := repo.GetCryptoService().Create(data.CanonicalRootRole, "", data.ECDSAKey) + if err != nil { + return err + } + rootKeyID = rootPublicKey.ID() + } + + // Initialize the notary repository with a remotely managed snapshot key + if err := repo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil { + return NotaryError(repoInfo.Name.Name(), err) + } + _, _ = fmt.Fprintf(ioStreams.Out(), "Finished initializing %q\n", repoInfo.Name.Name()) + err = repo.AddTarget(notaryTarget, data.CanonicalTargetsRole) + case nil: + // already initialized and we have successfully downloaded the latest metadata + err = AddToAllSignableRoles(repo, notaryTarget) + default: + return NotaryError(repoInfo.Name.Name(), err) + } + + if err == nil { + err = repo.Publish() + } + + if err != nil { + err = errors.Wrapf(err, "failed to sign %s:%s", repoInfo.Name.Name(), tag) + return NotaryError(repoInfo.Name.Name(), err) + } + + _, _ = fmt.Fprintf(ioStreams.Out(), "Successfully signed %s:%s\n", repoInfo.Name.Name(), tag) + return nil +} From e37d814ce96b01393a400c081666ea1cca2eb8bd Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 5 Mar 2025 16:16:01 +0100 Subject: [PATCH 3/3] cli/command/image: deprecate TagTrusted, move to cli/trust This function was shared between "image" and "container" packages, all of which needed the trust package, so move it there instead. Signed-off-by: Sebastiaan van Stijn --- cli/command/container/create.go | 3 ++- cli/command/image/build.go | 3 ++- cli/command/image/trust.go | 20 +++++++++++--------- cli/trust/trust_tag.go | 22 ++++++++++++++++++++++ 4 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 cli/trust/trust_tag.go diff --git a/cli/command/container/create.go b/cli/command/container/create.go index 42dab18983..ac8e6083fe 100644 --- a/cli/command/container/create.go +++ b/cli/command/container/create.go @@ -15,6 +15,7 @@ import ( "github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/internal/jsonstream" "github.com/docker/cli/cli/streams" + "github.com/docker/cli/cli/trust" "github.com/docker/cli/opts" "github.com/docker/docker/api/types/container" imagetypes "github.com/docker/docker/api/types/image" @@ -242,7 +243,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c return err } if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil { - return image.TagTrusted(ctx, dockerCli, trustedRef, taggedRef) + return trust.TagTrusted(ctx, dockerCli.Client(), dockerCli.Err(), trustedRef, taggedRef) } return nil } diff --git a/cli/command/image/build.go b/cli/command/image/build.go index f8355fbe96..039e7619ba 100644 --- a/cli/command/image/build.go +++ b/cli/command/image/build.go @@ -22,6 +22,7 @@ import ( "github.com/docker/cli/cli/command/image/build" "github.com/docker/cli/cli/internal/jsonstream" "github.com/docker/cli/cli/streams" + "github.com/docker/cli/cli/trust" "github.com/docker/cli/opts" "github.com/docker/docker/api" "github.com/docker/docker/api/types" @@ -406,7 +407,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) // Since the build was successful, now we must tag any of the resolved // images from the above Dockerfile rewrite. for _, resolved := range resolvedTags { - if err := TagTrusted(ctx, dockerCli, resolved.digestRef, resolved.tagRef); err != nil { + if err := trust.TagTrusted(ctx, dockerCli.Client(), dockerCli.Err(), resolved.digestRef, resolved.tagRef); err != nil { return err } } diff --git a/cli/command/image/trust.go b/cli/command/image/trust.go index aef21d47d7..b4a6d84a00 100644 --- a/cli/command/image/trust.go +++ b/cli/command/image/trust.go @@ -104,7 +104,11 @@ func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.Image return err } - if err := TagTrusted(ctx, cli, trustedRef, tagged); err != nil { + // Use familiar references when interacting with client and output + familiarRef := reference.FamiliarString(tagged) + trustedFamiliarRef := reference.FamiliarString(trustedRef) + _, _ = fmt.Fprintf(cli.Err(), "Tagging %s as %s\n", trustedFamiliarRef, familiarRef) + if err := cli.Client().ImageTag(ctx, trustedFamiliarRef, familiarRef); err != nil { return err } } @@ -225,15 +229,13 @@ func convertTarget(t client.Target) (target, error) { }, nil } -// TagTrusted tags a trusted ref +// TagTrusted tags a trusted ref. It is a shallow wrapper around APIClient.ImageTag +// that updates the given image references to their familiar format for tagging +// and printing. +// +// Deprecated: this function was only used internally, and will be removed in the next release. func TagTrusted(ctx context.Context, cli command.Cli, trustedRef reference.Canonical, ref reference.NamedTagged) error { - // Use familiar references when interacting with client and output - familiarRef := reference.FamiliarString(ref) - trustedFamiliarRef := reference.FamiliarString(trustedRef) - - _, _ = fmt.Fprintf(cli.Err(), "Tagging %s as %s\n", trustedFamiliarRef, familiarRef) - - return cli.Client().ImageTag(ctx, trustedFamiliarRef, familiarRef) + return trust.TagTrusted(ctx, cli.Client(), cli.Err(), trustedRef, ref) } // AuthResolver returns an auth resolver function from a command.Cli diff --git a/cli/trust/trust_tag.go b/cli/trust/trust_tag.go new file mode 100644 index 0000000000..053f9317d1 --- /dev/null +++ b/cli/trust/trust_tag.go @@ -0,0 +1,22 @@ +package trust + +import ( + "context" + "fmt" + "io" + + "github.com/distribution/reference" + "github.com/docker/docker/client" +) + +// TagTrusted tags a trusted ref. It is a shallow wrapper around [client.Client.ImageTag] +// that updates the given image references to their familiar format for tagging +// and printing. +func TagTrusted(ctx context.Context, apiClient client.ImageAPIClient, out io.Writer, trustedRef reference.Canonical, ref reference.NamedTagged) error { + // Use familiar references when interacting with client and output + familiarRef := reference.FamiliarString(ref) + trustedFamiliarRef := reference.FamiliarString(trustedRef) + + _, _ = fmt.Fprintf(out, "Tagging %s as %s\n", trustedFamiliarRef, familiarRef) + return apiClient.ImageTag(ctx, trustedFamiliarRef, familiarRef) +}