diff --git a/cmd/moby/docker.go b/cmd/moby/docker.go index a83ee766c..cd9fade1b 100644 --- a/cmd/moby/docker.go +++ b/cmd/moby/docker.go @@ -10,7 +10,6 @@ import ( "io" "io/ioutil" "os" - "os/exec" "strings" log "github.com/Sirupsen/logrus" @@ -161,48 +160,22 @@ func dockerRm(container string) error { func dockerPull(image string, trustedPull bool) error { log.Debugf("docker pull: %s", image) - docker, err := exec.LookPath("docker") - if err != nil { - return errors.New("Docker does not seem to be installed") - } - var args = []string{"pull"} if trustedPull { log.Debugf("pulling %s with content trust", image) - args = append(args, "--disable-content-trust=false") + trustedImg, err := TrustedReference(image) + if err != nil { + return fmt.Errorf("Trusted pull for %s failed: %v", image, err) + } + image = trustedImg.String() } - args = append(args, image) - cmd := exec.Command(docker, args...) - - stderrPipe, err := cmd.StderrPipe() + cli, err := dockerClient() if err != nil { + return errors.New("could not initialize Docker API client") + } + + if _, err := cli.ImagePull(context.Background(), image, types.ImagePullOptions{}); err != nil { return err } - - stdoutPipe, err := cmd.StdoutPipe() - if err != nil { - return err - } - - err = cmd.Start() - if err != nil { - return err - } - - _, err = ioutil.ReadAll(stdoutPipe) - if err != nil { - return err - } - - stderr, err := ioutil.ReadAll(stderrPipe) - if err != nil { - return err - } - - err = cmd.Wait() - if err != nil { - return fmt.Errorf("%v: %s", err, stderr) - } - log.Debugf("docker pull: %s...Done", image) return nil } diff --git a/cmd/moby/trust.go b/cmd/moby/trust.go new file mode 100644 index 000000000..7f64ec763 --- /dev/null +++ b/cmd/moby/trust.go @@ -0,0 +1,201 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "encoding/hex" + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "strings" + "time" + + "github.com/docker/distribution/reference" + "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/cli/trust" + notaryClient "github.com/docker/notary/client" + "github.com/docker/notary/trustpinning" + "github.com/docker/notary/tuf/data" + "github.com/opencontainers/go-digest" +) + +// TrustedReference parses an image string, and does a notary lookup to verify and retrieve the signed digest reference +func TrustedReference(image string) (reference.Reference, error) { + ref, err := reference.ParseAnyReference(image) + if err != nil { + return nil, err + } + + // to mimic docker pull: if we have a digest already, it's implicitly trusted + if digestRef, ok := ref.(reference.Digested); ok { + return digestRef, nil + } + // to mimic docker pull: if we have a digest already, it's implicitly trusted + if canonicalRef, ok := ref.(reference.Canonical); ok { + return canonicalRef, nil + } + + namedRef, ok := ref.(reference.Named) + if !ok { + return nil, errors.New("failed to resolve image digest using content trust: reference is not named") + } + taggedRef, ok := namedRef.(reference.NamedTagged) + if !ok { + return nil, errors.New("failed to resolve image digest using content trust: reference is not tagged") + } + + gun := taggedRef.Name() + targetName := taggedRef.Tag() + server, err := getTrustServer(gun) + if err != nil { + return nil, err + } + + rt, err := GetReadOnlyAuthTransport(server, []string{gun}, "", "", "") + if err != nil { + return nil, err + } + + nRepo, err := notaryClient.NewNotaryRepository( + "", + gun, + server, + rt, + nil, + trustpinning.TrustPinConfig{}, + ) + if err != nil { + return nil, err + } + target, err := nRepo.GetTargetByName(targetName, trust.ReleasesRole, data.CanonicalTargetsRole) + if err != nil { + return nil, 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 target.Role != trust.ReleasesRole && target.Role != data.CanonicalTargetsRole { + return nil, errors.New("not signed in valid role") + } + + h, ok := target.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 and digest + return reference.WithDigest(taggedRef, dgst) +} + +func getTrustServer(gun string) (string, error) { + if strings.HasPrefix(gun, "docker.io/") { + return "https://notary.docker.io", nil + } + return "", errors.New("non-hub images not yet supported") +} + +type credentialStore struct { + username string + password string + refreshTokens map[string]string +} + +func (tcs *credentialStore) Basic(url *url.URL) (string, string) { + return tcs.username, tcs.password +} + +// refresh tokens are the long lived tokens that can be used instead of a password +func (tcs *credentialStore) RefreshToken(u *url.URL, service string) string { + return tcs.refreshTokens[service] +} + +func (tcs *credentialStore) SetRefreshToken(u *url.URL, service string, token string) { + if tcs.refreshTokens != nil { + tcs.refreshTokens[service] = token + } +} + +// GetReadOnlyAuthTransport gets the Auth Transport used to communicate with notary +func GetReadOnlyAuthTransport(server string, scopes []string, username, password, rootCAPath string) (http.RoundTripper, error) { + httpsTransport, err := httpsTransport(rootCAPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", fmt.Sprintf("%s/v2/", server), nil) + if err != nil { + return nil, err + } + pingClient := &http.Client{ + Transport: httpsTransport, + Timeout: 5 * time.Second, + } + resp, err := pingClient.Do(req) + if err != nil { + return nil, err + } + challengeManager := challenge.NewSimpleManager() + if err := challengeManager.AddResponse(resp); err != nil { + return nil, err + } + + creds := credentialStore{ + username: username, + password: password, + refreshTokens: make(map[string]string), + } + + var scopeObjs []auth.Scope + for _, scopeName := range scopes { + scopeObjs = append(scopeObjs, auth.RepositoryScope{ + Repository: scopeName, + Actions: []string{"pull"}, + }) + } + + // allow setting multiple scopes so we don't have to reauth + tokenHandler := auth.NewTokenHandlerWithOptions(auth.TokenHandlerOptions{ + Transport: httpsTransport, + Credentials: &creds, + Scopes: scopeObjs, + }) + + authedTransport := transport.NewTransport(httpsTransport, auth.NewAuthorizer(challengeManager, tokenHandler)) + return authedTransport, nil +} + +func httpsTransport(caFile string) (*http.Transport, error) { + tlsConfig := &tls.Config{} + transport := http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: tlsConfig, + } + // Override with the system cert pool if the caFile was empty + if caFile == "" { + systemCertPool, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + transport.TLSClientConfig.RootCAs = systemCertPool + } else { + certPool := x509.NewCertPool() + pems, err := ioutil.ReadFile(caFile) + if err != nil { + return nil, err + } + certPool.AppendCertsFromPEM(pems) + transport.TLSClientConfig.RootCAs = certPool + } + return &transport, nil +}