From 261f9b62438468579cd8b1835dee097e66012384 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 6 Dec 2016 11:27:27 -0800 Subject: [PATCH 1/4] registry: Remove reference.go This removes some very old vestigial code that really should have been removed during the content addressability transition. It implements something called "reference" but it behaves differently from the actual reference package. This was only used by client-side content trust code, and is relatively easy to extricate. Signed-off-by: Aaron Lehmann (cherry picked from commit d91ed88365317cd86555e2f54bffa30ec6590dfe) --- cli/command/image/pull.go | 17 +++------- cli/command/image/trust.go | 55 ++++++++++++++---------------- registry/reference.go | 68 -------------------------------------- 3 files changed, 29 insertions(+), 111 deletions(-) delete mode 100644 registry/reference.go diff --git a/cli/command/image/pull.go b/cli/command/image/pull.go index 9116d45840..13de492f92 100644 --- a/cli/command/image/pull.go +++ b/cli/command/image/pull.go @@ -55,16 +55,6 @@ func runPull(dockerCli *command.DockerCli, opts pullOptions) error { fmt.Fprintf(dockerCli.Out(), "Using default tag: %s\n", reference.DefaultTag) } - var tag string - switch x := distributionRef.(type) { - case reference.Canonical: - tag = x.Digest().String() - case reference.NamedTagged: - tag = x.Tag() - } - - registryRef := registry.ParseReference(tag) - // Resolve the Repository name from fqn to RepositoryInfo repoInfo, err := registry.ParseRepositoryInfo(distributionRef) if err != nil { @@ -76,9 +66,10 @@ func runPull(dockerCli *command.DockerCli, opts pullOptions) error { authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "pull") - if command.IsTrusted() && !registryRef.HasDigest() { - // Check if tag is digest - err = trustedPull(ctx, dockerCli, repoInfo, registryRef, authConfig, requestPrivilege) + // Check if reference has a digest + _, isCanonical := distributionRef.(reference.Canonical) + if command.IsTrusted() && !isCanonical { + err = trustedPull(ctx, dockerCli, repoInfo, distributionRef, authConfig, requestPrivilege) } else { err = imagePullPrivileged(ctx, dockerCli, authConfig, distributionRef.String(), requestPrivilege, opts.all) } diff --git a/cli/command/image/trust.go b/cli/command/image/trust.go index d1106b532e..8f5c76d8ca 100644 --- a/cli/command/image/trust.go +++ b/cli/command/image/trust.go @@ -46,9 +46,9 @@ var ( ) type target struct { - reference registry.Reference - digest digest.Digest - size int64 + name string + digest digest.Digest + size int64 } // trustedPush handles content trust pushing of an image @@ -81,7 +81,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry target = nil return } - target.Name = registry.ParseReference(pushResult.Tag).String() + target.Name = pushResult.Tag target.Hashes = data.Hashes{string(pushResult.Digest.Algorithm()): h} target.Length = int64(pushResult.Size) } @@ -93,11 +93,9 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry return errors.New("cannot push a digest reference") case reference.NamedTagged: tag = x.Tag() - } - - // We want trust signatures to always take an explicit tag, - // otherwise it will act as an untrusted push. - if tag == "" { + 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 { return err } @@ -234,7 +232,7 @@ func imagePushPrivileged(ctx context.Context, cli *command.DockerCli, authConfig } // trustedPull handles content trust pulling of an image -func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { +func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { var refs []target notaryRepo, err := GetNotaryRepository(cli, repoInfo, authConfig, "pull") @@ -243,7 +241,7 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry return err } - if ref.String() == "" { + if tagged, isTagged := ref.(reference.NamedTagged); !isTagged { // List all targets targets, err := notaryRepo.ListTargets(releasesRole, data.CanonicalTargetsRole) if err != nil { @@ -266,14 +264,14 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry return notaryError(repoInfo.FullName(), fmt.Errorf("No trusted tags for %s", repoInfo.FullName())) } } else { - t, err := notaryRepo.GetTargetByName(ref.String(), releasesRole, data.CanonicalTargetsRole) + t, err := notaryRepo.GetTargetByName(tagged.Tag(), releasesRole, data.CanonicalTargetsRole) if err != nil { return notaryError(repoInfo.FullName(), 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 != releasesRole && t.Role != data.CanonicalTargetsRole { - return notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.String())) + return notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", tagged.Tag())) } logrus.Debugf("retrieving target for %s role\n", t.Role) @@ -286,7 +284,7 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry } for i, r := range refs { - displayTag := r.reference.String() + displayTag := r.name if displayTag != "" { displayTag = ":" + displayTag } @@ -300,19 +298,16 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry return err } - // If reference is not trusted, tag by trusted reference - if !r.reference.HasDigest() { - tagged, err := reference.WithTag(repoInfo, r.reference.String()) - if err != nil { - return err - } - trustedRef, err := reference.WithDigest(reference.TrimNamed(repoInfo), r.digest) - if err != nil { - return err - } - if err := TagTrusted(ctx, cli, trustedRef, tagged); err != nil { - return err - } + tagged, err := reference.WithTag(repoInfo, r.name) + if err != nil { + return err + } + trustedRef, err := reference.WithDigest(reference.TrimNamed(repoInfo), r.digest) + if err != nil { + return err + } + if err := TagTrusted(ctx, cli, trustedRef, tagged); err != nil { + return err } } return nil @@ -533,9 +528,9 @@ func convertTarget(t client.Target) (target, error) { return target{}, errors.New("no valid hash, expecting sha256") } return target{ - reference: registry.ParseReference(t.Name), - digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)), - size: t.Length, + name: t.Name, + digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)), + size: t.Length, }, nil } diff --git a/registry/reference.go b/registry/reference.go deleted file mode 100644 index e15f83eeeb..0000000000 --- a/registry/reference.go +++ /dev/null @@ -1,68 +0,0 @@ -package registry - -import ( - "strings" - - "github.com/docker/distribution/digest" -) - -// Reference represents a tag or digest within a repository -type Reference interface { - // HasDigest returns whether the reference has a verifiable - // content addressable reference which may be considered secure. - HasDigest() bool - - // ImageName returns an image name for the given repository - ImageName(string) string - - // Returns a string representation of the reference - String() string -} - -type tagReference struct { - tag string -} - -func (tr tagReference) HasDigest() bool { - return false -} - -func (tr tagReference) ImageName(repo string) string { - return repo + ":" + tr.tag -} - -func (tr tagReference) String() string { - return tr.tag -} - -type digestReference struct { - digest digest.Digest -} - -func (dr digestReference) HasDigest() bool { - return true -} - -func (dr digestReference) ImageName(repo string) string { - return repo + "@" + dr.String() -} - -func (dr digestReference) String() string { - return dr.digest.String() -} - -// ParseReference parses a reference into either a digest or tag reference -func ParseReference(ref string) Reference { - if strings.Contains(ref, ":") { - dgst, err := digest.ParseDigest(ref) - if err == nil { - return digestReference{digest: dgst} - } - } - return tagReference{tag: ref} -} - -// DigestReference creates a digest reference using a digest -func DigestReference(dgst digest.Digest) Reference { - return digestReference{digest: dgst} -} From 5e7d2ab3b8a580d811380c27433b2cab056ffd8d Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Mon, 5 Dec 2016 16:06:29 -0800 Subject: [PATCH 2/4] cli: Split out GetNotaryRepository and associated functions Split these into cli/trust so that other commands can make use of them. Signed-off-by: Aaron Lehmann (cherry picked from commit 4b8c79f25ee00ca5dfe22271c166938009bda976) --- cli/command/image/trust.go | 241 +++----------------------------- cli/command/image/trust_test.go | 9 +- cli/trust/trust.go | 221 +++++++++++++++++++++++++++++ 3 files changed, 244 insertions(+), 227 deletions(-) create mode 100644 cli/trust/trust.go diff --git a/cli/command/image/trust.go b/cli/command/image/trust.go index 8f5c76d8ca..f32c301959 100644 --- a/cli/command/image/trust.go +++ b/cli/command/image/trust.go @@ -6,43 +6,22 @@ import ( "errors" "fmt" "io" - "net" - "net/http" - "net/url" - "os" "path" - "path/filepath" "sort" - "time" "golang.org/x/net/context" "github.com/Sirupsen/logrus" "github.com/docker/distribution/digest" - "github.com/docker/distribution/registry/client/auth" - "github.com/docker/distribution/registry/client/auth/challenge" - "github.com/docker/distribution/registry/client/transport" "github.com/docker/docker/api/types" - registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/cli/command" - "github.com/docker/docker/cliconfig" + "github.com/docker/docker/cli/trust" "github.com/docker/docker/distribution" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/reference" "github.com/docker/docker/registry" - "github.com/docker/go-connections/tlsconfig" - "github.com/docker/notary" "github.com/docker/notary/client" - "github.com/docker/notary/passphrase" - "github.com/docker/notary/storage" - "github.com/docker/notary/trustmanager" - "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf/data" - "github.com/docker/notary/tuf/signed" -) - -var ( - releasesRole = path.Join(data.CanonicalTargetsRole, "releases") ) type target struct { @@ -118,7 +97,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry fmt.Fprintln(cli.Out(), "Signing and pushing trust metadata") - repo, err := GetNotaryRepository(cli, repoInfo, authConfig, "push", "pull") + repo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "push", "pull") if err != nil { fmt.Fprintf(cli.Out(), "Error establishing connection to notary repository: %s\n", err) return err @@ -145,7 +124,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry // Initialize the notary repository with a remotely managed snapshot key if err := repo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil { - return notaryError(repoInfo.FullName(), err) + return trust.NotaryError(repoInfo.FullName(), err) } fmt.Fprintf(cli.Out(), "Finished initializing %q\n", repoInfo.FullName()) err = repo.AddTarget(target, data.CanonicalTargetsRole) @@ -153,7 +132,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry // already initialized and we have successfully downloaded the latest metadata err = addTargetToAllSignableRoles(repo, target) default: - return notaryError(repoInfo.FullName(), err) + return trust.NotaryError(repoInfo.FullName(), err) } if err == nil { @@ -162,7 +141,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry if err != nil { fmt.Fprintf(cli.Out(), "Failed to sign %q:%s - %s\n", repoInfo.FullName(), tag, err.Error()) - return notaryError(repoInfo.FullName(), err) + return trust.NotaryError(repoInfo.FullName(), err) } fmt.Fprintf(cli.Out(), "Successfully signed %q:%s\n", repoInfo.FullName(), tag) @@ -235,7 +214,7 @@ func imagePushPrivileged(ctx context.Context, cli *command.DockerCli, authConfig func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { var refs []target - notaryRepo, err := GetNotaryRepository(cli, repoInfo, authConfig, "pull") + notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull") if err != nil { fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err) return err @@ -243,9 +222,9 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry if tagged, isTagged := ref.(reference.NamedTagged); !isTagged { // List all targets - targets, err := notaryRepo.ListTargets(releasesRole, data.CanonicalTargetsRole) + targets, err := notaryRepo.ListTargets(trust.ReleasesRole, data.CanonicalTargetsRole) if err != nil { - return notaryError(repoInfo.FullName(), err) + return trust.NotaryError(repoInfo.FullName(), err) } for _, tgt := range targets { t, err := convertTarget(tgt.Target) @@ -255,23 +234,23 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry } // Only list tags in the top level targets role or the releases delegation role - ignore // all other delegation roles - if tgt.Role != releasesRole && tgt.Role != data.CanonicalTargetsRole { + if tgt.Role != trust.ReleasesRole && tgt.Role != data.CanonicalTargetsRole { continue } refs = append(refs, t) } if len(refs) == 0 { - return notaryError(repoInfo.FullName(), fmt.Errorf("No trusted tags for %s", repoInfo.FullName())) + return trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trusted tags for %s", repoInfo.FullName())) } } else { - t, err := notaryRepo.GetTargetByName(tagged.Tag(), releasesRole, data.CanonicalTargetsRole) + t, err := notaryRepo.GetTargetByName(tagged.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) if err != nil { - return notaryError(repoInfo.FullName(), err) + return trust.NotaryError(repoInfo.FullName(), 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 != releasesRole && t.Role != data.CanonicalTargetsRole { - return notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", tagged.Tag())) + if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { + return trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", tagged.Tag())) } logrus.Debugf("retrieving target for %s role\n", t.Role) @@ -335,159 +314,6 @@ func imagePullPrivileged(ctx context.Context, cli *command.DockerCli, authConfig return jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil) } -func trustDirectory() string { - return filepath.Join(cliconfig.ConfigDir(), "trust") -} - -// certificateDirectory returns the directory containing -// TLS certificates for the given server. An error is -// returned if there was an error parsing the server string. -func certificateDirectory(server string) (string, error) { - u, err := url.Parse(server) - if err != nil { - return "", err - } - - return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil -} - -func trustServer(index *registrytypes.IndexInfo) (string, error) { - if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" { - urlObj, err := url.Parse(s) - if err != nil || urlObj.Scheme != "https" { - return "", fmt.Errorf("valid https URL required for trust server, got %s", s) - } - - return s, nil - } - if index.Official { - return registry.NotaryServer, nil - } - return "https://" + index.Name, nil -} - -type simpleCredentialStore struct { - auth types.AuthConfig -} - -func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) { - return scs.auth.Username, scs.auth.Password -} - -func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string { - return scs.auth.IdentityToken -} - -func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) { -} - -// GetNotaryRepository returns a NotaryRepository which stores all the -// information needed to operate on a notary repository. -// It creates an HTTP transport providing authentication support. -// TODO: move this too -func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, actions ...string) (*client.NotaryRepository, error) { - server, err := trustServer(repoInfo.Index) - if err != nil { - return nil, err - } - - var cfg = tlsconfig.ClientDefault() - cfg.InsecureSkipVerify = !repoInfo.Index.Secure - - // Get certificate base directory - certDir, err := certificateDirectory(server) - if err != nil { - return nil, err - } - logrus.Debugf("reading certificate directory: %s", certDir) - - if err := registry.ReadCertsDirectory(cfg, certDir); err != nil { - return nil, err - } - - base := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - }).Dial, - TLSHandshakeTimeout: 10 * time.Second, - TLSClientConfig: cfg, - DisableKeepAlives: true, - } - - // Skip configuration headers since request is not going to Docker daemon - modifiers := registry.DockerHeaders(command.UserAgent(), http.Header{}) - authTransport := transport.NewTransport(base, modifiers...) - pingClient := &http.Client{ - Transport: authTransport, - Timeout: 5 * time.Second, - } - endpointStr := server + "/v2/" - req, err := http.NewRequest("GET", endpointStr, nil) - if err != nil { - return nil, err - } - - challengeManager := challenge.NewSimpleManager() - - resp, err := pingClient.Do(req) - if err != nil { - // Ignore error on ping to operate in offline mode - logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err) - } else { - defer resp.Body.Close() - - // Add response to the challenge manager to parse out - // authentication header and register authentication method - if err := challengeManager.AddResponse(resp); err != nil { - return nil, err - } - } - - creds := simpleCredentialStore{auth: authConfig} - tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), actions...) - basicHandler := auth.NewBasicHandler(creds) - modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))) - tr := transport.NewTransport(base, modifiers...) - - return client.NewNotaryRepository( - trustDirectory(), - repoInfo.FullName(), - server, - tr, - getPassphraseRetriever(streams), - trustpinning.TrustPinConfig{}) -} - -func getPassphraseRetriever(streams command.Streams) notary.PassRetriever { - aliasMap := map[string]string{ - "root": "root", - "snapshot": "repository", - "targets": "repository", - "default": "repository", - } - baseRetriever := passphrase.PromptRetrieverWithInOut(streams.In(), streams.Out(), aliasMap) - env := map[string]string{ - "root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"), - "snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), - "targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), - "default": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), - } - - return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { - if v := env[alias]; v != "" { - return v, numAttempts > 1, nil - } - // For non-root roles, we can also try the "default" alias if it is specified - if v := env["default"]; v != "" && alias != data.CanonicalRootRole { - return v, numAttempts > 1, nil - } - return baseRetriever(keyName, alias, createNew, numAttempts) - } -} - // 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) @@ -498,20 +324,20 @@ func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference // Resolve the Auth config relevant for this server authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index) - notaryRepo, err := GetNotaryRepository(cli, repoInfo, authConfig, "pull") + notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull") if err != nil { fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err) return nil, err } - t, err := notaryRepo.GetTargetByName(ref.Tag(), releasesRole, data.CanonicalTargetsRole) + t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) if err != nil { return nil, err } // Only list tags in the top level targets role or the releases delegation role - ignore // all other delegation roles - if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole { - return nil, notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.Tag())) + if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { + return nil, trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.Tag())) } r, err := convertTarget(t.Target) if err != nil { @@ -540,34 +366,3 @@ func TagTrusted(ctx context.Context, cli *command.DockerCli, trustedRef referenc return cli.Client().ImageTag(ctx, trustedRef.String(), ref.String()) } - -// notaryError formats an error message received from the notary service -func notaryError(repoName string, err error) error { - switch err.(type) { - case *json.SyntaxError: - logrus.Debugf("Notary syntax error: %s", err) - return fmt.Errorf("Error: no trust data available for remote repository %s. Try running notary server and setting DOCKER_CONTENT_TRUST_SERVER to its HTTPS address?", repoName) - case signed.ErrExpired: - return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err) - case trustmanager.ErrKeyNotFound: - return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err) - case storage.NetworkError: - return fmt.Errorf("Error: error contacting notary server: %v", err) - case storage.ErrMetaNotFound: - return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) - case trustpinning.ErrRootRotationFail, trustpinning.ErrValidationFail, signed.ErrInvalidKeyType: - return fmt.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) - case signed.ErrNoKeys: - return fmt.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) - case signed.ErrLowVersion: - return fmt.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err) - case signed.ErrRoleThreshold: - return fmt.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err) - case client.ErrRepositoryNotExist: - return fmt.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err) - case signed.ErrInsufficientSignatures: - return fmt.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err) - } - - return err -} diff --git a/cli/command/image/trust_test.go b/cli/command/image/trust_test.go index ba6373f2da..78146465e6 100644 --- a/cli/command/image/trust_test.go +++ b/cli/command/image/trust_test.go @@ -5,6 +5,7 @@ import ( "testing" registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/cli/trust" "github.com/docker/docker/registry" ) @@ -19,7 +20,7 @@ func TestENVTrustServer(t *testing.T) { if err := os.Setenv("DOCKER_CONTENT_TRUST_SERVER", "https://notary-test.com:5000"); err != nil { t.Fatal("Failed to set ENV variable") } - output, err := trustServer(indexInfo) + output, err := trust.Server(indexInfo) expectedStr := "https://notary-test.com:5000" if err != nil || output != expectedStr { t.Fatalf("Expected server to be %s, got %s", expectedStr, output) @@ -32,7 +33,7 @@ func TestHTTPENVTrustServer(t *testing.T) { if err := os.Setenv("DOCKER_CONTENT_TRUST_SERVER", "http://notary-test.com:5000"); err != nil { t.Fatal("Failed to set ENV variable") } - _, err := trustServer(indexInfo) + _, err := trust.Server(indexInfo) if err == nil { t.Fatal("Expected error with invalid scheme") } @@ -40,7 +41,7 @@ func TestHTTPENVTrustServer(t *testing.T) { func TestOfficialTrustServer(t *testing.T) { indexInfo := ®istrytypes.IndexInfo{Name: "testserver", Official: true} - output, err := trustServer(indexInfo) + output, err := trust.Server(indexInfo) if err != nil || output != registry.NotaryServer { t.Fatalf("Expected server to be %s, got %s", registry.NotaryServer, output) } @@ -48,7 +49,7 @@ func TestOfficialTrustServer(t *testing.T) { func TestNonOfficialTrustServer(t *testing.T) { indexInfo := ®istrytypes.IndexInfo{Name: "testserver", Official: false} - output, err := trustServer(indexInfo) + output, err := trust.Server(indexInfo) expectedStr := "https://" + indexInfo.Name if err != nil || output != expectedStr { t.Fatalf("Expected server to be %s, got %s", expectedStr, output) diff --git a/cli/trust/trust.go b/cli/trust/trust.go new file mode 100644 index 0000000000..0f3482f2d7 --- /dev/null +++ b/cli/trust/trust.go @@ -0,0 +1,221 @@ +package trust + +import ( + "encoding/json" + "fmt" + "net" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/registry/client/auth" + "github.com/docker/distribution/registry/client/auth/challenge" + "github.com/docker/distribution/registry/client/transport" + "github.com/docker/docker/api/types" + registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/registry" + "github.com/docker/go-connections/tlsconfig" + "github.com/docker/notary" + "github.com/docker/notary/client" + "github.com/docker/notary/passphrase" + "github.com/docker/notary/storage" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/trustpinning" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" +) + +var ( + // ReleasesRole is the role named "releases" + ReleasesRole = path.Join(data.CanonicalTargetsRole, "releases") +) + +func trustDirectory() string { + return filepath.Join(cliconfig.ConfigDir(), "trust") +} + +// certificateDirectory returns the directory containing +// TLS certificates for the given server. An error is +// returned if there was an error parsing the server string. +func certificateDirectory(server string) (string, error) { + u, err := url.Parse(server) + if err != nil { + return "", err + } + + return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil +} + +// Server returns the base URL for the trust server. +func Server(index *registrytypes.IndexInfo) (string, error) { + if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" { + urlObj, err := url.Parse(s) + if err != nil || urlObj.Scheme != "https" { + return "", fmt.Errorf("valid https URL required for trust server, got %s", s) + } + + return s, nil + } + if index.Official { + return registry.NotaryServer, nil + } + return "https://" + index.Name, nil +} + +type simpleCredentialStore struct { + auth types.AuthConfig +} + +func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) { + return scs.auth.Username, scs.auth.Password +} + +func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string { + return scs.auth.IdentityToken +} + +func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) { +} + +// GetNotaryRepository returns a NotaryRepository which stores all the +// information needed to operate on a notary repository. +// It creates an HTTP transport providing authentication support. +func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, actions ...string) (*client.NotaryRepository, error) { + server, err := Server(repoInfo.Index) + if err != nil { + return nil, err + } + + var cfg = tlsconfig.ClientDefault() + cfg.InsecureSkipVerify = !repoInfo.Index.Secure + + // Get certificate base directory + certDir, err := certificateDirectory(server) + if err != nil { + return nil, err + } + logrus.Debugf("reading certificate directory: %s", certDir) + + if err := registry.ReadCertsDirectory(cfg, certDir); err != nil { + return nil, err + } + + base := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: cfg, + DisableKeepAlives: true, + } + + // Skip configuration headers since request is not going to Docker daemon + modifiers := registry.DockerHeaders(command.UserAgent(), http.Header{}) + authTransport := transport.NewTransport(base, modifiers...) + pingClient := &http.Client{ + Transport: authTransport, + Timeout: 5 * time.Second, + } + endpointStr := server + "/v2/" + req, err := http.NewRequest("GET", endpointStr, nil) + if err != nil { + return nil, err + } + + challengeManager := challenge.NewSimpleManager() + + resp, err := pingClient.Do(req) + if err != nil { + // Ignore error on ping to operate in offline mode + logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err) + } else { + defer resp.Body.Close() + + // Add response to the challenge manager to parse out + // authentication header and register authentication method + if err := challengeManager.AddResponse(resp); err != nil { + return nil, err + } + } + + creds := simpleCredentialStore{auth: authConfig} + tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), actions...) + basicHandler := auth.NewBasicHandler(creds) + modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))) + tr := transport.NewTransport(base, modifiers...) + + return client.NewNotaryRepository( + trustDirectory(), + repoInfo.FullName(), + server, + tr, + getPassphraseRetriever(streams), + trustpinning.TrustPinConfig{}) +} + +func getPassphraseRetriever(streams command.Streams) notary.PassRetriever { + aliasMap := map[string]string{ + "root": "root", + "snapshot": "repository", + "targets": "repository", + "default": "repository", + } + baseRetriever := passphrase.PromptRetrieverWithInOut(streams.In(), streams.Out(), aliasMap) + env := map[string]string{ + "root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"), + "snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + "targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + "default": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + } + + return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { + if v := env[alias]; v != "" { + return v, numAttempts > 1, nil + } + // For non-root roles, we can also try the "default" alias if it is specified + if v := env["default"]; v != "" && alias != data.CanonicalRootRole { + return v, numAttempts > 1, nil + } + return baseRetriever(keyName, alias, createNew, numAttempts) + } +} + +// NotaryError formats an error message received from the notary service +func NotaryError(repoName string, err error) error { + switch err.(type) { + case *json.SyntaxError: + logrus.Debugf("Notary syntax error: %s", err) + return fmt.Errorf("Error: no trust data available for remote repository %s. Try running notary server and setting DOCKER_CONTENT_TRUST_SERVER to its HTTPS address?", repoName) + case signed.ErrExpired: + return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err) + case trustmanager.ErrKeyNotFound: + return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err) + case storage.NetworkError: + return fmt.Errorf("Error: error contacting notary server: %v", err) + case storage.ErrMetaNotFound: + return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) + case trustpinning.ErrRootRotationFail, trustpinning.ErrValidationFail, signed.ErrInvalidKeyType: + return fmt.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) + case signed.ErrNoKeys: + return fmt.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) + case signed.ErrLowVersion: + return fmt.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err) + case signed.ErrRoleThreshold: + return fmt.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err) + case client.ErrRepositoryNotExist: + return fmt.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err) + case signed.ErrInsufficientSignatures: + return fmt.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err) + } + + return err +} From 61dc897a30e073bf501cc253002571ce72eeec19 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Mon, 5 Dec 2016 17:02:26 -0800 Subject: [PATCH 3/4] cli: Pin image to digest using content trust Implement notary-based digest lookup in the client when DOCKER_CONTENT_TRUST=1. Signed-off-by: Aaron Lehmann (cherry picked from commit d4d6f8c0d0c6cd0ba6dc96ab7a9ed07e1e766074) --- cli/command/service/create.go | 4 ++ cli/command/service/trust.go | 96 +++++++++++++++++++++++++++++++++++ cli/command/service/update.go | 6 +++ daemon/cluster/cluster.go | 6 ++- 4 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 cli/command/service/trust.go diff --git a/cli/command/service/create.go b/cli/command/service/create.go index a8382835a0..ca2bb089fd 100644 --- a/cli/command/service/create.go +++ b/cli/command/service/create.go @@ -72,6 +72,10 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error { ctx := context.Background() + if err := resolveServiceImageDigest(dockerCli, &service); err != nil { + return err + } + // only send auth if flag was set if opts.registryAuth { // Retrieve encoded auth token from the image reference diff --git a/cli/command/service/trust.go b/cli/command/service/trust.go new file mode 100644 index 0000000000..052d49c32a --- /dev/null +++ b/cli/command/service/trust.go @@ -0,0 +1,96 @@ +package service + +import ( + "encoding/hex" + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/digest" + distreference "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/trust" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "github.com/docker/notary/tuf/data" + "github.com/pkg/errors" + "golang.org/x/net/context" +) + +func resolveServiceImageDigest(dockerCli *command.DockerCli, service *swarm.ServiceSpec) error { + if !command.IsTrusted() { + // Digests are resolved by the daemon when not using content + // trust. + return nil + } + + image := service.TaskTemplate.ContainerSpec.Image + + // We only attempt to resolve the digest if the reference + // could be parsed as a digest reference. Specifying an image ID + // is valid but not resolvable. There is no warning message for + // an image ID because it's valid to use one. + if _, err := digest.ParseDigest(image); err == nil { + return nil + } + + ref, err := reference.ParseNamed(image) + if err != nil { + return fmt.Errorf("Could not parse image reference %s", service.TaskTemplate.ContainerSpec.Image) + } + if _, ok := ref.(reference.Canonical); !ok { + ref = reference.WithDefaultTag(ref) + + taggedRef, ok := ref.(reference.NamedTagged) + if !ok { + // This should never happen because a reference either + // has a digest, or WithDefaultTag would give it a tag. + return errors.New("Failed to resolve image digest using content trust: reference is missing a tag") + } + + resolvedImage, err := trustedResolveDigest(context.Background(), dockerCli, taggedRef) + if err != nil { + return fmt.Errorf("Failed to resolve image digest using content trust: %v", err) + } + logrus.Debugf("resolved image tag to %s using content trust", resolvedImage.String()) + service.TaskTemplate.ContainerSpec.Image = resolvedImage.String() + } + return nil +} + +func trustedResolveDigest(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (distreference.Canonical, error) { + repoInfo, err := registry.ParseRepositoryInfo(ref) + if err != nil { + return nil, err + } + + authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index) + + notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull") + if err != nil { + return nil, errors.Wrap(err, "error establishing connection to trust repository") + } + + t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) + if err != nil { + return nil, trust.NotaryError(repoInfo.FullName(), 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.FullName(), fmt.Errorf("No trust data for %s", ref.String())) + } + + logrus.Debugf("retrieving target for %s role\n", 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)) + + // Using distribution reference package to make sure that adding a + // digest does not erase the tag. When the two reference packages + // are unified, this will no longer be an issue. + return distreference.WithDigest(ref, dgst) +} diff --git a/cli/command/service/update.go b/cli/command/service/update.go index 4bbcf35a8d..514b1bd510 100644 --- a/cli/command/service/update.go +++ b/cli/command/service/update.go @@ -103,6 +103,12 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str return err } + if flags.Changed("image") { + if err := resolveServiceImageDigest(dockerCli, spec); err != nil { + return err + } + } + updatedSecrets, err := getUpdatedSecrets(apiClient, flags, spec.TaskTemplate.ContainerSpec.Secrets) if err != nil { return err diff --git a/daemon/cluster/cluster.go b/daemon/cluster/cluster.go index b2afe15eef..21cd7f63a6 100644 --- a/daemon/cluster/cluster.go +++ b/daemon/cluster/cluster.go @@ -1111,9 +1111,11 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (*apity if err != nil { logrus.Warnf("unable to pin image %s to digest: %s", ctnr.Image, err.Error()) resp.Warnings = append(resp.Warnings, fmt.Sprintf("unable to pin image %s to digest: %s", ctnr.Image, err.Error())) - } else { + } else if ctnr.Image != digestImage { logrus.Debugf("pinning image %s by digest: %s", ctnr.Image, digestImage) ctnr.Image = digestImage + } else { + logrus.Debugf("creating service using supplied digest reference %s", ctnr.Image) } } @@ -1223,6 +1225,8 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ } else if newCtnr.Image != digestImage { logrus.Debugf("pinning image %s by digest: %s", newCtnr.Image, digestImage) newCtnr.Image = digestImage + } else { + logrus.Debugf("updating service using supplied digest reference %s", newCtnr.Image) } } From 8f724a277abc96498dcb6a1b8425abf2b04dbdcc Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 6 Dec 2016 10:57:58 -0800 Subject: [PATCH 4/4] integration-cli: Add integration tests for swarm services + content trust Signed-off-by: Aaron Lehmann (cherry picked from commit 62cd3b39f901d14f984f1176ef0844918c145d21) --- integration-cli/check_test.go | 33 ++++++++++ integration-cli/docker_cli_swarm_test.go | 81 ++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/integration-cli/check_test.go b/integration-cli/check_test.go index 81f458f5ae..7084d6f8af 100644 --- a/integration-cli/check_test.go +++ b/integration-cli/check_test.go @@ -348,3 +348,36 @@ func (s *DockerTrustSuite) TearDownTest(c *check.C) { os.RemoveAll(filepath.Join(cliconfig.ConfigDir(), "trust")) s.ds.TearDownTest(c) } + +func init() { + ds := &DockerSuite{} + check.Suite(&DockerTrustedSwarmSuite{ + trustSuite: DockerTrustSuite{ + ds: ds, + }, + swarmSuite: DockerSwarmSuite{ + ds: ds, + }, + }) +} + +type DockerTrustedSwarmSuite struct { + swarmSuite DockerSwarmSuite + trustSuite DockerTrustSuite + reg *testRegistryV2 + not *testNotary +} + +func (s *DockerTrustedSwarmSuite) SetUpTest(c *check.C) { + s.swarmSuite.SetUpTest(c) + s.trustSuite.SetUpTest(c) +} + +func (s *DockerTrustedSwarmSuite) TearDownTest(c *check.C) { + s.trustSuite.TearDownTest(c) + s.swarmSuite.TearDownTest(c) +} + +func (s *DockerTrustedSwarmSuite) OnTimeout(c *check.C) { + s.swarmSuite.OnTimeout(c) +} diff --git a/integration-cli/docker_cli_swarm_test.go b/integration-cli/docker_cli_swarm_test.go index b0f2de7a0a..376f366782 100644 --- a/integration-cli/docker_cli_swarm_test.go +++ b/integration-cli/docker_cli_swarm_test.go @@ -1085,3 +1085,84 @@ func (s *DockerSwarmSuite) TestSwarmNetworkIPAMOptions(c *check.C) { c.Assert(err, checker.IsNil, check.Commentf(out)) c.Assert(strings.TrimSpace(out), checker.Equals, "map[foo:bar]") } + +func (s *DockerTrustedSwarmSuite) TestTrustedServiceCreate(c *check.C) { + d := s.swarmSuite.AddDaemon(c, true, true) + + // Attempt creating a service from an image that is known to notary. + repoName := s.trustSuite.setupTrustedImage(c, "trusted-pull") + + name := "trusted" + serviceCmd := d.command("-D", "service", "create", "--name", name, repoName, "top") + s.trustSuite.trustedCmd(serviceCmd) + out, _, err := runCommandWithOutput(serviceCmd) + c.Assert(err, checker.IsNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "resolved image tag to", check.Commentf(out)) + + out, err = d.Cmd("service", "inspect", "--pretty", name) + c.Assert(err, checker.IsNil, check.Commentf(out)) + c.Assert(out, checker.Contains, repoName+"@", check.Commentf(out)) + + // Try trusted service create on an untrusted tag. + + repoName = fmt.Sprintf("%v/untrustedservicecreate/createtest:latest", privateRegistryURL) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + dockerCmd(c, "push", repoName) + dockerCmd(c, "rmi", repoName) + + name = "untrusted" + serviceCmd = d.command("service", "create", "--name", name, repoName, "top") + s.trustSuite.trustedCmd(serviceCmd) + out, _, err = runCommandWithOutput(serviceCmd) + + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Error: remote trust data does not exist", check.Commentf(out)) + + out, err = d.Cmd("service", "inspect", "--pretty", name) + c.Assert(err, checker.NotNil, check.Commentf(out)) +} + +func (s *DockerTrustedSwarmSuite) TestTrustedServiceUpdate(c *check.C) { + d := s.swarmSuite.AddDaemon(c, true, true) + + // Attempt creating a service from an image that is known to notary. + repoName := s.trustSuite.setupTrustedImage(c, "trusted-pull") + + name := "myservice" + + // Create a service without content trust + _, err := d.Cmd("service", "create", "--name", name, repoName, "top") + c.Assert(err, checker.IsNil) + + out, err := d.Cmd("service", "inspect", "--pretty", name) + c.Assert(err, checker.IsNil, check.Commentf(out)) + // Daemon won't insert the digest because this is disabled by + // DOCKER_SERVICE_PREFER_OFFLINE_IMAGE. + c.Assert(out, check.Not(checker.Contains), repoName+"@", check.Commentf(out)) + + serviceCmd := d.command("-D", "service", "update", "--image", repoName, name) + s.trustSuite.trustedCmd(serviceCmd) + out, _, err = runCommandWithOutput(serviceCmd) + c.Assert(err, checker.IsNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "resolved image tag to", check.Commentf(out)) + + out, err = d.Cmd("service", "inspect", "--pretty", name) + c.Assert(err, checker.IsNil, check.Commentf(out)) + c.Assert(out, checker.Contains, repoName+"@", check.Commentf(out)) + + // Try trusted service update on an untrusted tag. + + repoName = fmt.Sprintf("%v/untrustedservicecreate/createtest:latest", privateRegistryURL) + // tag the image and upload it to the private registry + dockerCmd(c, "tag", "busybox", repoName) + dockerCmd(c, "push", repoName) + dockerCmd(c, "rmi", repoName) + + serviceCmd = d.command("service", "update", "--image", repoName, name) + s.trustSuite.trustedCmd(serviceCmd) + out, _, err = runCommandWithOutput(serviceCmd) + + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(string(out), checker.Contains, "Error: remote trust data does not exist", check.Commentf(out)) +}