From d80436021c21c26b492f0014511f13f41d8b42d9 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 5 Mar 2025 15:35:31 +0100 Subject: [PATCH] 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 +}