diff --git a/cli/command/container/create.go b/cli/command/container/create.go index 7dc644d28c..d5e63bd9ef 100644 --- a/cli/command/container/create.go +++ b/cli/command/container/create.go @@ -170,7 +170,7 @@ func createContainer(ctx context.Context, dockerCli *command.DockerCli, config * if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() { var err error - trustedRef, err = image.TrustedReference(ctx, dockerCli, ref) + trustedRef, err = image.TrustedReference(ctx, dockerCli, ref, nil) if err != nil { return nil, err } diff --git a/cli/command/image/build.go b/cli/command/image/build.go index e3e7ff2b02..0c88af5fcd 100644 --- a/cli/command/image/build.go +++ b/cli/command/image/build.go @@ -235,7 +235,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { var resolvedTags []*resolvedTag if command.IsTrusted() { translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) { - return TrustedReference(ctx, dockerCli, ref) + return TrustedReference(ctx, dockerCli, ref, nil) } // Wrap the tar archive to replace the Dockerfile entry with the rewritten // Dockerfile which uses trusted pulls. diff --git a/cli/command/image/trust.go b/cli/command/image/trust.go index f32c301959..5136a22156 100644 --- a/cli/command/image/trust.go +++ b/cli/command/image/trust.go @@ -39,6 +39,11 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry defer responseBody.Close() + return PushTrustedReference(cli, repoInfo, ref, authConfig, responseBody) +} + +// PushTrustedReference pushes a canonical reference to the trust server. +func PushTrustedReference(cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.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. target := &client.Target{} @@ -75,14 +80,14 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry default: // We want trust signatures to always take an explicit tag, // otherwise it will act as an untrusted push. - if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil); err != nil { + if err := jsonmessage.DisplayJSONMessagesToStream(in, cli.Out(), nil); err != nil { return err } fmt.Fprintln(cli.Out(), "No tag specified, skipping trust metadata push") return nil } - if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), handleTarget); err != nil { + if err := jsonmessage.DisplayJSONMessagesToStream(in, cli.Out(), handleTarget); err != nil { return err } @@ -315,8 +320,16 @@ func imagePullPrivileged(ctx context.Context, cli *command.DockerCli, authConfig } // TrustedReference returns the canonical trusted reference for an image reference -func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (reference.Canonical, error) { - repoInfo, err := registry.ParseRepositoryInfo(ref) +func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged, rs registry.Service) (reference.Canonical, error) { + var ( + repoInfo *registry.RepositoryInfo + err error + ) + if rs != nil { + repoInfo, err = rs.ResolveRepository(ref) + } else { + repoInfo, err = registry.ParseRepositoryInfo(ref) + } if err != nil { return nil, err } @@ -332,7 +345,7 @@ func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) if err != nil { - return nil, err + return nil, trust.NotaryError(repoInfo.FullName(), err) } // Only list tags in the top level targets role or the releases delegation role - ignore // all other delegation roles diff --git a/cli/command/plugin/install.go b/cli/command/plugin/install.go index 71bdeeff22..a64dc2525a 100644 --- a/cli/command/plugin/install.go +++ b/cli/command/plugin/install.go @@ -11,6 +11,7 @@ import ( registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/image" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/reference" "github.com/docker/docker/registry" @@ -46,6 +47,8 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install") flags.StringVar(&options.alias, "alias", "", "Local name for plugin") + command.AddTrustedFlags(flags, true) + return cmd } @@ -63,6 +66,24 @@ func getRepoIndexFromUnnormalizedRef(ref distreference.Named) (*registrytypes.In return repoInfo.Index, nil } +type pluginRegistryService struct { + registry.Service +} + +func (s pluginRegistryService) ResolveRepository(name reference.Named) (repoInfo *registry.RepositoryInfo, err error) { + repoInfo, err = s.Service.ResolveRepository(name) + if repoInfo != nil { + repoInfo.Class = "plugin" + } + return +} + +func newRegistryService() registry.Service { + return pluginRegistryService{ + Service: registry.NewService(registry.ServiceOptions{V2Only: true}), + } +} + func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { // Parse name using distribution reference package to support name // containing both tag and digest. Names with both tag and digest @@ -85,13 +106,41 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { } alias = aref.String() } + ctx := context.Background() index, err := getRepoIndexFromUnnormalizedRef(ref) if err != nil { return err } - ctx := context.Background() + remote := ref.String() + + _, isCanonical := ref.(distreference.Canonical) + if command.IsTrusted() && !isCanonical { + if alias == "" { + alias = ref.String() + } + var nt reference.NamedTagged + named, err := reference.ParseNamed(ref.Name()) + if err != nil { + return err + } + if tagged, ok := ref.(distreference.Tagged); ok { + nt, err = reference.WithTag(named, tagged.Tag()) + if err != nil { + return err + } + } else { + named = reference.WithDefaultTag(named) + nt = named.(reference.NamedTagged) + } + + trusted, err := image.TrustedReference(ctx, dockerCli, nt, newRegistryService()) + if err != nil { + return err + } + remote = trusted.String() + } authConfig := command.ResolveAuthConfig(ctx, dockerCli, index) @@ -104,7 +153,7 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { options := types.PluginInstallOptions{ RegistryAuth: encodedAuth, - RemoteRef: ref.String(), + RemoteRef: remote, Disabled: opts.disable, AcceptAllPermissions: opts.grantPerms, AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.name), diff --git a/cli/command/plugin/push.go b/cli/command/plugin/push.go index 667379cdd2..b0766307f3 100644 --- a/cli/command/plugin/push.go +++ b/cli/command/plugin/push.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/image" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/reference" "github.com/docker/docker/registry" @@ -22,6 +23,11 @@ func newPushCommand(dockerCli *command.DockerCli) *cobra.Command { return runPush(dockerCli, args[0]) }, } + + flags := cmd.Flags() + + command.AddTrustedFlags(flags, true) + return cmd } @@ -55,5 +61,11 @@ func runPush(dockerCli *command.DockerCli, name string) error { return err } defer responseBody.Close() + + if command.IsTrusted() { + repoInfo.Class = "plugin" + return image.PushTrustedReference(dockerCli, repoInfo, named, authConfig, responseBody) + } + return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil) } diff --git a/cli/trust/trust.go b/cli/trust/trust.go index 0f3482f2d7..51914f74b0 100644 --- a/cli/trust/trust.go +++ b/cli/trust/trust.go @@ -147,8 +147,19 @@ func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryI } } + scope := auth.RepositoryScope{ + Repository: repoInfo.FullName(), + Actions: actions, + Class: repoInfo.Class, + } creds := simpleCredentialStore{auth: authConfig} - tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), actions...) + tokenHandlerOptions := auth.TokenHandlerOptions{ + Transport: authTransport, + Credentials: creds, + Scopes: []auth.Scope{scope}, + ClientID: registry.AuthClientID, + } + tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions) basicHandler := auth.NewBasicHandler(creds) modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))) tr := transport.NewTransport(base, modifiers...) diff --git a/integration-cli/docker_cli_plugins_test.go b/integration-cli/docker_cli_plugins_test.go index f75e3bedc6..a25df13731 100644 --- a/integration-cli/docker_cli_plugins_test.go +++ b/integration-cli/docker_cli_plugins_test.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "os/exec" "github.com/docker/docker/pkg/integration/checker" "github.com/go-check/check" @@ -269,3 +270,63 @@ func (s *DockerSuite) TestPluginInspectOnWindows(c *check.C) { c.Assert(out, checker.Contains, "plugins are not supported on this platform") c.Assert(err.Error(), checker.Contains, "plugins are not supported on this platform") } + +func (s *DockerTrustSuite) TestPluginTrustedInstall(c *check.C) { + testRequires(c, DaemonIsLinux, IsAmd64, Network) + + trustedName := s.setupTrustedplugin(c, pNameWithTag, "trusted-plugin-install") + + installCmd := exec.Command(dockerBinary, "plugin", "install", "--grant-all-permissions", trustedName) + s.trustedCmd(installCmd) + out, _, err := runCommandWithOutput(installCmd) + + c.Assert(strings.TrimSpace(out), checker.Contains, trustedName) + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Contains, trustedName) + + out, _, err = dockerCmdWithError("plugin", "ls") + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, "true") + + out, _, err = dockerCmdWithError("plugin", "disable", trustedName) + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Contains, trustedName) + + out, _, err = dockerCmdWithError("plugin", "enable", trustedName) + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Contains, trustedName) + + out, _, err = dockerCmdWithError("plugin", "rm", "-f", trustedName) + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Contains, trustedName) + + // Try untrusted pull to ensure we pushed the tag to the registry + installCmd = exec.Command(dockerBinary, "plugin", "install", "--disable-content-trust=true", "--grant-all-permissions", trustedName) + s.trustedCmd(installCmd) + out, _, err = runCommandWithOutput(installCmd) + c.Assert(err, check.IsNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Status: Downloaded", check.Commentf(out)) + + out, _, err = dockerCmdWithError("plugin", "ls") + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, "true") + +} + +func (s *DockerTrustSuite) TestPluginUntrustedInstall(c *check.C) { + testRequires(c, DaemonIsLinux, IsAmd64, Network) + + pluginName := fmt.Sprintf("%v/dockercliuntrusted/plugintest:latest", privateRegistryURL) + // install locally and push to private registry + dockerCmd(c, "plugin", "install", "--grant-all-permissions", "--alias", pluginName, pNameWithTag) + dockerCmd(c, "plugin", "push", pluginName) + dockerCmd(c, "plugin", "rm", "-f", pluginName) + + // Try trusted install on untrusted plugin + installCmd := exec.Command(dockerBinary, "plugin", "install", "--grant-all-permissions", pluginName) + s.trustedCmd(installCmd) + out, _, err := runCommandWithOutput(installCmd) + + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Error: remote trust data does not exist", check.Commentf(out)) +} diff --git a/integration-cli/trust_server.go b/integration-cli/trust_server.go index 0c815a8f0b..18876311a1 100644 --- a/integration-cli/trust_server.go +++ b/integration-cli/trust_server.go @@ -211,6 +211,29 @@ func (s *DockerTrustSuite) setupTrustedImage(c *check.C, name string) string { return repoName } +func (s *DockerTrustSuite) setupTrustedplugin(c *check.C, source, name string) string { + repoName := fmt.Sprintf("%v/dockercli/%s:latest", privateRegistryURL, name) + // tag the image and upload it to the private registry + dockerCmd(c, "plugin", "install", "--grant-all-permissions", "--alias", repoName, source) + + pushCmd := exec.Command(dockerBinary, "plugin", "push", repoName) + s.trustedCmd(pushCmd) + out, _, err := runCommandWithOutput(pushCmd) + + if err != nil { + c.Fatalf("Error running trusted plugin push: %s\n%s", err, out) + } + if !strings.Contains(string(out), "Signing and pushing trust metadata") { + c.Fatalf("Missing expected output on trusted push:\n%s", out) + } + + if out, status := dockerCmd(c, "plugin", "rm", "-f", repoName); status != 0 { + c.Fatalf("Error removing plugin %q\n%s", repoName, out) + } + + return repoName +} + func notaryClientEnv(cmd *exec.Cmd) { pwd := "12345678" env := []string{