1
0
mirror of https://github.com/docker/cli.git synced 2026-01-13 18:22:35 +03:00

Merge pull request #6603 from thaJeztah/remove_trust_integration

remove support for client-side docker content trust validation
This commit is contained in:
Sebastiaan van Stijn
2025-11-04 14:05:39 +01:00
committed by GitHub
29 changed files with 138 additions and 556 deletions

View File

@@ -17,11 +17,9 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/jsonstream"
"github.com/docker/cli/opts"
"github.com/moby/moby/api/types/mount"
@@ -41,7 +39,6 @@ const (
type createOptions struct {
name string
platform string
untrusted bool
pull string // always, missing, never
quiet bool
useAPISocket bool
@@ -88,7 +85,9 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
_ = flags.SetAnnotation("platform", "version", []string{"1.32"})
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms())
flags.BoolVar(&options.untrusted, "disable-content-trust", !trust.Enabled(), "Skip image verification")
// TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30
flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)")
_ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed")
copts = addFlags(flags)
addCompletions(cmd, dockerCLI)
@@ -213,10 +212,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
hostConfig := containerCfg.HostConfig
networkingConfig := containerCfg.NetworkingConfig
var (
trustedRef reference.Canonical
namedRef reference.Named
)
var namedRef reference.Named
// TODO(thaJeztah): add a platform option-type / flag-type.
if options.platform != "" {
@@ -240,15 +236,6 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
}
if named, ok := ref.(reference.Named); ok {
namedRef = reference.TagNameOnly(named)
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && !options.untrusted {
var err error
trustedRef, err = image.TrustedReference(ctx, dockerCli, taggedRef)
if err != nil {
return "", err
}
config.Image = reference.FamiliarString(trustedRef)
}
}
const dockerConfigPathInContainer = "/run/secrets/docker/config.json"
@@ -331,9 +318,6 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
if err := pullImage(ctx, dockerCli, config.Image, options); err != nil {
return err
}
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
return trust.TagTrusted(ctx, dockerCli.Client(), dockerCli.Err(), trustedRef, taggedRef)
}
return nil
}

View File

@@ -13,7 +13,6 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/notary"
"github.com/google/go-cmp/cmp"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/system"
@@ -136,10 +135,9 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
}
fakeCLI := test.NewFakeCli(apiClient)
id, err := createContainer(context.Background(), fakeCLI, config, &createOptions{
name: "name",
platform: runtime.GOOS,
untrusted: true,
pull: tc.PullPolicy,
name: "name",
platform: runtime.GOOS,
pull: tc.PullPolicy,
})
if tc.ExpectedErrMsg != "" {
@@ -215,51 +213,6 @@ func TestCreateContainerValidateFlags(t *testing.T) {
}
}
func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
testCases := []struct {
name string
args []string
expectedError string
notaryFunc test.NotaryClientFuncType
}{
{
name: "offline-notary-server",
notaryFunc: notary.GetOfflineNotaryRepository,
expectedError: "client is offline",
args: []string{"image:tag"},
},
{
name: "uninitialized-notary-server",
notaryFunc: notary.GetUninitializedNotaryRepository,
expectedError: "remote trust data does not exist",
args: []string{"image:tag"},
},
{
name: "empty-notary-server",
notaryFunc: notary.GetEmptyTargetsNotaryRepository,
expectedError: "No valid trust data for tag",
args: []string{"image:tag"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Setenv("DOCKER_CONTENT_TRUST", "true")
fakeCLI := test.NewFakeCli(&fakeClient{
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
return client.ContainerCreateResult{}, errors.New("shouldn't try to pull image")
},
})
fakeCLI.SetNotaryClient(tc.notaryFunc)
cmd := newCreateCommand(fakeCLI)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.ErrorContains(t, err, tc.expectedError)
})
}
}
func TestNewCreateCommandWithWarnings(t *testing.T) {
testCases := []struct {
name string

View File

@@ -12,7 +12,6 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/opts"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
@@ -73,7 +72,10 @@ func newRunCommand(dockerCLI command.Cli) *cobra.Command {
// TODO(thaJeztah): consider adding platform as "image create option" on containerOptions
flags.StringVar(&options.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
_ = flags.SetAnnotation("platform", "version", []string{"1.32"})
flags.BoolVar(&options.untrusted, "disable-content-trust", !trust.Enabled(), "Skip image verification")
// TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30
flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)")
_ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed")
copts = addFlags(flags)
_ = cmd.RegisterFlagCompletionFunc("detach-keys", completeDetachKeys)

View File

@@ -13,7 +13,6 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/notary"
"github.com/moby/moby/api/types"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
@@ -295,54 +294,6 @@ func TestRunPullTermination(t *testing.T) {
}
}
func TestRunCommandWithContentTrustErrors(t *testing.T) {
testCases := []struct {
name string
args []string
expectedError string
notaryFunc test.NotaryClientFuncType
}{
{
name: "offline-notary-server",
notaryFunc: notary.GetOfflineNotaryRepository,
expectedError: "client is offline",
args: []string{"image:tag"},
},
{
name: "uninitialized-notary-server",
notaryFunc: notary.GetUninitializedNotaryRepository,
expectedError: "remote trust data does not exist",
args: []string{"image:tag"},
},
{
name: "empty-notary-server",
notaryFunc: notary.GetEmptyTargetsNotaryRepository,
expectedError: "No valid trust data for tag",
args: []string{"image:tag"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Setenv("DOCKER_CONTENT_TRUST", "true")
fakeCLI := test.NewFakeCli(&fakeClient{
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
return client.ContainerCreateResult{}, errors.New("shouldn't try to pull image")
},
})
fakeCLI.SetNotaryClient(tc.notaryFunc)
cmd := newRunCommand(fakeCLI)
cmd.SetArgs(tc.args)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
err := cmd.Execute()
statusErr := cli.StatusError{}
assert.Check(t, errors.As(err, &statusErr))
assert.Check(t, is.Equal(statusErr.StatusCode, 125))
assert.Check(t, is.ErrorContains(err, tc.expectedError))
})
}
}
func TestRunContainerImagePullPolicyInvalid(t *testing.T) {
cases := []struct {
PullPolicy string

View File

@@ -149,8 +149,9 @@ func newBuildCommand(dockerCLI command.Cli) *cobra.Command {
flags.SetAnnotation("target", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#target"})
flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to the file")
// TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30
flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)")
_ = flags.MarkHidden("disable-content-trust")
_ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed")
flags.StringVar(&options.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
flags.SetAnnotation("platform", "version", []string{"1.38"})

View File

@@ -13,10 +13,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/jsonstream"
"github.com/moby/moby/api/pkg/authconfig"
registrytypes "github.com/moby/moby/api/types/registry"
"github.com/moby/moby/client"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"
@@ -24,11 +21,10 @@ import (
// pullOptions defines what and how to pull.
type pullOptions struct {
remote string
all bool
platform string
quiet bool
untrusted bool
remote string
all bool
platform string
quiet bool
}
// newPullCommand creates a new `docker pull` command
@@ -57,7 +53,11 @@ func newPullCommand(dockerCLI command.Cli) *cobra.Command {
flags.BoolVarP(&opts.all, "all-tags", "a", false, "Download all tagged images in the repository")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress verbose output")
flags.BoolVar(&opts.untrusted, "disable-content-trust", !trust.Enabled(), "Skip image verification")
// TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30
flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)")
_ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed")
flags.StringVar(&opts.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
_ = flags.SetAnnotation("platform", "version", []string{"1.32"})
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms())
@@ -80,46 +80,22 @@ func runPull(ctx context.Context, dockerCLI command.Cli, opts pullOptions) error
}
}
if opts.platform != "" {
// TODO(thaJeztah): add a platform option-type / flag-type.
if _, err = platforms.Parse(opts.platform); err != nil {
return err
}
}
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver(dockerCLI), distributionRef.String())
if err != nil {
return err
}
// Check if reference has a digest
_, isCanonical := distributionRef.(reference.Canonical)
if !opts.untrusted && !isCanonical {
if err := trustedPull(ctx, dockerCLI, imgRefAndAuth, opts); err != nil {
return err
}
} else {
if err := imagePullPrivileged(ctx, dockerCLI, imgRefAndAuth.Reference(), imgRefAndAuth.AuthConfig(), opts); err != nil {
return err
}
}
_, _ = fmt.Fprintln(dockerCLI.Out(), imgRefAndAuth.Reference().String())
return nil
}
// imagePullPrivileged pulls the image and displays it to the output
func imagePullPrivileged(ctx context.Context, dockerCLI command.Cli, ref reference.Named, authConfig *registrytypes.AuthConfig, opts pullOptions) error {
encodedAuth, err := authconfig.Encode(*authConfig)
if err != nil {
return err
}
var ociPlatforms []ocispec.Platform
if opts.platform != "" {
// Already validated.
ociPlatforms = append(ociPlatforms, platforms.MustParse(opts.platform))
// TODO(thaJeztah): add a platform option-type / flag-type.
p, err := platforms.Parse(opts.platform)
if err != nil {
return err
}
ociPlatforms = append(ociPlatforms, p)
}
responseBody, err := dockerCLI.Client().ImagePull(ctx, reference.FamiliarString(ref), client.ImagePullOptions{
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), distributionRef.String())
if err != nil {
return err
}
responseBody, err := dockerCLI.Client().ImagePull(ctx, reference.FamiliarString(distributionRef), client.ImagePullOptions{
RegistryAuth: encodedAuth,
PrivilegeFunc: nil,
All: opts.all,
@@ -134,5 +110,9 @@ func imagePullPrivileged(ctx context.Context, dockerCLI command.Cli, ref referen
if opts.quiet {
out = streams.NewOut(io.Discard)
}
return jsonstream.Display(ctx, responseBody, out)
if err := jsonstream.Display(ctx, responseBody, out); err != nil {
return err
}
_, _ = fmt.Fprintln(dockerCLI.Out(), distributionRef.String())
return nil
}

View File

@@ -7,7 +7,6 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/notary"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
@@ -89,49 +88,3 @@ func TestNewPullCommandSuccess(t *testing.T) {
})
}
}
func TestNewPullCommandWithContentTrustErrors(t *testing.T) {
testCases := []struct {
name string
args []string
expectedError string
notaryFunc test.NotaryClientFuncType
}{
{
name: "offline-notary-server",
notaryFunc: notary.GetOfflineNotaryRepository,
expectedError: "client is offline",
args: []string{"image:tag"},
},
{
name: "uninitialized-notary-server",
notaryFunc: notary.GetUninitializedNotaryRepository,
expectedError: "remote trust data does not exist",
args: []string{"image:tag"},
},
{
name: "empty-notary-server",
notaryFunc: notary.GetEmptyTargetsNotaryRepository,
expectedError: "No valid trust data for tag",
args: []string{"image:tag"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Setenv("DOCKER_CONTENT_TRUST", "true")
cli := test.NewFakeCli(&fakeClient{
imagePullFunc: func(ref string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
// FIXME(thaJeztah): how to mock this?
return fakeStreamResult{ReadCloser: http.NoBody}, nil
},
})
cli.SetNotaryClient(tc.notaryFunc)
cmd := newPullCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.ErrorContains(t, err, tc.expectedError)
})
}
}

View File

@@ -16,7 +16,6 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/jsonstream"
"github.com/docker/cli/internal/tui"
"github.com/moby/moby/api/types/auxprogress"
@@ -27,11 +26,10 @@ import (
)
type pushOptions struct {
all bool
remote string
untrusted bool
quiet bool
platform string
all bool
remote string
quiet bool
platform string
}
// newPushCommand creates a new `docker push` command
@@ -57,7 +55,10 @@ func newPushCommand(dockerCLI command.Cli) *cobra.Command {
flags := cmd.Flags()
flags.BoolVarP(&opts.all, "all-tags", "a", false, "Push all tags of an image to the repository")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress verbose output")
flags.BoolVar(&opts.untrusted, "disable-content-trust", !trust.Enabled(), "Skip image signing")
// TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30
flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)")
_ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed")
// Don't default to DOCKER_DEFAULT_PLATFORM env variable, always default to
// pushing the image as-is. This also avoids forcing the platform selection
@@ -129,10 +130,6 @@ To push the complete multi-platform image, remove the --platform flag.
}
}()
if !opts.untrusted {
return pushTrustedReference(ctx, dockerCli, ref, responseBody)
}
if opts.quiet {
err = jsonstream.Display(ctx, responseBody, streams.NewOut(io.Discard), jsonstream.WithAuxCallback(handleAux()))
if err == nil {

View File

@@ -1,204 +0,0 @@
package image
import (
"context"
"encoding/hex"
"errors"
"fmt"
"io"
"github.com/distribution/reference"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/registry"
registrytypes "github.com/moby/moby/api/types/registry"
"github.com/moby/moby/client"
"github.com/opencontainers/go-digest"
"github.com/sirupsen/logrus"
notaryclient "github.com/theupdateframework/notary/client"
"github.com/theupdateframework/notary/tuf/data"
)
type target struct {
name string
digest digest.Digest
size int64
}
// notaryClientProvider is used in tests to provide a dummy notary client.
type notaryClientProvider interface {
NotaryClient() (notaryclient.Repository, error)
}
// newNotaryClient provides a Notary Repository to interact with signed metadata for an image.
func newNotaryClient(cli command.Streams, repoInfo *trust.RepositoryInfo, authConfig *registrytypes.AuthConfig) (notaryclient.Repository, error) {
if ncp, ok := cli.(notaryClientProvider); ok {
// notaryClientProvider is used in tests to provide a dummy notary client.
return ncp.NotaryClient()
}
return trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, authConfig, "pull")
}
// pushTrustedReference pushes a canonical reference to the trust server.
func pushTrustedReference(ctx context.Context, dockerCLI command.Cli, ref reference.Named, responseBody io.Reader) error {
// Resolve the Repository name from fqn to RepositoryInfo, and create an
// IndexInfo. Docker Content Trust uses the IndexInfo.Official field to
// select the right domain for Docker Hub's Notary server;
// https://github.com/docker/cli/blob/v28.4.0/cli/trust/trust.go#L65-L79
indexInfo := registry.NewIndexInfo(ref)
repoInfo := &trust.RepositoryInfo{
Name: reference.TrimNamed(ref),
Index: indexInfo,
}
authConfig := command.ResolveAuthConfig(dockerCLI.ConfigFile(), indexInfo)
return trust.PushTrustedReference(ctx, dockerCLI, repoInfo, ref, authConfig, responseBody, command.UserAgent())
}
// trustedPull handles content trust pulling of an image
func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, opts pullOptions) error {
refs, err := getTrustedPullTargets(cli, imgRefAndAuth)
if err != nil {
return err
}
ref := imgRefAndAuth.Reference()
for i, r := range refs {
displayTag := r.name
if displayTag != "" {
displayTag = ":" + displayTag
}
_, _ = fmt.Fprintf(cli.Out(), "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), reference.FamiliarName(ref), displayTag, r.digest)
trustedRef, err := reference.WithDigest(reference.TrimNamed(ref), r.digest)
if err != nil {
return err
}
updatedImgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver(cli), trustedRef.String())
if err != nil {
return err
}
if err := imagePullPrivileged(ctx, cli, updatedImgRefAndAuth.Reference(), updatedImgRefAndAuth.AuthConfig(), pullOptions{
all: false,
platform: opts.platform,
quiet: opts.quiet,
remote: opts.remote,
}); err != nil {
return err
}
tagged, err := reference.WithTag(reference.TrimNamed(ref), r.name)
if err != nil {
return err
}
// 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)
_, err = cli.Client().ImageTag(ctx, client.ImageTagOptions{
Source: trustedFamiliarRef,
Target: familiarRef,
})
if err != nil {
return err
}
}
return nil
}
func getTrustedPullTargets(cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth) ([]target, error) {
notaryRepo, err := newNotaryClient(cli, imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig())
if err != nil {
return nil, fmt.Errorf("error establishing connection to trust repository: %w", err)
}
ref := imgRefAndAuth.Reference()
tagged, isTagged := ref.(reference.NamedTagged)
if !isTagged {
// List all targets
targets, err := notaryRepo.ListTargets(trust.ReleasesRole, data.CanonicalTargetsRole)
if err != nil {
return nil, trust.NotaryError(ref.Name(), err)
}
var refs []target
for _, tgt := range targets {
t, err := convertTarget(tgt.Target)
if err != nil {
_, _ = fmt.Fprintf(cli.Err(), "Skipping target for %q\n", reference.FamiliarName(ref))
continue
}
// Only list tags in the top level targets role or the releases delegation role - ignore
// all other delegation roles
if tgt.Role != trust.ReleasesRole && tgt.Role != data.CanonicalTargetsRole {
continue
}
refs = append(refs, t)
}
if len(refs) == 0 {
return nil, trust.NotaryError(ref.Name(), fmt.Errorf("no trusted tags for %s", ref.Name()))
}
return refs, nil
}
t, err := notaryRepo.GetTargetByName(tagged.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole)
if err != nil {
return nil, trust.NotaryError(ref.Name(), err)
}
// Only get the tag if it's in the top level targets role or the releases delegation role
// ignore it if it's in any other delegation roles
if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole {
return nil, trust.NotaryError(ref.Name(), fmt.Errorf("no trust data for %s", tagged.Tag()))
}
logrus.Debugf("retrieving target for %s role", t.Role)
r, err := convertTarget(t.Target)
return []target{r}, err
}
// TrustedReference returns the canonical trusted reference for an image reference
func TrustedReference(ctx context.Context, cli command.Cli, ref reference.NamedTagged) (reference.Canonical, error) {
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver(cli), ref.String())
if err != nil {
return nil, err
}
notaryRepo, err := newNotaryClient(cli, imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig())
if err != nil {
return nil, fmt.Errorf("error establishing connection to trust repository: %w", err)
}
t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole)
if err != nil {
return nil, trust.NotaryError(imgRefAndAuth.RepoInfo().Name.Name(), err)
}
// Only list tags in the top level targets role or the releases delegation role - ignore
// all other delegation roles
if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole {
return nil, trust.NotaryError(imgRefAndAuth.RepoInfo().Name.Name(), notaryclient.ErrNoSuchTarget(ref.Tag()))
}
r, err := convertTarget(t.Target)
if err != nil {
return nil, err
}
return reference.WithDigest(reference.TrimNamed(ref), r.digest)
}
func convertTarget(t notaryclient.Target) (target, error) {
h, ok := t.Hashes["sha256"]
if !ok {
return target{}, errors.New("no valid hash, expecting sha256")
}
return target{
name: t.Name,
digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)),
size: t.Length,
}, nil
}
// authResolver returns an auth resolver function from a [config.Provider].
func authResolver(dockerCLI config.Provider) func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig {
return func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig {
return command.ResolveAuthConfig(dockerCLI.ConfigFile(), index)
}
}

View File

@@ -44,6 +44,27 @@ func newManifestStore(dockerCLI command.Cli) store.Store {
return store.NewStore(filepath.Join(config.Dir(), "manifests"))
}
// authConfigKey is the key used to store credentials for Docker Hub. It is
// a copy of [registry.IndexServer].
//
// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#IndexServer
const authConfigKey = "https://index.docker.io/v1/"
// getAuthConfigKey special-cases using the full index address of the official
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
//
// It is similar to [registry.GetAuthConfigKey], but does not require on
// [registrytypes.IndexInfo] as intermediate.
//
// [registry.GetAuthConfigKey]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#GetAuthConfigKey
// [registrytypes.IndexInfo]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/api/types/registry#IndexInfo
func getAuthConfigKey(domainName string) string {
if domainName == "docker.io" || domainName == "index.docker.io" {
return authConfigKey
}
return domainName
}
// newRegistryClient returns a client for communicating with a Docker distribution
// registry
func newRegistryClient(dockerCLI command.Cli, allowInsecure bool) registryclient.RegistryClient {
@@ -51,8 +72,20 @@ func newRegistryClient(dockerCLI command.Cli, allowInsecure bool) registryclient
// manifestStoreProvider is used in tests to provide a dummy store.
return msp.RegistryClient(allowInsecure)
}
resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig {
return command.ResolveAuthConfig(dockerCLI.ConfigFile(), index)
cfg := dockerCLI.ConfigFile()
resolver := func(ctx context.Context, domainName string) registry.AuthConfig {
configKey := getAuthConfigKey(domainName)
a, _ := cfg.GetAuthConfig(configKey)
return registry.AuthConfig{
Username: a.Username,
Password: a.Password,
ServerAddress: a.ServerAddress,
// TODO(thaJeztah): Are these expected to be included?
Auth: a.Auth,
IdentityToken: a.IdentityToken,
RegistryToken: a.RegistryToken,
}
}
// FIXME(thaJeztah): this should use the userAgent as configured on the dockerCLI.
return registryclient.NewRegistryClient(resolver, command.UserAgent(), allowInsecure)

View File

@@ -44,8 +44,10 @@ func newInstallCommand(dockerCLI command.Cli) *cobra.Command {
flags.BoolVar(&options.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin")
flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install")
flags.StringVar(&options.localName, "alias", "", "Local name for plugin")
// TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30
flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)")
_ = flags.MarkHidden("disable-content-trust")
_ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed")
return cmd
}

View File

@@ -26,8 +26,9 @@ func newPushCommand(dockerCLI command.Cli) *cobra.Command {
}
flags := cmd.Flags()
// TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30
flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)")
_ = flags.MarkHidden("disable-content-trust")
_ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed")
return cmd
}

View File

@@ -34,8 +34,9 @@ func newUpgradeCommand(dockerCLI command.Cli) *cobra.Command {
flags := cmd.Flags()
flags.BoolVar(&options.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin")
// TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30
flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)")
_ = flags.MarkHidden("disable-content-trust")
_ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed")
flags.BoolVar(&options.skipRemoteCheck, "skip-remote-check", false, "Do not check if specified remote plugin matches existing plugin image")
return cmd
}

View File

@@ -39,10 +39,7 @@ const authConfigKey = "https://index.docker.io/v1/"
// credential-store. It returns an empty AuthConfig if no credentials were
// found.
//
// It is similar to [registry.ResolveAuthConfig], but uses the credentials-
// store, instead of looking up credentials from a map.
//
// [registry.ResolveAuthConfig]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#ResolveAuthConfig
// Deprecated: this function is no longer used, and will be removed in the next release.
func ResolveAuthConfig(cfg *configfile.ConfigFile, index *registrytypes.IndexInfo) registrytypes.AuthConfig {
configKey := index.Name
if index.Official {

View File

@@ -115,10 +115,6 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
return err
}
if err := resolveServiceImageDigestContentTrust(dockerCLI, &service); err != nil {
return err
}
// only send auth if flag was set
var encodedAuth string
if opts.registryAuth {

View File

@@ -1,86 +0,0 @@
package service
import (
"encoding/hex"
"errors"
"fmt"
"github.com/distribution/reference"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/registry"
"github.com/moby/moby/api/types/swarm"
"github.com/opencontainers/go-digest"
"github.com/sirupsen/logrus"
"github.com/theupdateframework/notary/tuf/data"
)
func resolveServiceImageDigestContentTrust(dockerCli command.Cli, service *swarm.ServiceSpec) error {
if !trust.Enabled() {
// When not using content trust, digest resolution happens later when
// contacting the registry to retrieve image information.
return nil
}
ref, err := reference.ParseAnyReference(service.TaskTemplate.ContainerSpec.Image)
if err != nil {
return fmt.Errorf("invalid reference %s: %w", service.TaskTemplate.ContainerSpec.Image, err)
}
// If reference does not have digest (is not canonical nor image id)
if _, ok := ref.(reference.Digested); !ok {
namedRef, ok := ref.(reference.Named)
if !ok {
return errors.New("failed to resolve image digest using content trust: reference is not named")
}
namedRef = reference.TagNameOnly(namedRef)
taggedRef, ok := namedRef.(reference.NamedTagged)
if !ok {
return errors.New("failed to resolve image digest using content trust: reference is not tagged")
}
resolvedImage, err := trustedResolveDigest(dockerCli, taggedRef)
if err != nil {
return fmt.Errorf("failed to resolve image digest using content trust: %w", err)
}
resolvedFamiliar := reference.FamiliarString(resolvedImage)
logrus.Debugf("resolved image tag to %s using content trust", resolvedFamiliar)
service.TaskTemplate.ContainerSpec.Image = resolvedFamiliar
}
return nil
}
func trustedResolveDigest(cli command.Cli, ref reference.NamedTagged) (reference.Canonical, error) {
indexInfo := registry.NewIndexInfo(ref)
authConfig := command.ResolveAuthConfig(cli.ConfigFile(), indexInfo)
repoInfo := &trust.RepositoryInfo{
Name: reference.TrimNamed(ref),
Index: indexInfo,
}
notaryRepo, err := trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, &authConfig, "pull")
if err != nil {
return nil, fmt.Errorf("error establishing connection to trust repository: %w", err)
}
t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole)
if err != nil {
return nil, trust.NotaryError(repoInfo.Name.Name(), err)
}
// Only get the tag if it's in the top level targets role or the releases delegation role
// ignore it if it's in any other delegation roles
if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole {
return nil, trust.NotaryError(repoInfo.Name.Name(), fmt.Errorf("no trust data for %s", reference.FamiliarString(ref)))
}
logrus.Debugf("retrieving target for %s role", t.Role)
h, ok := t.Hashes["sha256"]
if !ok {
return nil, errors.New("no valid hash, expecting sha256")
}
dgst := digest.NewDigestFromHex("sha256", hex.EncodeToString(h))
// Allow returning canonical reference with tag
return reference.WithDigest(ref, dgst)
}

View File

@@ -193,9 +193,6 @@ func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
}
if flags.Changed("image") {
if err := resolveServiceImageDigestContentTrust(dockerCLI, spec); err != nil {
return err
}
updateOpts.QueryRegistry = !options.noResolveImage
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/trust"
"github.com/fvbommel/sortorder"
registrytypes "github.com/moby/moby/api/types/registry"
@@ -172,6 +173,39 @@ func matchReleasedSignatures(allTargets []client.TargetSignedStruct) []trustTagR
// authResolver returns an auth resolver function from a [config.Provider].
func authResolver(dockerCLI config.Provider) func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig {
return func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig {
return command.ResolveAuthConfig(dockerCLI.ConfigFile(), index)
return resolveAuthConfig(dockerCLI.ConfigFile(), index)
}
}
// authConfigKey is the key used to store credentials for Docker Hub. It is
// a copy of [registry.IndexServer].
//
// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#IndexServer
const authConfigKey = "https://index.docker.io/v1/"
// resolveAuthConfig returns auth-config for the given registry from the
// credential-store. It returns an empty AuthConfig if no credentials were
// found.
//
// It is similar to [registry.ResolveAuthConfig], but uses the credentials-
// store, instead of looking up credentials from a map.
//
// [registry.ResolveAuthConfig]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#ResolveAuthConfig
func resolveAuthConfig(cfg *configfile.ConfigFile, index *registrytypes.IndexInfo) registrytypes.AuthConfig {
configKey := index.Name
if index.Official {
configKey = authConfigKey
}
a, _ := cfg.GetAuthConfig(configKey)
return registrytypes.AuthConfig{
Username: a.Username,
Password: a.Password,
ServerAddress: a.ServerAddress,
// TODO(thaJeztah): Are these expected to be included?
Auth: a.Auth,
IdentityToken: a.IdentityToken,
RegistryToken: a.RegistryToken,
}
}

View File

@@ -92,7 +92,7 @@ func runSignImage(ctx context.Context, dockerCLI command.Cli, options signOption
}
_, _ = fmt.Fprintf(dockerCLI.Err(), "Signing and pushing trust data for local image %s, may overwrite remote trust data\n", imageName)
authConfig := command.ResolveAuthConfig(dockerCLI.ConfigFile(), imgRefAndAuth.RepoInfo().Index)
authConfig := resolveAuthConfig(dockerCLI.ConfigFile(), imgRefAndAuth.RepoInfo().Index)
encodedAuth, err := authconfig.Encode(authConfig)
if err != nil {
return err

View File

@@ -37,7 +37,6 @@ Create a new container
| `--device-read-iops` | `list` | | Limit read rate (IO per second) from a device |
| `--device-write-bps` | `list` | | Limit write rate (bytes per second) to a device |
| `--device-write-iops` | `list` | | Limit write rate (IO per second) to a device |
| `--disable-content-trust` | `bool` | `true` | Skip image verification |
| `--dns` | `list` | | Set custom DNS servers |
| `--dns-option` | `list` | | Set DNS options |
| `--dns-search` | `list` | | Set custom DNS search domains |

View File

@@ -39,7 +39,6 @@ Create and run a new container from an image
| `--device-read-iops` | `list` | | Limit read rate (IO per second) from a device |
| `--device-write-bps` | `list` | | Limit write rate (bytes per second) to a device |
| `--device-write-iops` | `list` | | Limit write rate (IO per second) to a device |
| `--disable-content-trust` | `bool` | `true` | Skip image verification |
| `--dns` | `list` | | Set custom DNS servers |
| `--dns-option` | `list` | | Set DNS options |
| `--dns-search` | `list` | | Set custom DNS search domains |

View File

@@ -37,7 +37,6 @@ Create a new container
| `--device-read-iops` | `list` | | Limit read rate (IO per second) from a device |
| `--device-write-bps` | `list` | | Limit write rate (bytes per second) to a device |
| `--device-write-iops` | `list` | | Limit write rate (IO per second) to a device |
| `--disable-content-trust` | `bool` | `true` | Skip image verification |
| `--dns` | `list` | | Set custom DNS servers |
| `--dns-option` | `list` | | Set DNS options |
| `--dns-search` | `list` | | Set custom DNS search domains |

View File

@@ -123,8 +123,6 @@ line:
| `DOCKER_API_VERSION` | Override the negotiated API version to use for debugging (e.g. `1.19`) |
| `DOCKER_CERT_PATH` | Location of your authentication keys. This variable is used both by the `docker` CLI and the [`dockerd` daemon](https://docs.docker.com/reference/cli/dockerd/) |
| `DOCKER_CONFIG` | The location of your client configuration files. |
| `DOCKER_CONTENT_TRUST_SERVER` | The URL of the Notary server to use. Defaults to the same URL as the registry. |
| `DOCKER_CONTENT_TRUST` | When set Docker uses notary to sign and verify images. Equates to `--disable-content-trust=false` for build, create, pull, push, run. |
| `DOCKER_CONTEXT` | Name of the `docker context` to use (overrides `DOCKER_HOST` env var and default context set with `docker context use`) |
| `DOCKER_CUSTOM_HEADERS` | (Experimental) Configure [custom HTTP headers](#custom-http-headers) to be sent by the client. Headers must be provided as a comma-separated list of `name=value` pairs. This is the equivalent to the `HttpHeaders` field in the configuration file. |
| `DOCKER_DEFAULT_PLATFORM` | Default platform for commands that take the `--platform` flag. |

View File

@@ -12,7 +12,6 @@ Download an image from a registry
| Name | Type | Default | Description |
|:---------------------------------------------|:---------|:--------|:-------------------------------------------------|
| [`-a`](#all-tags), [`--all-tags`](#all-tags) | `bool` | | Download all tagged images in the repository |
| `--disable-content-trust` | `bool` | `true` | Skip image verification |
| `--platform` | `string` | | Set platform if server is multi-platform capable |
| `-q`, `--quiet` | `bool` | | Suppress verbose output |

View File

@@ -12,7 +12,6 @@ Upload an image to a registry
| Name | Type | Default | Description |
|:---------------------------------------------|:---------|:--------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`-a`](#all-tags), [`--all-tags`](#all-tags) | `bool` | | Push all tags of an image to the repository |
| `--disable-content-trust` | `bool` | `true` | Skip image signing |
| `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.<br>Image index won't be pushed, meaning that other manifests, including attestations won't be preserved.<br>'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) |
| `-q`, `--quiet` | `bool` | | Suppress verbose output |

View File

@@ -9,12 +9,11 @@ Download an image from a registry
### Options
| Name | Type | Default | Description |
|:--------------------------|:---------|:--------|:-------------------------------------------------|
| `-a`, `--all-tags` | `bool` | | Download all tagged images in the repository |
| `--disable-content-trust` | `bool` | `true` | Skip image verification |
| `--platform` | `string` | | Set platform if server is multi-platform capable |
| `-q`, `--quiet` | `bool` | | Suppress verbose output |
| Name | Type | Default | Description |
|:-------------------|:---------|:--------|:-------------------------------------------------|
| `-a`, `--all-tags` | `bool` | | Download all tagged images in the repository |
| `--platform` | `string` | | Set platform if server is multi-platform capable |
| `-q`, `--quiet` | `bool` | | Suppress verbose output |
<!---MARKER_GEN_END-->

View File

@@ -9,12 +9,11 @@ Upload an image to a registry
### Options
| Name | Type | Default | Description |
|:--------------------------|:---------|:--------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-a`, `--all-tags` | `bool` | | Push all tags of an image to the repository |
| `--disable-content-trust` | `bool` | `true` | Skip image signing |
| `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.<br>Image index won't be pushed, meaning that other manifests, including attestations won't be preserved.<br>'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) |
| `-q`, `--quiet` | `bool` | | Suppress verbose output |
| Name | Type | Default | Description |
|:-------------------|:---------|:--------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-a`, `--all-tags` | `bool` | | Push all tags of an image to the repository |
| `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.<br>Image index won't be pushed, meaning that other manifests, including attestations won't be preserved.<br>'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) |
| `-q`, `--quiet` | `bool` | | Suppress verbose output |
<!---MARKER_GEN_END-->

View File

@@ -39,7 +39,6 @@ Create and run a new container from an image
| `--device-read-iops` | `list` | | Limit read rate (IO per second) from a device |
| `--device-write-bps` | `list` | | Limit write rate (bytes per second) to a device |
| `--device-write-iops` | `list` | | Limit write rate (IO per second) to a device |
| `--disable-content-trust` | `bool` | `true` | Skip image verification |
| `--dns` | `list` | | Set custom DNS servers |
| `--dns-option` | `list` | | Set DNS options |
| `--dns-search` | `list` | | Set custom DNS search domains |

View File

@@ -34,7 +34,7 @@ func NewRegistryClient(resolver AuthConfigResolver, userAgent string, insecure b
}
// AuthConfigResolver returns Auth Configuration for an index
type AuthConfigResolver func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig
type AuthConfigResolver func(ctx context.Context, hostName string) registrytypes.AuthConfig
type client struct {
authConfigResolver AuthConfigResolver
@@ -146,7 +146,7 @@ func (c *client) getRepositoryForReference(ctx context.Context, ref reference.Na
func (c *client) getHTTPTransportForRepoEndpoint(ctx context.Context, repoEndpoint repositoryEndpoint) (http.RoundTripper, error) {
httpTransport, err := getHTTPTransport(
c.authConfigResolver(ctx, repoEndpoint.indexInfo),
c.authConfigResolver(ctx, repoEndpoint.indexInfo.Name),
repoEndpoint.endpoint,
repoEndpoint.repoName,
c.userAgent,