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