Add integration test for docker content trust
Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
| ... | ... |
@@ -170,7 +170,7 @@ func createContainer(ctx context.Context, dockerCli *command.DockerCli, config * |
| 170 | 170 |
|
| 171 | 171 |
if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() {
|
| 172 | 172 |
var err error |
| 173 |
- trustedRef, err = image.TrustedReference(ctx, dockerCli, ref) |
|
| 173 |
+ trustedRef, err = image.TrustedReference(ctx, dockerCli, ref, nil) |
|
| 174 | 174 |
if err != nil {
|
| 175 | 175 |
return nil, err |
| 176 | 176 |
} |
| ... | ... |
@@ -235,7 +235,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
|
| 235 | 235 |
var resolvedTags []*resolvedTag |
| 236 | 236 |
if command.IsTrusted() {
|
| 237 | 237 |
translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) {
|
| 238 |
- return TrustedReference(ctx, dockerCli, ref) |
|
| 238 |
+ return TrustedReference(ctx, dockerCli, ref, nil) |
|
| 239 | 239 |
} |
| 240 | 240 |
// Wrap the tar archive to replace the Dockerfile entry with the rewritten |
| 241 | 241 |
// Dockerfile which uses trusted pulls. |
| ... | ... |
@@ -39,6 +39,11 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry |
| 39 | 39 |
|
| 40 | 40 |
defer responseBody.Close() |
| 41 | 41 |
|
| 42 |
+ return PushTrustedReference(cli, repoInfo, ref, authConfig, responseBody) |
|
| 43 |
+} |
|
| 44 |
+ |
|
| 45 |
+// PushTrustedReference pushes a canonical reference to the trust server. |
|
| 46 |
+func PushTrustedReference(cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, in io.Reader) error {
|
|
| 42 | 47 |
// If it is a trusted push we would like to find the target entry which match the |
| 43 | 48 |
// tag provided in the function and then do an AddTarget later. |
| 44 | 49 |
target := &client.Target{}
|
| ... | ... |
@@ -75,14 +80,14 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry |
| 75 | 75 |
default: |
| 76 | 76 |
// We want trust signatures to always take an explicit tag, |
| 77 | 77 |
// otherwise it will act as an untrusted push. |
| 78 |
- if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil); err != nil {
|
|
| 78 |
+ if err := jsonmessage.DisplayJSONMessagesToStream(in, cli.Out(), nil); err != nil {
|
|
| 79 | 79 |
return err |
| 80 | 80 |
} |
| 81 | 81 |
fmt.Fprintln(cli.Out(), "No tag specified, skipping trust metadata push") |
| 82 | 82 |
return nil |
| 83 | 83 |
} |
| 84 | 84 |
|
| 85 |
- if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), handleTarget); err != nil {
|
|
| 85 |
+ if err := jsonmessage.DisplayJSONMessagesToStream(in, cli.Out(), handleTarget); err != nil {
|
|
| 86 | 86 |
return err |
| 87 | 87 |
} |
| 88 | 88 |
|
| ... | ... |
@@ -315,8 +320,16 @@ func imagePullPrivileged(ctx context.Context, cli *command.DockerCli, authConfig |
| 315 | 315 |
} |
| 316 | 316 |
|
| 317 | 317 |
// TrustedReference returns the canonical trusted reference for an image reference |
| 318 |
-func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (reference.Canonical, error) {
|
|
| 319 |
- repoInfo, err := registry.ParseRepositoryInfo(ref) |
|
| 318 |
+func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged, rs registry.Service) (reference.Canonical, error) {
|
|
| 319 |
+ var ( |
|
| 320 |
+ repoInfo *registry.RepositoryInfo |
|
| 321 |
+ err error |
|
| 322 |
+ ) |
|
| 323 |
+ if rs != nil {
|
|
| 324 |
+ repoInfo, err = rs.ResolveRepository(ref) |
|
| 325 |
+ } else {
|
|
| 326 |
+ repoInfo, err = registry.ParseRepositoryInfo(ref) |
|
| 327 |
+ } |
|
| 320 | 328 |
if err != nil {
|
| 321 | 329 |
return nil, err |
| 322 | 330 |
} |
| ... | ... |
@@ -332,7 +345,7 @@ func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference |
| 332 | 332 |
|
| 333 | 333 |
t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) |
| 334 | 334 |
if err != nil {
|
| 335 |
- return nil, err |
|
| 335 |
+ return nil, trust.NotaryError(repoInfo.FullName(), err) |
|
| 336 | 336 |
} |
| 337 | 337 |
// Only list tags in the top level targets role or the releases delegation role - ignore |
| 338 | 338 |
// all other delegation roles |
| ... | ... |
@@ -11,6 +11,7 @@ import ( |
| 11 | 11 |
registrytypes "github.com/docker/docker/api/types/registry" |
| 12 | 12 |
"github.com/docker/docker/cli" |
| 13 | 13 |
"github.com/docker/docker/cli/command" |
| 14 |
+ "github.com/docker/docker/cli/command/image" |
|
| 14 | 15 |
"github.com/docker/docker/pkg/jsonmessage" |
| 15 | 16 |
"github.com/docker/docker/reference" |
| 16 | 17 |
"github.com/docker/docker/registry" |
| ... | ... |
@@ -46,6 +47,8 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 46 | 46 |
flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install") |
| 47 | 47 |
flags.StringVar(&options.alias, "alias", "", "Local name for plugin") |
| 48 | 48 |
|
| 49 |
+ command.AddTrustedFlags(flags, true) |
|
| 50 |
+ |
|
| 49 | 51 |
return cmd |
| 50 | 52 |
} |
| 51 | 53 |
|
| ... | ... |
@@ -63,6 +66,24 @@ func getRepoIndexFromUnnormalizedRef(ref distreference.Named) (*registrytypes.In |
| 63 | 63 |
return repoInfo.Index, nil |
| 64 | 64 |
} |
| 65 | 65 |
|
| 66 |
+type pluginRegistryService struct {
|
|
| 67 |
+ registry.Service |
|
| 68 |
+} |
|
| 69 |
+ |
|
| 70 |
+func (s pluginRegistryService) ResolveRepository(name reference.Named) (repoInfo *registry.RepositoryInfo, err error) {
|
|
| 71 |
+ repoInfo, err = s.Service.ResolveRepository(name) |
|
| 72 |
+ if repoInfo != nil {
|
|
| 73 |
+ repoInfo.Class = "plugin" |
|
| 74 |
+ } |
|
| 75 |
+ return |
|
| 76 |
+} |
|
| 77 |
+ |
|
| 78 |
+func newRegistryService() registry.Service {
|
|
| 79 |
+ return pluginRegistryService{
|
|
| 80 |
+ Service: registry.NewService(registry.ServiceOptions{V2Only: true}),
|
|
| 81 |
+ } |
|
| 82 |
+} |
|
| 83 |
+ |
|
| 66 | 84 |
func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
|
| 67 | 85 |
// Parse name using distribution reference package to support name |
| 68 | 86 |
// containing both tag and digest. Names with both tag and digest |
| ... | ... |
@@ -85,13 +106,41 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
|
| 85 | 85 |
} |
| 86 | 86 |
alias = aref.String() |
| 87 | 87 |
} |
| 88 |
+ ctx := context.Background() |
|
| 88 | 89 |
|
| 89 | 90 |
index, err := getRepoIndexFromUnnormalizedRef(ref) |
| 90 | 91 |
if err != nil {
|
| 91 | 92 |
return err |
| 92 | 93 |
} |
| 93 | 94 |
|
| 94 |
- ctx := context.Background() |
|
| 95 |
+ remote := ref.String() |
|
| 96 |
+ |
|
| 97 |
+ _, isCanonical := ref.(distreference.Canonical) |
|
| 98 |
+ if command.IsTrusted() && !isCanonical {
|
|
| 99 |
+ if alias == "" {
|
|
| 100 |
+ alias = ref.String() |
|
| 101 |
+ } |
|
| 102 |
+ var nt reference.NamedTagged |
|
| 103 |
+ named, err := reference.ParseNamed(ref.Name()) |
|
| 104 |
+ if err != nil {
|
|
| 105 |
+ return err |
|
| 106 |
+ } |
|
| 107 |
+ if tagged, ok := ref.(distreference.Tagged); ok {
|
|
| 108 |
+ nt, err = reference.WithTag(named, tagged.Tag()) |
|
| 109 |
+ if err != nil {
|
|
| 110 |
+ return err |
|
| 111 |
+ } |
|
| 112 |
+ } else {
|
|
| 113 |
+ named = reference.WithDefaultTag(named) |
|
| 114 |
+ nt = named.(reference.NamedTagged) |
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ trusted, err := image.TrustedReference(ctx, dockerCli, nt, newRegistryService()) |
|
| 118 |
+ if err != nil {
|
|
| 119 |
+ return err |
|
| 120 |
+ } |
|
| 121 |
+ remote = trusted.String() |
|
| 122 |
+ } |
|
| 95 | 123 |
|
| 96 | 124 |
authConfig := command.ResolveAuthConfig(ctx, dockerCli, index) |
| 97 | 125 |
|
| ... | ... |
@@ -104,7 +153,7 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error {
|
| 104 | 104 |
|
| 105 | 105 |
options := types.PluginInstallOptions{
|
| 106 | 106 |
RegistryAuth: encodedAuth, |
| 107 |
- RemoteRef: ref.String(), |
|
| 107 |
+ RemoteRef: remote, |
|
| 108 | 108 |
Disabled: opts.disable, |
| 109 | 109 |
AcceptAllPermissions: opts.grantPerms, |
| 110 | 110 |
AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.name), |
| ... | ... |
@@ -7,6 +7,7 @@ import ( |
| 7 | 7 |
|
| 8 | 8 |
"github.com/docker/docker/cli" |
| 9 | 9 |
"github.com/docker/docker/cli/command" |
| 10 |
+ "github.com/docker/docker/cli/command/image" |
|
| 10 | 11 |
"github.com/docker/docker/pkg/jsonmessage" |
| 11 | 12 |
"github.com/docker/docker/reference" |
| 12 | 13 |
"github.com/docker/docker/registry" |
| ... | ... |
@@ -22,6 +23,11 @@ func newPushCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 22 | 22 |
return runPush(dockerCli, args[0]) |
| 23 | 23 |
}, |
| 24 | 24 |
} |
| 25 |
+ |
|
| 26 |
+ flags := cmd.Flags() |
|
| 27 |
+ |
|
| 28 |
+ command.AddTrustedFlags(flags, true) |
|
| 29 |
+ |
|
| 25 | 30 |
return cmd |
| 26 | 31 |
} |
| 27 | 32 |
|
| ... | ... |
@@ -55,5 +61,11 @@ func runPush(dockerCli *command.DockerCli, name string) error {
|
| 55 | 55 |
return err |
| 56 | 56 |
} |
| 57 | 57 |
defer responseBody.Close() |
| 58 |
+ |
|
| 59 |
+ if command.IsTrusted() {
|
|
| 60 |
+ repoInfo.Class = "plugin" |
|
| 61 |
+ return image.PushTrustedReference(dockerCli, repoInfo, named, authConfig, responseBody) |
|
| 62 |
+ } |
|
| 63 |
+ |
|
| 58 | 64 |
return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil) |
| 59 | 65 |
} |
| ... | ... |
@@ -147,8 +147,19 @@ func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryI |
| 147 | 147 |
} |
| 148 | 148 |
} |
| 149 | 149 |
|
| 150 |
+ scope := auth.RepositoryScope{
|
|
| 151 |
+ Repository: repoInfo.FullName(), |
|
| 152 |
+ Actions: actions, |
|
| 153 |
+ Class: repoInfo.Class, |
|
| 154 |
+ } |
|
| 150 | 155 |
creds := simpleCredentialStore{auth: authConfig}
|
| 151 |
- tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), actions...) |
|
| 156 |
+ tokenHandlerOptions := auth.TokenHandlerOptions{
|
|
| 157 |
+ Transport: authTransport, |
|
| 158 |
+ Credentials: creds, |
|
| 159 |
+ Scopes: []auth.Scope{scope},
|
|
| 160 |
+ ClientID: registry.AuthClientID, |
|
| 161 |
+ } |
|
| 162 |
+ tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions) |
|
| 152 | 163 |
basicHandler := auth.NewBasicHandler(creds) |
| 153 | 164 |
modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))) |
| 154 | 165 |
tr := transport.NewTransport(base, modifiers...) |
| ... | ... |
@@ -2,6 +2,7 @@ package main |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"fmt" |
| 5 |
+ "os/exec" |
|
| 5 | 6 |
|
| 6 | 7 |
"github.com/docker/docker/pkg/integration/checker" |
| 7 | 8 |
"github.com/go-check/check" |
| ... | ... |
@@ -269,3 +270,63 @@ func (s *DockerSuite) TestPluginInspectOnWindows(c *check.C) {
|
| 269 | 269 |
c.Assert(out, checker.Contains, "plugins are not supported on this platform") |
| 270 | 270 |
c.Assert(err.Error(), checker.Contains, "plugins are not supported on this platform") |
| 271 | 271 |
} |
| 272 |
+ |
|
| 273 |
+func (s *DockerTrustSuite) TestPluginTrustedInstall(c *check.C) {
|
|
| 274 |
+ testRequires(c, DaemonIsLinux, IsAmd64, Network) |
|
| 275 |
+ |
|
| 276 |
+ trustedName := s.setupTrustedplugin(c, pNameWithTag, "trusted-plugin-install") |
|
| 277 |
+ |
|
| 278 |
+ installCmd := exec.Command(dockerBinary, "plugin", "install", "--grant-all-permissions", trustedName) |
|
| 279 |
+ s.trustedCmd(installCmd) |
|
| 280 |
+ out, _, err := runCommandWithOutput(installCmd) |
|
| 281 |
+ |
|
| 282 |
+ c.Assert(strings.TrimSpace(out), checker.Contains, trustedName) |
|
| 283 |
+ c.Assert(err, checker.IsNil) |
|
| 284 |
+ c.Assert(strings.TrimSpace(out), checker.Contains, trustedName) |
|
| 285 |
+ |
|
| 286 |
+ out, _, err = dockerCmdWithError("plugin", "ls")
|
|
| 287 |
+ c.Assert(err, checker.IsNil) |
|
| 288 |
+ c.Assert(out, checker.Contains, "true") |
|
| 289 |
+ |
|
| 290 |
+ out, _, err = dockerCmdWithError("plugin", "disable", trustedName)
|
|
| 291 |
+ c.Assert(err, checker.IsNil) |
|
| 292 |
+ c.Assert(strings.TrimSpace(out), checker.Contains, trustedName) |
|
| 293 |
+ |
|
| 294 |
+ out, _, err = dockerCmdWithError("plugin", "enable", trustedName)
|
|
| 295 |
+ c.Assert(err, checker.IsNil) |
|
| 296 |
+ c.Assert(strings.TrimSpace(out), checker.Contains, trustedName) |
|
| 297 |
+ |
|
| 298 |
+ out, _, err = dockerCmdWithError("plugin", "rm", "-f", trustedName)
|
|
| 299 |
+ c.Assert(err, checker.IsNil) |
|
| 300 |
+ c.Assert(strings.TrimSpace(out), checker.Contains, trustedName) |
|
| 301 |
+ |
|
| 302 |
+ // Try untrusted pull to ensure we pushed the tag to the registry |
|
| 303 |
+ installCmd = exec.Command(dockerBinary, "plugin", "install", "--disable-content-trust=true", "--grant-all-permissions", trustedName) |
|
| 304 |
+ s.trustedCmd(installCmd) |
|
| 305 |
+ out, _, err = runCommandWithOutput(installCmd) |
|
| 306 |
+ c.Assert(err, check.IsNil, check.Commentf(out)) |
|
| 307 |
+ c.Assert(string(out), checker.Contains, "Status: Downloaded", check.Commentf(out)) |
|
| 308 |
+ |
|
| 309 |
+ out, _, err = dockerCmdWithError("plugin", "ls")
|
|
| 310 |
+ c.Assert(err, checker.IsNil) |
|
| 311 |
+ c.Assert(out, checker.Contains, "true") |
|
| 312 |
+ |
|
| 313 |
+} |
|
| 314 |
+ |
|
| 315 |
+func (s *DockerTrustSuite) TestPluginUntrustedInstall(c *check.C) {
|
|
| 316 |
+ testRequires(c, DaemonIsLinux, IsAmd64, Network) |
|
| 317 |
+ |
|
| 318 |
+ pluginName := fmt.Sprintf("%v/dockercliuntrusted/plugintest:latest", privateRegistryURL)
|
|
| 319 |
+ // install locally and push to private registry |
|
| 320 |
+ dockerCmd(c, "plugin", "install", "--grant-all-permissions", "--alias", pluginName, pNameWithTag) |
|
| 321 |
+ dockerCmd(c, "plugin", "push", pluginName) |
|
| 322 |
+ dockerCmd(c, "plugin", "rm", "-f", pluginName) |
|
| 323 |
+ |
|
| 324 |
+ // Try trusted install on untrusted plugin |
|
| 325 |
+ installCmd := exec.Command(dockerBinary, "plugin", "install", "--grant-all-permissions", pluginName) |
|
| 326 |
+ s.trustedCmd(installCmd) |
|
| 327 |
+ out, _, err := runCommandWithOutput(installCmd) |
|
| 328 |
+ |
|
| 329 |
+ c.Assert(err, check.NotNil, check.Commentf(out)) |
|
| 330 |
+ c.Assert(string(out), checker.Contains, "Error: remote trust data does not exist", check.Commentf(out)) |
|
| 331 |
+} |
| ... | ... |
@@ -211,6 +211,29 @@ func (s *DockerTrustSuite) setupTrustedImage(c *check.C, name string) string {
|
| 211 | 211 |
return repoName |
| 212 | 212 |
} |
| 213 | 213 |
|
| 214 |
+func (s *DockerTrustSuite) setupTrustedplugin(c *check.C, source, name string) string {
|
|
| 215 |
+ repoName := fmt.Sprintf("%v/dockercli/%s:latest", privateRegistryURL, name)
|
|
| 216 |
+ // tag the image and upload it to the private registry |
|
| 217 |
+ dockerCmd(c, "plugin", "install", "--grant-all-permissions", "--alias", repoName, source) |
|
| 218 |
+ |
|
| 219 |
+ pushCmd := exec.Command(dockerBinary, "plugin", "push", repoName) |
|
| 220 |
+ s.trustedCmd(pushCmd) |
|
| 221 |
+ out, _, err := runCommandWithOutput(pushCmd) |
|
| 222 |
+ |
|
| 223 |
+ if err != nil {
|
|
| 224 |
+ c.Fatalf("Error running trusted plugin push: %s\n%s", err, out)
|
|
| 225 |
+ } |
|
| 226 |
+ if !strings.Contains(string(out), "Signing and pushing trust metadata") {
|
|
| 227 |
+ c.Fatalf("Missing expected output on trusted push:\n%s", out)
|
|
| 228 |
+ } |
|
| 229 |
+ |
|
| 230 |
+ if out, status := dockerCmd(c, "plugin", "rm", "-f", repoName); status != 0 {
|
|
| 231 |
+ c.Fatalf("Error removing plugin %q\n%s", repoName, out)
|
|
| 232 |
+ } |
|
| 233 |
+ |
|
| 234 |
+ return repoName |
|
| 235 |
+} |
|
| 236 |
+ |
|
| 214 | 237 |
func notaryClientEnv(cmd *exec.Cmd) {
|
| 215 | 238 |
pwd := "12345678" |
| 216 | 239 |
env := []string{
|