Browse code

Use notary library for trusted image fetch and signing

Add a trusted flag to force the cli to resolve a tag into a digest via the notary trust library and pull by digest.
On push the flag the trust flag will indicate the digest and size of a manifest should be signed and push to a notary server.
If a tag is given, the cli will resolve the tag into a digest and pull by digest.
After pulling, if a tag is given the cli makes a request to tag the image.

Use certificate directory for notary requests

Read certificates using same logic used by daemon for registry requests.

Catch JSON syntax errors from Notary client

When an uncaught error occurs in Notary it may show up in Docker as a JSON syntax error, causing a confusing error message to the user.
Provide a generic error when a JSON syntax error occurs.

Catch expiration errors and wrap in additional context.

Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)

Derek McGowan authored on 2015/07/16 05:42:45
Showing 15 changed files
... ...
@@ -15,7 +15,6 @@ import (
15 15
 	"github.com/docker/docker/pkg/parsers"
16 16
 	"github.com/docker/docker/registry"
17 17
 	"github.com/docker/docker/runconfig"
18
-	"github.com/docker/docker/utils"
19 18
 )
20 19
 
21 20
 func (cli *DockerCli) pullImage(image string) error {
... ...
@@ -95,20 +94,42 @@ func (cli *DockerCli) createContainer(config *runconfig.Config, hostConfig *runc
95 95
 		defer containerIDFile.Close()
96 96
 	}
97 97
 
98
+	repo, tag := parsers.ParseRepositoryTag(config.Image)
99
+	if tag == "" {
100
+		tag = tags.DEFAULTTAG
101
+	}
102
+
103
+	ref := registry.ParseReference(tag)
104
+	var trustedRef registry.Reference
105
+
106
+	if isTrusted() && !ref.HasDigest() {
107
+		var err error
108
+		trustedRef, err = cli.trustedReference(repo, ref)
109
+		if err != nil {
110
+			return nil, err
111
+		}
112
+		config.Image = trustedRef.ImageName(repo)
113
+	}
114
+
98 115
 	//create the container
99 116
 	serverResp, err := cli.call("POST", "/containers/create?"+containerValues.Encode(), mergedConfig, nil)
100 117
 	//if image not found try to pull it
101 118
 	if serverResp.statusCode == 404 && strings.Contains(err.Error(), config.Image) {
102
-		repo, tag := parsers.ParseRepositoryTag(config.Image)
103
-		if tag == "" {
104
-			tag = tags.DEFAULTTAG
105
-		}
106
-		fmt.Fprintf(cli.err, "Unable to find image '%s' locally\n", utils.ImageReference(repo, tag))
119
+		fmt.Fprintf(cli.err, "Unable to find image '%s' locally\n", ref.ImageName(repo))
107 120
 
108 121
 		// we don't want to write to stdout anything apart from container.ID
109 122
 		if err = cli.pullImageCustomOut(config.Image, cli.err); err != nil {
110 123
 			return nil, err
111 124
 		}
125
+		if trustedRef != nil && !ref.HasDigest() {
126
+			repoInfo, err := registry.ParseRepositoryInfo(repo)
127
+			if err != nil {
128
+				return nil, err
129
+			}
130
+			if err := cli.tagTrusted(repoInfo, trustedRef, ref); err != nil {
131
+				return nil, err
132
+			}
133
+		}
112 134
 		// Retry
113 135
 		if serverResp, err = cli.call("POST", "/containers/create?"+containerValues.Encode(), mergedConfig, nil); err != nil {
114 136
 			return nil, err
... ...
@@ -139,6 +160,7 @@ func (cli *DockerCli) createContainer(config *runconfig.Config, hostConfig *runc
139 139
 // Usage: docker create [OPTIONS] IMAGE [COMMAND] [ARG...]
140 140
 func (cli *DockerCli) CmdCreate(args ...string) error {
141 141
 	cmd := Cli.Subcmd("create", []string{"IMAGE [COMMAND] [ARG...]"}, "Create a new container", true)
142
+	addTrustedFlags(cmd, true)
142 143
 
143 144
 	// These are flags not stored in Config/HostConfig
144 145
 	var (
... ...
@@ -9,7 +9,6 @@ import (
9 9
 	flag "github.com/docker/docker/pkg/mflag"
10 10
 	"github.com/docker/docker/pkg/parsers"
11 11
 	"github.com/docker/docker/registry"
12
-	"github.com/docker/docker/utils"
13 12
 )
14 13
 
15 14
 // CmdPull pulls an image or a repository from the registry.
... ...
@@ -18,24 +17,21 @@ import (
18 18
 func (cli *DockerCli) CmdPull(args ...string) error {
19 19
 	cmd := Cli.Subcmd("pull", []string{"NAME[:TAG|@DIGEST]"}, "Pull an image or a repository from a registry", true)
20 20
 	allTags := cmd.Bool([]string{"a", "-all-tags"}, false, "Download all tagged images in the repository")
21
+	addTrustedFlags(cmd, true)
21 22
 	cmd.Require(flag.Exact, 1)
22 23
 
23 24
 	cmd.ParseFlags(args, true)
25
+	remote := cmd.Arg(0)
24 26
 
25
-	var (
26
-		v         = url.Values{}
27
-		remote    = cmd.Arg(0)
28
-		newRemote = remote
29
-	)
30 27
 	taglessRemote, tag := parsers.ParseRepositoryTag(remote)
31 28
 	if tag == "" && !*allTags {
32
-		newRemote = utils.ImageReference(taglessRemote, tags.DEFAULTTAG)
33
-	}
34
-	if tag != "" && *allTags {
29
+		tag = tags.DEFAULTTAG
30
+		fmt.Fprintf(cli.out, "Using default tag: %s\n", tag)
31
+	} else if tag != "" && *allTags {
35 32
 		return fmt.Errorf("tag can't be used with --all-tags/-a")
36 33
 	}
37 34
 
38
-	v.Set("fromImage", newRemote)
35
+	ref := registry.ParseReference(tag)
39 36
 
40 37
 	// Resolve the Repository name from fqn to RepositoryInfo
41 38
 	repoInfo, err := registry.ParseRepositoryInfo(taglessRemote)
... ...
@@ -43,6 +39,15 @@ func (cli *DockerCli) CmdPull(args ...string) error {
43 43
 		return err
44 44
 	}
45 45
 
46
+	if isTrusted() && !ref.HasDigest() {
47
+		// Check if tag is digest
48
+		authConfig := registry.ResolveAuthConfig(cli.configFile, repoInfo.Index)
49
+		return cli.trustedPull(repoInfo, ref, authConfig)
50
+	}
51
+
52
+	v := url.Values{}
53
+	v.Set("fromImage", ref.ImageName(taglessRemote))
54
+
46 55
 	_, _, err = cli.clientRequestAttemptLogin("POST", "/images/create?"+v.Encode(), nil, cli.out, repoInfo.Index, "pull")
47 56
 	return err
48 57
 }
... ...
@@ -15,6 +15,7 @@ import (
15 15
 // Usage: docker push NAME[:TAG]
16 16
 func (cli *DockerCli) CmdPush(args ...string) error {
17 17
 	cmd := Cli.Subcmd("push", []string{"NAME[:TAG]"}, "Push an image or a repository to a registry", true)
18
+	addTrustedFlags(cmd, false)
18 19
 	cmd.Require(flag.Exact, 1)
19 20
 
20 21
 	cmd.ParseFlags(args, true)
... ...
@@ -40,6 +41,10 @@ func (cli *DockerCli) CmdPush(args ...string) error {
40 40
 		return fmt.Errorf("You cannot push a \"root\" repository. Please rename your repository to <user>/<repo> (ex: %s/%s)", username, repoInfo.LocalName)
41 41
 	}
42 42
 
43
+	if isTrusted() {
44
+		return cli.trustedPush(repoInfo, tag, authConfig)
45
+	}
46
+
43 47
 	v := url.Values{}
44 48
 	v.Set("tag", tag)
45 49
 
... ...
@@ -41,6 +41,7 @@ func (cid *cidFile) Write(id string) error {
41 41
 // Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
42 42
 func (cli *DockerCli) CmdRun(args ...string) error {
43 43
 	cmd := Cli.Subcmd("run", []string{"IMAGE [COMMAND] [ARG...]"}, "Run a command in a new container", true)
44
+	addTrustedFlags(cmd, true)
44 45
 
45 46
 	// These are flags not stored in Config/HostConfig
46 47
 	var (
47 48
new file mode 100644
... ...
@@ -0,0 +1,435 @@
0
+package client
1
+
2
+import (
3
+	"bufio"
4
+	"encoding/hex"
5
+	"encoding/json"
6
+	"errors"
7
+	"fmt"
8
+	"io"
9
+	"net"
10
+	"net/http"
11
+	"net/url"
12
+	"os"
13
+	"path/filepath"
14
+	"regexp"
15
+	"strconv"
16
+	"strings"
17
+	"time"
18
+
19
+	"github.com/Sirupsen/logrus"
20
+	"github.com/docker/distribution/digest"
21
+	"github.com/docker/distribution/registry/client/auth"
22
+	"github.com/docker/distribution/registry/client/transport"
23
+	"github.com/docker/docker/cliconfig"
24
+	"github.com/docker/docker/pkg/ansiescape"
25
+	"github.com/docker/docker/pkg/ioutils"
26
+	flag "github.com/docker/docker/pkg/mflag"
27
+	"github.com/docker/docker/pkg/tlsconfig"
28
+	"github.com/docker/docker/registry"
29
+	"github.com/docker/notary/client"
30
+	"github.com/docker/notary/pkg/passphrase"
31
+	"github.com/docker/notary/trustmanager"
32
+	"github.com/endophage/gotuf/data"
33
+)
34
+
35
+var untrusted bool
36
+
37
+func addTrustedFlags(fs *flag.FlagSet, verify bool) {
38
+	var trusted bool
39
+	if e := os.Getenv("DOCKER_TRUST"); e != "" {
40
+		if t, err := strconv.ParseBool(e); t || err != nil {
41
+			// treat any other value as true
42
+			trusted = true
43
+		}
44
+	}
45
+	message := "Skip image signing"
46
+	if verify {
47
+		message = "Skip image verification"
48
+	}
49
+	fs.BoolVar(&untrusted, []string{"-untrusted"}, !trusted, message)
50
+}
51
+
52
+func isTrusted() bool {
53
+	return !untrusted
54
+}
55
+
56
+var targetRegexp = regexp.MustCompile(`([\S]+): digest: ([\S]+) size: ([\d]+)`)
57
+
58
+type target struct {
59
+	reference registry.Reference
60
+	digest    digest.Digest
61
+	size      int64
62
+}
63
+
64
+func (cli *DockerCli) trustDirectory() string {
65
+	return filepath.Join(cliconfig.ConfigDir(), "trust")
66
+}
67
+
68
+// certificateDirectory returns the directory containing
69
+// TLS certificates for the given server. An error is
70
+// returned if there was an error parsing the server string.
71
+func (cli *DockerCli) certificateDirectory(server string) (string, error) {
72
+	u, err := url.Parse(server)
73
+	if err != nil {
74
+		return "", err
75
+	}
76
+
77
+	return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil
78
+}
79
+
80
+func trustServer(index *registry.IndexInfo) string {
81
+	if s := os.Getenv("DOCKER_TRUST_SERVER"); s != "" {
82
+		if !strings.HasPrefix(s, "https://") {
83
+			return "https://" + s
84
+		}
85
+		return s
86
+	}
87
+	if index.Official {
88
+		return registry.NotaryServer
89
+	}
90
+	return "https://" + index.Name
91
+}
92
+
93
+type simpleCredentialStore struct {
94
+	auth cliconfig.AuthConfig
95
+}
96
+
97
+func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) {
98
+	return scs.auth.Username, scs.auth.Password
99
+}
100
+
101
+func (cli *DockerCli) getNotaryRepository(repoInfo *registry.RepositoryInfo, authConfig cliconfig.AuthConfig) (*client.NotaryRepository, error) {
102
+	server := trustServer(repoInfo.Index)
103
+	if !strings.HasPrefix(server, "https://") {
104
+		return nil, errors.New("unsupported scheme: https required for trust server")
105
+	}
106
+
107
+	var cfg = tlsconfig.ClientDefault
108
+	cfg.InsecureSkipVerify = !repoInfo.Index.Secure
109
+
110
+	// Get certificate base directory
111
+	certDir, err := cli.certificateDirectory(server)
112
+	if err != nil {
113
+		return nil, err
114
+	}
115
+	logrus.Debugf("reading certificate directory: %s", certDir)
116
+
117
+	if err := registry.ReadCertsDirectory(&cfg, certDir); err != nil {
118
+		return nil, err
119
+	}
120
+
121
+	base := &http.Transport{
122
+		Proxy: http.ProxyFromEnvironment,
123
+		Dial: (&net.Dialer{
124
+			Timeout:   30 * time.Second,
125
+			KeepAlive: 30 * time.Second,
126
+			DualStack: true,
127
+		}).Dial,
128
+		TLSHandshakeTimeout: 10 * time.Second,
129
+		TLSClientConfig:     &cfg,
130
+		DisableKeepAlives:   true,
131
+	}
132
+
133
+	// Skip configuration headers since request is not going to Docker daemon
134
+	modifiers := registry.DockerHeaders(http.Header{})
135
+	authTransport := transport.NewTransport(base, modifiers...)
136
+	pingClient := &http.Client{
137
+		Transport: authTransport,
138
+		Timeout:   5 * time.Second,
139
+	}
140
+	endpointStr := server + "/v2/"
141
+	req, err := http.NewRequest("GET", endpointStr, nil)
142
+	if err != nil {
143
+		return nil, err
144
+	}
145
+	resp, err := pingClient.Do(req)
146
+	if err != nil {
147
+		return nil, err
148
+	}
149
+	defer resp.Body.Close()
150
+
151
+	challengeManager := auth.NewSimpleChallengeManager()
152
+	if err := challengeManager.AddResponse(resp); err != nil {
153
+		return nil, err
154
+	}
155
+
156
+	creds := simpleCredentialStore{auth: authConfig}
157
+	tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.CanonicalName, "push", "pull")
158
+	basicHandler := auth.NewBasicHandler(creds)
159
+	modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)))
160
+	tr := transport.NewTransport(base, modifiers...)
161
+
162
+	return client.NewNotaryRepository(cli.trustDirectory(), repoInfo.CanonicalName, server, tr, cli.getPassphraseRetriever())
163
+}
164
+
165
+func convertTarget(t client.Target) (target, error) {
166
+	h, ok := t.Hashes["sha256"]
167
+	if !ok {
168
+		return target{}, errors.New("no valid hash, expecting sha256")
169
+	}
170
+	return target{
171
+		reference: registry.ParseReference(t.Name),
172
+		digest:    digest.NewDigestFromHex("sha256", hex.EncodeToString(h)),
173
+		size:      t.Length,
174
+	}, nil
175
+}
176
+
177
+func (cli *DockerCli) getPassphraseRetriever() passphrase.Retriever {
178
+	baseRetriever := passphrase.PromptRetrieverWithInOut(cli.in, cli.out)
179
+	env := map[string]string{
180
+		"root":     os.Getenv("DOCKER_TRUST_ROOT_PASSPHRASE"),
181
+		"targets":  os.Getenv("DOCKER_TRUST_TARGET_PASSPHRASE"),
182
+		"snapshot": os.Getenv("DOCKER_TRUST_SNAPSHOT_PASSPHRASE"),
183
+	}
184
+	return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) {
185
+		if v := env[alias]; v != "" {
186
+			return v, numAttempts > 1, nil
187
+		}
188
+		return baseRetriever(keyName, alias, createNew, numAttempts)
189
+	}
190
+}
191
+
192
+func (cli *DockerCli) trustedReference(repo string, ref registry.Reference) (registry.Reference, error) {
193
+	repoInfo, err := registry.ParseRepositoryInfo(repo)
194
+	if err != nil {
195
+		return nil, err
196
+	}
197
+
198
+	// Resolve the Auth config relevant for this server
199
+	authConfig := registry.ResolveAuthConfig(cli.configFile, repoInfo.Index)
200
+
201
+	notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig)
202
+	if err != nil {
203
+		fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err)
204
+		return nil, err
205
+	}
206
+
207
+	t, err := notaryRepo.GetTargetByName(ref.String())
208
+	if err != nil {
209
+		return nil, err
210
+	}
211
+	r, err := convertTarget(*t)
212
+	if err != nil {
213
+		return nil, err
214
+
215
+	}
216
+
217
+	return registry.DigestReference(r.digest), nil
218
+}
219
+
220
+func (cli *DockerCli) tagTrusted(repoInfo *registry.RepositoryInfo, trustedRef, ref registry.Reference) error {
221
+	fullName := trustedRef.ImageName(repoInfo.LocalName)
222
+	fmt.Fprintf(cli.out, "Tagging %s as %s\n", fullName, ref.ImageName(repoInfo.LocalName))
223
+	tv := url.Values{}
224
+	tv.Set("repo", repoInfo.LocalName)
225
+	tv.Set("tag", ref.String())
226
+	tv.Set("force", "1")
227
+
228
+	if _, _, err := readBody(cli.call("POST", "/images/"+fullName+"/tag?"+tv.Encode(), nil, nil)); err != nil {
229
+		return err
230
+	}
231
+
232
+	return nil
233
+}
234
+
235
+func notaryError(err error) error {
236
+	switch err.(type) {
237
+	case *json.SyntaxError:
238
+		logrus.Debugf("Notary syntax error: %s", err)
239
+		return errors.New("no trust data available for remote repository")
240
+	case client.ErrExpired:
241
+		return fmt.Errorf("remote repository out-of-date: %v", err)
242
+	case trustmanager.ErrKeyNotFound:
243
+		return fmt.Errorf("signing keys not found: %v", err)
244
+	}
245
+
246
+	return err
247
+}
248
+
249
+func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig cliconfig.AuthConfig) error {
250
+	var (
251
+		v    = url.Values{}
252
+		refs = []target{}
253
+	)
254
+
255
+	notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig)
256
+	if err != nil {
257
+		fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err)
258
+		return err
259
+	}
260
+
261
+	if ref.String() == "" {
262
+		// List all targets
263
+		targets, err := notaryRepo.ListTargets()
264
+		if err != nil {
265
+			return notaryError(err)
266
+		}
267
+		for _, tgt := range targets {
268
+			t, err := convertTarget(*tgt)
269
+			if err != nil {
270
+				fmt.Fprintf(cli.out, "Skipping target for %q\n", repoInfo.LocalName)
271
+				continue
272
+			}
273
+			refs = append(refs, t)
274
+		}
275
+	} else {
276
+		t, err := notaryRepo.GetTargetByName(ref.String())
277
+		if err != nil {
278
+			return notaryError(err)
279
+		}
280
+		r, err := convertTarget(*t)
281
+		if err != nil {
282
+			return err
283
+
284
+		}
285
+		refs = append(refs, r)
286
+	}
287
+
288
+	v.Set("fromImage", repoInfo.LocalName)
289
+	for i, r := range refs {
290
+		displayTag := r.reference.String()
291
+		if displayTag != "" {
292
+			displayTag = ":" + displayTag
293
+		}
294
+		fmt.Fprintf(cli.out, "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.LocalName, displayTag, r.digest)
295
+		v.Set("tag", r.digest.String())
296
+
297
+		_, _, err = cli.clientRequestAttemptLogin("POST", "/images/create?"+v.Encode(), nil, cli.out, repoInfo.Index, "pull")
298
+		if err != nil {
299
+			return err
300
+		}
301
+
302
+		// If reference is not trusted, tag by trusted reference
303
+		if !r.reference.HasDigest() {
304
+			if err := cli.tagTrusted(repoInfo, registry.DigestReference(r.digest), r.reference); err != nil {
305
+				return err
306
+
307
+			}
308
+		}
309
+	}
310
+	return nil
311
+}
312
+
313
+func targetStream(in io.Writer) (io.WriteCloser, <-chan []target) {
314
+	r, w := io.Pipe()
315
+	out := io.MultiWriter(in, w)
316
+	targetChan := make(chan []target)
317
+
318
+	go func() {
319
+		targets := []target{}
320
+		scanner := bufio.NewScanner(r)
321
+		scanner.Split(ansiescape.ScanANSILines)
322
+		for scanner.Scan() {
323
+			line := scanner.Bytes()
324
+			if matches := targetRegexp.FindSubmatch(line); len(matches) == 4 {
325
+				dgst, err := digest.ParseDigest(string(matches[2]))
326
+				if err != nil {
327
+					// Line does match what is expected, continue looking for valid lines
328
+					logrus.Debugf("Bad digest value %q in matched line, ignoring\n", string(matches[2]))
329
+					continue
330
+				}
331
+				s, err := strconv.ParseInt(string(matches[3]), 10, 64)
332
+				if err != nil {
333
+					// Line does match what is expected, continue looking for valid lines
334
+					logrus.Debugf("Bad size value %q in matched line, ignoring\n", string(matches[3]))
335
+					continue
336
+				}
337
+
338
+				targets = append(targets, target{
339
+					reference: registry.ParseReference(string(matches[1])),
340
+					digest:    dgst,
341
+					size:      s,
342
+				})
343
+			}
344
+		}
345
+		targetChan <- targets
346
+	}()
347
+
348
+	return ioutils.NewWriteCloserWrapper(out, w.Close), targetChan
349
+}
350
+
351
+func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, tag string, authConfig cliconfig.AuthConfig) error {
352
+	streamOut, targetChan := targetStream(cli.out)
353
+
354
+	v := url.Values{}
355
+	v.Set("tag", tag)
356
+
357
+	_, _, err := cli.clientRequestAttemptLogin("POST", "/images/"+repoInfo.LocalName+"/push?"+v.Encode(), nil, streamOut, repoInfo.Index, "push")
358
+	// Close stream channel to finish target parsing
359
+	if err := streamOut.Close(); err != nil {
360
+		return err
361
+	}
362
+	// Check error from request
363
+	if err != nil {
364
+		return err
365
+	}
366
+
367
+	// Get target results
368
+	targets := <-targetChan
369
+
370
+	if tag == "" {
371
+		fmt.Fprintf(cli.out, "No tag specified, skipping trust metadata push\n")
372
+		return nil
373
+	}
374
+	if len(targets) == 0 {
375
+		fmt.Fprintf(cli.out, "No targets found, skipping trust metadata push\n")
376
+		return nil
377
+	}
378
+
379
+	fmt.Fprintf(cli.out, "Signing and pushing trust metadata\n")
380
+
381
+	repo, err := cli.getNotaryRepository(repoInfo, authConfig)
382
+	if err != nil {
383
+		fmt.Fprintf(cli.out, "Error establishing connection to notary repository: %s\n", err)
384
+		return err
385
+	}
386
+
387
+	for _, target := range targets {
388
+		h, err := hex.DecodeString(target.digest.Hex())
389
+		if err != nil {
390
+			return err
391
+		}
392
+		t := &client.Target{
393
+			Name: target.reference.String(),
394
+			Hashes: data.Hashes{
395
+				string(target.digest.Algorithm()): h,
396
+			},
397
+			Length: int64(target.size),
398
+		}
399
+		if err := repo.AddTarget(t); err != nil {
400
+			return err
401
+		}
402
+	}
403
+
404
+	err = repo.Publish()
405
+	if _, ok := err.(*client.ErrRepoNotInitialized); !ok {
406
+		return notaryError(err)
407
+	}
408
+
409
+	ks := repo.KeyStoreManager
410
+	keys := ks.RootKeyStore().ListKeys()
411
+	var rootKey string
412
+
413
+	if len(keys) == 0 {
414
+		rootKey, err = ks.GenRootKey("ecdsa")
415
+		if err != nil {
416
+			return err
417
+		}
418
+	} else {
419
+		// TODO(dmcgowan): let user choose
420
+		rootKey = keys[0]
421
+	}
422
+
423
+	cryptoService, err := ks.GetRootCryptoService(rootKey)
424
+	if err != nil {
425
+		return err
426
+	}
427
+
428
+	if err := repo.Initialize(cryptoService); err != nil {
429
+		return notaryError(err)
430
+	}
431
+	fmt.Fprintf(cli.out, "Finished initializing %q\n", repoInfo.CanonicalName)
432
+
433
+	return notaryError(repo.Publish())
434
+}
... ...
@@ -288,7 +288,7 @@ func (p *v2Puller) pullV2Tag(tag, taggedName string) (bool, error) {
288 288
 		}
289 289
 	}
290 290
 
291
-	manifestDigest, err := digestFromManifest(manifest, p.repoInfo.LocalName)
291
+	manifestDigest, _, err := digestFromManifest(manifest, p.repoInfo.LocalName)
292 292
 	if err != nil {
293 293
 		return false, err
294 294
 	}
... ...
@@ -184,12 +184,12 @@ func (p *v2Pusher) pushV2Tag(tag string) error {
184 184
 		return err
185 185
 	}
186 186
 
187
-	manifestDigest, err := digestFromManifest(signed, p.repo.Name())
187
+	manifestDigest, manifestSize, err := digestFromManifest(signed, p.repo.Name())
188 188
 	if err != nil {
189 189
 		return err
190 190
 	}
191 191
 	if manifestDigest != "" {
192
-		out.Write(p.sf.FormatStatus("", "Digest: %s", manifestDigest))
192
+		out.Write(p.sf.FormatStatus("", "%s: digest: %s size: %d", tag, manifestDigest, manifestSize))
193 193
 	}
194 194
 
195 195
 	manSvc, err := p.repo.Manifests(context.Background())
... ...
@@ -97,15 +97,15 @@ func NewV2Repository(repoInfo *registry.RepositoryInfo, endpoint registry.APIEnd
97 97
 	return client.NewRepository(ctx, repoName, endpoint.URL, tr)
98 98
 }
99 99
 
100
-func digestFromManifest(m *manifest.SignedManifest, localName string) (digest.Digest, error) {
100
+func digestFromManifest(m *manifest.SignedManifest, localName string) (digest.Digest, int, error) {
101 101
 	payload, err := m.Payload()
102 102
 	if err != nil {
103 103
 		logrus.Debugf("could not retrieve manifest payload: %v", err)
104
-		return "", err
104
+		return "", 0, err
105 105
 	}
106 106
 	manifestDigest, err := digest.FromBytes(payload)
107 107
 	if err != nil {
108 108
 		logrus.Infof("Could not compute manifest digest for %s:%s : %v", localName, m.Tag, err)
109 109
 	}
110
-	return manifestDigest, nil
110
+	return manifestDigest, len(payload), nil
111 111
 }
... ...
@@ -10,8 +10,9 @@ import (
10 10
 )
11 11
 
12 12
 var (
13
-	repoName    = fmt.Sprintf("%v/dockercli/busybox-by-dgst", privateRegistryURL)
14
-	digestRegex = regexp.MustCompile("Digest: ([^\n]+)")
13
+	repoName        = fmt.Sprintf("%v/dockercli/busybox-by-dgst", privateRegistryURL)
14
+	pushDigestRegex = regexp.MustCompile("[\\S]+: digest: ([\\S]+) size: [0-9]+")
15
+	digestRegex     = regexp.MustCompile("Digest: ([\\S]+)")
15 16
 )
16 17
 
17 18
 func setupImage(c *check.C) (string, error) {
... ...
@@ -45,8 +46,7 @@ func setupImageWithTag(c *check.C, tag string) (string, error) {
45 45
 		return "", fmt.Errorf("error deleting images prior to real test: %s, %v", rmiout, err)
46 46
 	}
47 47
 
48
-	// the push output includes "Digest: <digest>", so find that
49
-	matches := digestRegex.FindStringSubmatch(out)
48
+	matches := pushDigestRegex.FindStringSubmatch(out)
50 49
 	if len(matches) != 2 {
51 50
 		return "", fmt.Errorf("unable to parse digest from push output: %s", out)
52 51
 	}
53 52
new file mode 100644
... ...
@@ -0,0 +1,89 @@
0
+package ansiescape
1
+
2
+import "bytes"
3
+
4
+// dropCR drops a leading or terminal \r from the data.
5
+func dropCR(data []byte) []byte {
6
+	if len(data) > 0 && data[len(data)-1] == '\r' {
7
+		data = data[0 : len(data)-1]
8
+	}
9
+	if len(data) > 0 && data[0] == '\r' {
10
+		data = data[1:]
11
+	}
12
+	return data
13
+}
14
+
15
+// escapeSequenceLength calculates the length of an ANSI escape sequence
16
+// If there is not enough characters to match a sequence, -1 is returned,
17
+// if there is no valid sequence 0 is returned, otherwise the number
18
+// of bytes in the sequence is returned. Only returns length for
19
+// line moving sequences.
20
+func escapeSequenceLength(data []byte) int {
21
+	next := 0
22
+	if len(data) <= next {
23
+		return -1
24
+	}
25
+	if data[next] != '[' {
26
+		return 0
27
+	}
28
+	for {
29
+		next = next + 1
30
+		if len(data) <= next {
31
+			return -1
32
+		}
33
+		if (data[next] > '9' || data[next] < '0') && data[next] != ';' {
34
+			break
35
+		}
36
+	}
37
+	if len(data) <= next {
38
+		return -1
39
+	}
40
+	// Only match line moving codes
41
+	switch data[next] {
42
+	case 'A', 'B', 'E', 'F', 'H', 'h':
43
+		return next + 1
44
+	}
45
+
46
+	return 0
47
+}
48
+
49
+// ScanANSILines is a scanner function which splits the
50
+// input based on ANSI escape codes and new lines.
51
+func ScanANSILines(data []byte, atEOF bool) (advance int, token []byte, err error) {
52
+	if atEOF && len(data) == 0 {
53
+		return 0, nil, nil
54
+	}
55
+
56
+	// Look for line moving escape sequence
57
+	if i := bytes.IndexByte(data, '\x1b'); i >= 0 {
58
+		last := 0
59
+		for i >= 0 {
60
+			last = last + i
61
+
62
+			// get length of ANSI escape sequence
63
+			sl := escapeSequenceLength(data[last+1:])
64
+			if sl == -1 {
65
+				return 0, nil, nil
66
+			}
67
+			if sl == 0 {
68
+				// If no relevant sequence was found, skip
69
+				last = last + 1
70
+				i = bytes.IndexByte(data[last:], '\x1b')
71
+				continue
72
+			}
73
+
74
+			return last + 1 + sl, dropCR(data[0:(last)]), nil
75
+		}
76
+	}
77
+	if i := bytes.IndexByte(data, '\n'); i >= 0 {
78
+		// No escape sequence, check for new line
79
+		return i + 1, dropCR(data[0:i]), nil
80
+	}
81
+
82
+	// If we're at EOF, we have a final, non-terminated line. Return it.
83
+	if atEOF {
84
+		return len(data), dropCR(data), nil
85
+	}
86
+	// Request more data.
87
+	return 0, nil, nil
88
+}
0 89
new file mode 100644
... ...
@@ -0,0 +1,53 @@
0
+package ansiescape
1
+
2
+import (
3
+	"bufio"
4
+	"strings"
5
+	"testing"
6
+)
7
+
8
+func TestSplit(t *testing.T) {
9
+	lines := []string{
10
+		"test line 1",
11
+		"another test line",
12
+		"some test line",
13
+		"line with non-cursor moving sequence \x1b[1T", // Scroll Down
14
+		"line with \x1b[31;1mcolor\x1b[0m then reset",  // "color" in Bold Red
15
+		"cursor forward \x1b[1C and backward \x1b[1D",
16
+		"invalid sequence \x1babcd",
17
+		"",
18
+		"after empty",
19
+	}
20
+	splitSequences := []string{
21
+		"\x1b[1A",   // Cursor up
22
+		"\x1b[1B",   // Cursor down
23
+		"\x1b[1E",   // Cursor next line
24
+		"\x1b[1F",   // Cursor previous line
25
+		"\x1b[1;1H", // Move cursor to position
26
+		"\x1b[1;1h", // Move cursor to position
27
+		"\n",
28
+		"\r\n",
29
+		"\n\r",
30
+		"\x1b[1A\r",
31
+		"\r\x1b[1A",
32
+	}
33
+
34
+	for _, sequence := range splitSequences {
35
+		scanner := bufio.NewScanner(strings.NewReader(strings.Join(lines, sequence)))
36
+		scanner.Split(ScanANSILines)
37
+		i := 0
38
+		for scanner.Scan() {
39
+			if i >= len(lines) {
40
+				t.Fatalf("Too many scanned lines")
41
+			}
42
+			scanned := scanner.Text()
43
+			if scanned != lines[i] {
44
+				t.Fatalf("Wrong line scanned with sequence %q\n\tExpected: %q\n\tActual:   %q", sequence, lines[i], scanned)
45
+			}
46
+			i++
47
+		}
48
+		if i < len(lines) {
49
+			t.Errorf("Wrong number of lines for sequence %q: %d, expected %d", sequence, i, len(lines))
50
+		}
51
+	}
52
+}
... ...
@@ -38,8 +38,8 @@ const (
38 38
 	IndexServer = DefaultV1Registry + "/v1/"
39 39
 	// IndexName is the name of the index
40 40
 	IndexName = "docker.io"
41
-
42
-	// IndexServer = "https://registry-stage.hub.docker.com/v1/"
41
+	// NotaryServer is the endpoint serving the Notary trust server
42
+	NotaryServer = "https://notary.docker.io"
43 43
 )
44 44
 
45 45
 var (
46 46
new file mode 100644
... ...
@@ -0,0 +1,68 @@
0
+package registry
1
+
2
+import (
3
+	"strings"
4
+
5
+	"github.com/docker/distribution/digest"
6
+)
7
+
8
+// Reference represents a tag or digest within a repository
9
+type Reference interface {
10
+	// HasDigest returns whether the reference has a verifiable
11
+	// content addressable reference which may be considered secure.
12
+	HasDigest() bool
13
+
14
+	// ImageName returns an image name for the given repository
15
+	ImageName(string) string
16
+
17
+	// Returns a string representation of the reference
18
+	String() string
19
+}
20
+
21
+type tagReference struct {
22
+	tag string
23
+}
24
+
25
+func (tr tagReference) HasDigest() bool {
26
+	return false
27
+}
28
+
29
+func (tr tagReference) ImageName(repo string) string {
30
+	return repo + ":" + tr.tag
31
+}
32
+
33
+func (tr tagReference) String() string {
34
+	return tr.tag
35
+}
36
+
37
+type digestReference struct {
38
+	digest digest.Digest
39
+}
40
+
41
+func (dr digestReference) HasDigest() bool {
42
+	return true
43
+}
44
+
45
+func (dr digestReference) ImageName(repo string) string {
46
+	return repo + "@" + dr.String()
47
+}
48
+
49
+func (dr digestReference) String() string {
50
+	return dr.digest.String()
51
+}
52
+
53
+// ParseReference parses a reference into either a digest or tag reference
54
+func ParseReference(ref string) Reference {
55
+	if strings.Contains(ref, ":") {
56
+		dgst, err := digest.ParseDigest(ref)
57
+		if err == nil {
58
+			return digestReference{digest: dgst}
59
+		}
60
+	}
61
+	return tagReference{tag: ref}
62
+}
63
+
64
+// DigestReference creates a digest reference using a digest
65
+func DigestReference(dgst digest.Digest) Reference {
66
+	return digestReference{digest: dgst}
67
+}
... ...
@@ -2,10 +2,14 @@ package registry
2 2
 
3 3
 import (
4 4
 	"crypto/tls"
5
+	"crypto/x509"
5 6
 	"errors"
7
+	"fmt"
8
+	"io/ioutil"
6 9
 	"net"
7 10
 	"net/http"
8 11
 	"os"
12
+	"path/filepath"
9 13
 	"runtime"
10 14
 	"strings"
11 15
 	"time"
... ...
@@ -54,6 +58,54 @@ func hasFile(files []os.FileInfo, name string) bool {
54 54
 	return false
55 55
 }
56 56
 
57
+// ReadCertsDirectory reads the directory for TLS certificates
58
+// including roots and certificate pairs and updates the
59
+// provided TLS configuration.
60
+func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
61
+	fs, err := ioutil.ReadDir(directory)
62
+	if err != nil && !os.IsNotExist(err) {
63
+		return err
64
+	}
65
+
66
+	for _, f := range fs {
67
+		if strings.HasSuffix(f.Name(), ".crt") {
68
+			if tlsConfig.RootCAs == nil {
69
+				// TODO(dmcgowan): Copy system pool
70
+				tlsConfig.RootCAs = x509.NewCertPool()
71
+			}
72
+			logrus.Debugf("crt: %s", filepath.Join(directory, f.Name()))
73
+			data, err := ioutil.ReadFile(filepath.Join(directory, f.Name()))
74
+			if err != nil {
75
+				return err
76
+			}
77
+			tlsConfig.RootCAs.AppendCertsFromPEM(data)
78
+		}
79
+		if strings.HasSuffix(f.Name(), ".cert") {
80
+			certName := f.Name()
81
+			keyName := certName[:len(certName)-5] + ".key"
82
+			logrus.Debugf("cert: %s", filepath.Join(directory, f.Name()))
83
+			if !hasFile(fs, keyName) {
84
+				return fmt.Errorf("Missing key %s for certificate %s", keyName, certName)
85
+			}
86
+			cert, err := tls.LoadX509KeyPair(filepath.Join(directory, certName), filepath.Join(directory, keyName))
87
+			if err != nil {
88
+				return err
89
+			}
90
+			tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
91
+		}
92
+		if strings.HasSuffix(f.Name(), ".key") {
93
+			keyName := f.Name()
94
+			certName := keyName[:len(keyName)-4] + ".cert"
95
+			logrus.Debugf("key: %s", filepath.Join(directory, f.Name()))
96
+			if !hasFile(fs, certName) {
97
+				return fmt.Errorf("Missing certificate %s for key %s", certName, keyName)
98
+			}
99
+		}
100
+	}
101
+
102
+	return nil
103
+}
104
+
57 105
 // DockerHeaders returns request modifiers that ensure requests have
58 106
 // the User-Agent header set to dockerUserAgent and that metaHeaders
59 107
 // are added.
... ...
@@ -2,12 +2,9 @@ package registry
2 2
 
3 3
 import (
4 4
 	"crypto/tls"
5
-	"crypto/x509"
6 5
 	"fmt"
7
-	"io/ioutil"
8 6
 	"net/http"
9 7
 	"net/url"
10
-	"os"
11 8
 	"path/filepath"
12 9
 	"strings"
13 10
 
... ...
@@ -110,57 +107,11 @@ func (s *Service) TLSConfig(hostname string) (*tls.Config, error) {
110 110
 	tlsConfig.InsecureSkipVerify = !isSecure
111 111
 
112 112
 	if isSecure {
113
-		hasFile := func(files []os.FileInfo, name string) bool {
114
-			for _, f := range files {
115
-				if f.Name() == name {
116
-					return true
117
-				}
118
-			}
119
-			return false
120
-		}
121
-
122 113
 		hostDir := filepath.Join(CertsDir, hostname)
123 114
 		logrus.Debugf("hostDir: %s", hostDir)
124
-		fs, err := ioutil.ReadDir(hostDir)
125
-		if err != nil && !os.IsNotExist(err) {
115
+		if err := ReadCertsDirectory(&tlsConfig, hostDir); err != nil {
126 116
 			return nil, err
127 117
 		}
128
-
129
-		for _, f := range fs {
130
-			if strings.HasSuffix(f.Name(), ".crt") {
131
-				if tlsConfig.RootCAs == nil {
132
-					// TODO(dmcgowan): Copy system pool
133
-					tlsConfig.RootCAs = x509.NewCertPool()
134
-				}
135
-				logrus.Debugf("crt: %s", filepath.Join(hostDir, f.Name()))
136
-				data, err := ioutil.ReadFile(filepath.Join(hostDir, f.Name()))
137
-				if err != nil {
138
-					return nil, err
139
-				}
140
-				tlsConfig.RootCAs.AppendCertsFromPEM(data)
141
-			}
142
-			if strings.HasSuffix(f.Name(), ".cert") {
143
-				certName := f.Name()
144
-				keyName := certName[:len(certName)-5] + ".key"
145
-				logrus.Debugf("cert: %s", filepath.Join(hostDir, f.Name()))
146
-				if !hasFile(fs, keyName) {
147
-					return nil, fmt.Errorf("Missing key %s for certificate %s", keyName, certName)
148
-				}
149
-				cert, err := tls.LoadX509KeyPair(filepath.Join(hostDir, certName), filepath.Join(hostDir, keyName))
150
-				if err != nil {
151
-					return nil, err
152
-				}
153
-				tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
154
-			}
155
-			if strings.HasSuffix(f.Name(), ".key") {
156
-				keyName := f.Name()
157
-				certName := keyName[:len(keyName)-4] + ".cert"
158
-				logrus.Debugf("key: %s", filepath.Join(hostDir, f.Name()))
159
-				if !hasFile(fs, certName) {
160
-					return nil, fmt.Errorf("Missing certificate %s for key %s", certName, keyName)
161
-				}
162
-			}
163
-		}
164 118
 	}
165 119
 
166 120
 	return &tlsConfig, nil