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