github.com/a4a881d4/docker@v1.9.0-rc2/api/client/trust.go (about)

     1  package client
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/hex"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net"
    11  	"net/http"
    12  	"net/url"
    13  	"os"
    14  	"path/filepath"
    15  	"regexp"
    16  	"sort"
    17  	"strconv"
    18  	"time"
    19  
    20  	"github.com/Sirupsen/logrus"
    21  	"github.com/docker/distribution/digest"
    22  	"github.com/docker/distribution/registry/client/auth"
    23  	"github.com/docker/distribution/registry/client/transport"
    24  	"github.com/docker/docker/cliconfig"
    25  	"github.com/docker/docker/pkg/ansiescape"
    26  	"github.com/docker/docker/pkg/ioutils"
    27  	flag "github.com/docker/docker/pkg/mflag"
    28  	"github.com/docker/docker/pkg/tlsconfig"
    29  	"github.com/docker/docker/registry"
    30  	"github.com/docker/notary/client"
    31  	"github.com/docker/notary/pkg/passphrase"
    32  	"github.com/docker/notary/trustmanager"
    33  	"github.com/endophage/gotuf/data"
    34  )
    35  
    36  var untrusted bool
    37  
    38  func addTrustedFlags(fs *flag.FlagSet, verify bool) {
    39  	var trusted bool
    40  	if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" {
    41  		if t, err := strconv.ParseBool(e); t || err != nil {
    42  			// treat any other value as true
    43  			trusted = true
    44  		}
    45  	}
    46  	message := "Skip image signing"
    47  	if verify {
    48  		message = "Skip image verification"
    49  	}
    50  	fs.BoolVar(&untrusted, []string{"-disable-content-trust"}, !trusted, message)
    51  }
    52  
    53  func isTrusted() bool {
    54  	return !untrusted
    55  }
    56  
    57  var targetRegexp = regexp.MustCompile(`([\S]+): digest: ([\S]+) size: ([\d]+)`)
    58  
    59  type target struct {
    60  	reference registry.Reference
    61  	digest    digest.Digest
    62  	size      int64
    63  }
    64  
    65  func (cli *DockerCli) trustDirectory() string {
    66  	return filepath.Join(cliconfig.ConfigDir(), "trust")
    67  }
    68  
    69  // certificateDirectory returns the directory containing
    70  // TLS certificates for the given server. An error is
    71  // returned if there was an error parsing the server string.
    72  func (cli *DockerCli) certificateDirectory(server string) (string, error) {
    73  	u, err := url.Parse(server)
    74  	if err != nil {
    75  		return "", err
    76  	}
    77  
    78  	return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil
    79  }
    80  
    81  func trustServer(index *registry.IndexInfo) (string, error) {
    82  	if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" {
    83  		urlObj, err := url.Parse(s)
    84  		if err != nil || urlObj.Scheme != "https" {
    85  			return "", fmt.Errorf("valid https URL required for trust server, got %s", s)
    86  		}
    87  
    88  		return s, nil
    89  	}
    90  	if index.Official {
    91  		return registry.NotaryServer, nil
    92  	}
    93  	return "https://" + index.Name, nil
    94  }
    95  
    96  type simpleCredentialStore struct {
    97  	auth cliconfig.AuthConfig
    98  }
    99  
   100  func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) {
   101  	return scs.auth.Username, scs.auth.Password
   102  }
   103  
   104  func (cli *DockerCli) getNotaryRepository(repoInfo *registry.RepositoryInfo, authConfig cliconfig.AuthConfig) (*client.NotaryRepository, error) {
   105  	server, err := trustServer(repoInfo.Index)
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  
   110  	var cfg = tlsconfig.ClientDefault
   111  	cfg.InsecureSkipVerify = !repoInfo.Index.Secure
   112  
   113  	// Get certificate base directory
   114  	certDir, err := cli.certificateDirectory(server)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  	logrus.Debugf("reading certificate directory: %s", certDir)
   119  
   120  	if err := registry.ReadCertsDirectory(&cfg, certDir); err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	base := &http.Transport{
   125  		Proxy: http.ProxyFromEnvironment,
   126  		Dial: (&net.Dialer{
   127  			Timeout:   30 * time.Second,
   128  			KeepAlive: 30 * time.Second,
   129  			DualStack: true,
   130  		}).Dial,
   131  		TLSHandshakeTimeout: 10 * time.Second,
   132  		TLSClientConfig:     &cfg,
   133  		DisableKeepAlives:   true,
   134  	}
   135  
   136  	// Skip configuration headers since request is not going to Docker daemon
   137  	modifiers := registry.DockerHeaders(http.Header{})
   138  	authTransport := transport.NewTransport(base, modifiers...)
   139  	pingClient := &http.Client{
   140  		Transport: authTransport,
   141  		Timeout:   5 * time.Second,
   142  	}
   143  	endpointStr := server + "/v2/"
   144  	req, err := http.NewRequest("GET", endpointStr, nil)
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  
   149  	challengeManager := auth.NewSimpleChallengeManager()
   150  
   151  	resp, err := pingClient.Do(req)
   152  	if err != nil {
   153  		// Ignore error on ping to operate in offline mode
   154  		logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err)
   155  	} else {
   156  		defer resp.Body.Close()
   157  
   158  		// Add response to the challenge manager to parse out
   159  		// authentication header and register authentication method
   160  		if err := challengeManager.AddResponse(resp); err != nil {
   161  			return nil, err
   162  		}
   163  	}
   164  
   165  	creds := simpleCredentialStore{auth: authConfig}
   166  	tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.CanonicalName, "push", "pull")
   167  	basicHandler := auth.NewBasicHandler(creds)
   168  	modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)))
   169  	tr := transport.NewTransport(base, modifiers...)
   170  
   171  	return client.NewNotaryRepository(cli.trustDirectory(), repoInfo.CanonicalName, server, tr, cli.getPassphraseRetriever())
   172  }
   173  
   174  func convertTarget(t client.Target) (target, error) {
   175  	h, ok := t.Hashes["sha256"]
   176  	if !ok {
   177  		return target{}, errors.New("no valid hash, expecting sha256")
   178  	}
   179  	return target{
   180  		reference: registry.ParseReference(t.Name),
   181  		digest:    digest.NewDigestFromHex("sha256", hex.EncodeToString(h)),
   182  		size:      t.Length,
   183  	}, nil
   184  }
   185  
   186  func (cli *DockerCli) getPassphraseRetriever() passphrase.Retriever {
   187  	aliasMap := map[string]string{
   188  		"root":     "root",
   189  		"snapshot": "repository",
   190  		"targets":  "repository",
   191  	}
   192  	baseRetriever := passphrase.PromptRetrieverWithInOut(cli.in, cli.out, aliasMap)
   193  	env := map[string]string{
   194  		"root":     os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"),
   195  		"snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
   196  		"targets":  os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
   197  	}
   198  
   199  	// Backwards compatibility with old env names. We should remove this in 1.10
   200  	if env["root"] == "" {
   201  		if passphrase := os.Getenv("DOCKER_CONTENT_TRUST_OFFLINE_PASSPHRASE"); passphrase != "" {
   202  			env["root"] = passphrase
   203  			fmt.Fprintf(cli.err, "[DEPRECATED] The environment variable DOCKER_CONTENT_TRUST_OFFLINE_PASSPHRASE has been deprecated and will be removed in v1.10. Please use DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE\n")
   204  		}
   205  	}
   206  	if env["snapshot"] == "" || env["targets"] == "" {
   207  		if passphrase := os.Getenv("DOCKER_CONTENT_TRUST_TAGGING_PASSPHRASE"); passphrase != "" {
   208  			env["snapshot"] = passphrase
   209  			env["targets"] = passphrase
   210  			fmt.Fprintf(cli.err, "[DEPRECATED] The environment variable DOCKER_CONTENT_TRUST_TAGGING_PASSPHRASE has been deprecated and will be removed in v1.10. Please use DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE\n")
   211  		}
   212  	}
   213  
   214  	return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) {
   215  		if v := env[alias]; v != "" {
   216  			return v, numAttempts > 1, nil
   217  		}
   218  		return baseRetriever(keyName, alias, createNew, numAttempts)
   219  	}
   220  }
   221  
   222  func (cli *DockerCli) trustedReference(repo string, ref registry.Reference) (registry.Reference, error) {
   223  	repoInfo, err := registry.ParseRepositoryInfo(repo)
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  
   228  	// Resolve the Auth config relevant for this server
   229  	authConfig := registry.ResolveAuthConfig(cli.configFile, repoInfo.Index)
   230  
   231  	notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig)
   232  	if err != nil {
   233  		fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err)
   234  		return nil, err
   235  	}
   236  
   237  	t, err := notaryRepo.GetTargetByName(ref.String())
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  	r, err := convertTarget(*t)
   242  	if err != nil {
   243  		return nil, err
   244  
   245  	}
   246  
   247  	return registry.DigestReference(r.digest), nil
   248  }
   249  
   250  func (cli *DockerCli) tagTrusted(repoInfo *registry.RepositoryInfo, trustedRef, ref registry.Reference) error {
   251  	fullName := trustedRef.ImageName(repoInfo.LocalName)
   252  	fmt.Fprintf(cli.out, "Tagging %s as %s\n", fullName, ref.ImageName(repoInfo.LocalName))
   253  	tv := url.Values{}
   254  	tv.Set("repo", repoInfo.LocalName)
   255  	tv.Set("tag", ref.String())
   256  	tv.Set("force", "1")
   257  
   258  	if _, _, err := readBody(cli.call("POST", "/images/"+fullName+"/tag?"+tv.Encode(), nil, nil)); err != nil {
   259  		return err
   260  	}
   261  
   262  	return nil
   263  }
   264  
   265  func notaryError(err error) error {
   266  	switch err.(type) {
   267  	case *json.SyntaxError:
   268  		logrus.Debugf("Notary syntax error: %s", err)
   269  		return errors.New("no trust data available for remote repository")
   270  	case client.ErrExpired:
   271  		return fmt.Errorf("remote repository out-of-date: %v", err)
   272  	case trustmanager.ErrKeyNotFound:
   273  		return fmt.Errorf("signing keys not found: %v", err)
   274  	case *net.OpError:
   275  		return fmt.Errorf("error contacting notary server: %v", err)
   276  	}
   277  
   278  	return err
   279  }
   280  
   281  func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig cliconfig.AuthConfig) error {
   282  	var (
   283  		v    = url.Values{}
   284  		refs = []target{}
   285  	)
   286  
   287  	notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig)
   288  	if err != nil {
   289  		fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err)
   290  		return err
   291  	}
   292  
   293  	if ref.String() == "" {
   294  		// List all targets
   295  		targets, err := notaryRepo.ListTargets()
   296  		if err != nil {
   297  			return notaryError(err)
   298  		}
   299  		for _, tgt := range targets {
   300  			t, err := convertTarget(*tgt)
   301  			if err != nil {
   302  				fmt.Fprintf(cli.out, "Skipping target for %q\n", repoInfo.LocalName)
   303  				continue
   304  			}
   305  			refs = append(refs, t)
   306  		}
   307  	} else {
   308  		t, err := notaryRepo.GetTargetByName(ref.String())
   309  		if err != nil {
   310  			return notaryError(err)
   311  		}
   312  		r, err := convertTarget(*t)
   313  		if err != nil {
   314  			return err
   315  
   316  		}
   317  		refs = append(refs, r)
   318  	}
   319  
   320  	v.Set("fromImage", repoInfo.LocalName)
   321  	for i, r := range refs {
   322  		displayTag := r.reference.String()
   323  		if displayTag != "" {
   324  			displayTag = ":" + displayTag
   325  		}
   326  		fmt.Fprintf(cli.out, "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.LocalName, displayTag, r.digest)
   327  		v.Set("tag", r.digest.String())
   328  
   329  		_, _, err = cli.clientRequestAttemptLogin("POST", "/images/create?"+v.Encode(), nil, cli.out, repoInfo.Index, "pull")
   330  		if err != nil {
   331  			return err
   332  		}
   333  
   334  		// If reference is not trusted, tag by trusted reference
   335  		if !r.reference.HasDigest() {
   336  			if err := cli.tagTrusted(repoInfo, registry.DigestReference(r.digest), r.reference); err != nil {
   337  				return err
   338  
   339  			}
   340  		}
   341  	}
   342  	return nil
   343  }
   344  
   345  func selectKey(keys map[string]string) string {
   346  	if len(keys) == 0 {
   347  		return ""
   348  	}
   349  
   350  	keyIDs := []string{}
   351  	for k := range keys {
   352  		keyIDs = append(keyIDs, k)
   353  	}
   354  
   355  	// TODO(dmcgowan): let user choose if multiple keys, now pick consistently
   356  	sort.Strings(keyIDs)
   357  
   358  	return keyIDs[0]
   359  }
   360  
   361  func targetStream(in io.Writer) (io.WriteCloser, <-chan []target) {
   362  	r, w := io.Pipe()
   363  	out := io.MultiWriter(in, w)
   364  	targetChan := make(chan []target)
   365  
   366  	go func() {
   367  		targets := []target{}
   368  		scanner := bufio.NewScanner(r)
   369  		scanner.Split(ansiescape.ScanANSILines)
   370  		for scanner.Scan() {
   371  			line := scanner.Bytes()
   372  			if matches := targetRegexp.FindSubmatch(line); len(matches) == 4 {
   373  				dgst, err := digest.ParseDigest(string(matches[2]))
   374  				if err != nil {
   375  					// Line does match what is expected, continue looking for valid lines
   376  					logrus.Debugf("Bad digest value %q in matched line, ignoring\n", string(matches[2]))
   377  					continue
   378  				}
   379  				s, err := strconv.ParseInt(string(matches[3]), 10, 64)
   380  				if err != nil {
   381  					// Line does match what is expected, continue looking for valid lines
   382  					logrus.Debugf("Bad size value %q in matched line, ignoring\n", string(matches[3]))
   383  					continue
   384  				}
   385  
   386  				targets = append(targets, target{
   387  					reference: registry.ParseReference(string(matches[1])),
   388  					digest:    dgst,
   389  					size:      s,
   390  				})
   391  			}
   392  		}
   393  		targetChan <- targets
   394  	}()
   395  
   396  	return ioutils.NewWriteCloserWrapper(out, w.Close), targetChan
   397  }
   398  
   399  func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, tag string, authConfig cliconfig.AuthConfig) error {
   400  	streamOut, targetChan := targetStream(cli.out)
   401  
   402  	v := url.Values{}
   403  	v.Set("tag", tag)
   404  
   405  	_, _, err := cli.clientRequestAttemptLogin("POST", "/images/"+repoInfo.LocalName+"/push?"+v.Encode(), nil, streamOut, repoInfo.Index, "push")
   406  	// Close stream channel to finish target parsing
   407  	if err := streamOut.Close(); err != nil {
   408  		return err
   409  	}
   410  	// Check error from request
   411  	if err != nil {
   412  		return err
   413  	}
   414  
   415  	// Get target results
   416  	targets := <-targetChan
   417  
   418  	if tag == "" {
   419  		fmt.Fprintf(cli.out, "No tag specified, skipping trust metadata push\n")
   420  		return nil
   421  	}
   422  	if len(targets) == 0 {
   423  		fmt.Fprintf(cli.out, "No targets found, skipping trust metadata push\n")
   424  		return nil
   425  	}
   426  
   427  	fmt.Fprintf(cli.out, "Signing and pushing trust metadata\n")
   428  
   429  	repo, err := cli.getNotaryRepository(repoInfo, authConfig)
   430  	if err != nil {
   431  		fmt.Fprintf(cli.out, "Error establishing connection to notary repository: %s\n", err)
   432  		return err
   433  	}
   434  
   435  	for _, target := range targets {
   436  		h, err := hex.DecodeString(target.digest.Hex())
   437  		if err != nil {
   438  			return err
   439  		}
   440  		t := &client.Target{
   441  			Name: target.reference.String(),
   442  			Hashes: data.Hashes{
   443  				string(target.digest.Algorithm()): h,
   444  			},
   445  			Length: int64(target.size),
   446  		}
   447  		if err := repo.AddTarget(t); err != nil {
   448  			return err
   449  		}
   450  	}
   451  
   452  	err = repo.Publish()
   453  	if _, ok := err.(*client.ErrRepoNotInitialized); !ok {
   454  		return notaryError(err)
   455  	}
   456  
   457  	ks := repo.KeyStoreManager
   458  	keys := ks.RootKeyStore().ListKeys()
   459  
   460  	rootKey := selectKey(keys)
   461  	if rootKey == "" {
   462  		rootKey, err = ks.GenRootKey("ecdsa")
   463  		if err != nil {
   464  			return err
   465  		}
   466  	}
   467  
   468  	cryptoService, err := ks.GetRootCryptoService(rootKey)
   469  	if err != nil {
   470  		return err
   471  	}
   472  
   473  	if err := repo.Initialize(cryptoService); err != nil {
   474  		return notaryError(err)
   475  	}
   476  	fmt.Fprintf(cli.out, "Finished initializing %q\n", repoInfo.CanonicalName)
   477  
   478  	return notaryError(repo.Publish())
   479  }