Browse code

Merge pull request #13576 from stevvooe/verify-digests

Properly verify manifests and layer digests on pull

Arnaud Porterie authored on 2015/06/03 03:16:23
Showing 21 changed files
... ...
@@ -8,92 +8,164 @@ import (
8 8
 	"github.com/docker/distribution/digest"
9 9
 	"github.com/docker/docker/registry"
10 10
 	"github.com/docker/docker/trust"
11
-	"github.com/docker/docker/utils"
12 11
 	"github.com/docker/libtrust"
13 12
 )
14 13
 
15
-// loadManifest loads a manifest from a byte array and verifies its content.
16
-// The signature must be verified or an error is returned. If the manifest
17
-// contains no signatures by a trusted key for the name in the manifest, the
18
-// image is not considered verified. The parsed manifest object and a boolean
19
-// for whether the manifest is verified is returned.
20
-func (s *TagStore) loadManifest(manifestBytes []byte, dgst, ref string) (*registry.ManifestData, bool, error) {
21
-	sig, err := libtrust.ParsePrettySignature(manifestBytes, "signatures")
14
+// loadManifest loads a manifest from a byte array and verifies its content,
15
+// returning the local digest, the manifest itself, whether or not it was
16
+// verified. If ref is a digest, rather than a tag, this will be treated as
17
+// the local digest. An error will be returned if the signature verification
18
+// fails, local digest verification fails and, if provided, the remote digest
19
+// verification fails. The boolean return will only be false without error on
20
+// the failure of signatures trust check.
21
+func (s *TagStore) loadManifest(manifestBytes []byte, ref string, remoteDigest digest.Digest) (digest.Digest, *registry.ManifestData, bool, error) {
22
+	payload, keys, err := unpackSignedManifest(manifestBytes)
22 23
 	if err != nil {
23
-		return nil, false, fmt.Errorf("error parsing payload: %s", err)
24
+		return "", nil, false, fmt.Errorf("error unpacking manifest: %v", err)
24 25
 	}
25 26
 
26
-	keys, err := sig.Verify()
27
-	if err != nil {
28
-		return nil, false, fmt.Errorf("error verifying payload: %s", err)
29
-	}
30
-
31
-	payload, err := sig.Payload()
32
-	if err != nil {
33
-		return nil, false, fmt.Errorf("error retrieving payload: %s", err)
34
-	}
27
+	// TODO(stevvooe): It would be a lot better here to build up a stack of
28
+	// verifiers, then push the bytes one time for signatures and digests, but
29
+	// the manifests are typically small, so this optimization is not worth
30
+	// hacking this code without further refactoring.
35 31
 
36
-	var manifestDigest digest.Digest
32
+	var localDigest digest.Digest
37 33
 
38
-	if dgst != "" {
39
-		manifestDigest, err = digest.ParseDigest(dgst)
40
-		if err != nil {
41
-			return nil, false, fmt.Errorf("invalid manifest digest from registry: %s", err)
34
+	// Verify the local digest, if present in ref. ParseDigest will validate
35
+	// that the ref is a digest and verify against that if present. Otherwize
36
+	// (on error), we simply compute the localDigest and proceed.
37
+	if dgst, err := digest.ParseDigest(ref); err == nil {
38
+		// verify the manifest against local ref
39
+		if err := verifyDigest(dgst, payload); err != nil {
40
+			return "", nil, false, fmt.Errorf("verifying local digest: %v", err)
42 41
 		}
43 42
 
44
-		dgstVerifier, err := digest.NewDigestVerifier(manifestDigest)
43
+		localDigest = dgst
44
+	} else {
45
+		// We don't have a local digest, since we are working from a tag.
46
+		// Compute the digest of the payload and return that.
47
+		logrus.Debugf("provided manifest reference %q is not a digest: %v", ref, err)
48
+		localDigest, err = digest.FromBytes(payload)
45 49
 		if err != nil {
46
-			return nil, false, fmt.Errorf("unable to verify manifest digest from registry: %s", err)
47
-		}
48
-
49
-		dgstVerifier.Write(payload)
50
-
51
-		if !dgstVerifier.Verified() {
52
-			computedDigest, _ := digest.FromBytes(payload)
53
-			return nil, false, fmt.Errorf("unable to verify manifest digest: registry has %q, computed %q", manifestDigest, computedDigest)
50
+			// near impossible
51
+			logrus.Errorf("error calculating local digest during tag pull: %v", err)
52
+			return "", nil, false, err
54 53
 		}
55 54
 	}
56 55
 
57
-	if utils.DigestReference(ref) && ref != manifestDigest.String() {
58
-		return nil, false, fmt.Errorf("mismatching image manifest digest: got %q, expected %q", manifestDigest, ref)
56
+	// verify against the remote digest, if available
57
+	if remoteDigest != "" {
58
+		if err := verifyDigest(remoteDigest, payload); err != nil {
59
+			return "", nil, false, fmt.Errorf("verifying remote digest: %v", err)
60
+		}
59 61
 	}
60 62
 
61 63
 	var manifest registry.ManifestData
62 64
 	if err := json.Unmarshal(payload, &manifest); err != nil {
63
-		return nil, false, fmt.Errorf("error unmarshalling manifest: %s", err)
65
+		return "", nil, false, fmt.Errorf("error unmarshalling manifest: %s", err)
64 66
 	}
65
-	if manifest.SchemaVersion != 1 {
66
-		return nil, false, fmt.Errorf("unsupported schema version: %d", manifest.SchemaVersion)
67
+
68
+	// validate the contents of the manifest
69
+	if err := validateManifest(&manifest); err != nil {
70
+		return "", nil, false, err
67 71
 	}
68 72
 
69 73
 	var verified bool
74
+	verified, err = s.verifyTrustedKeys(manifest.Name, keys)
75
+	if err != nil {
76
+		return "", nil, false, fmt.Errorf("error verifying trusted keys: %v", err)
77
+	}
78
+
79
+	return localDigest, &manifest, verified, nil
80
+}
81
+
82
+// unpackSignedManifest takes the raw, signed manifest bytes, unpacks the jws
83
+// and returns the payload and public keys used to signed the manifest.
84
+// Signatures are verified for authenticity but not against the trust store.
85
+func unpackSignedManifest(p []byte) ([]byte, []libtrust.PublicKey, error) {
86
+	sig, err := libtrust.ParsePrettySignature(p, "signatures")
87
+	if err != nil {
88
+		return nil, nil, fmt.Errorf("error parsing payload: %s", err)
89
+	}
90
+
91
+	keys, err := sig.Verify()
92
+	if err != nil {
93
+		return nil, nil, fmt.Errorf("error verifying payload: %s", err)
94
+	}
95
+
96
+	payload, err := sig.Payload()
97
+	if err != nil {
98
+		return nil, nil, fmt.Errorf("error retrieving payload: %s", err)
99
+	}
100
+
101
+	return payload, keys, nil
102
+}
103
+
104
+// verifyTrustedKeys checks the keys provided against the trust store,
105
+// ensuring that the provided keys are trusted for the namespace. The keys
106
+// provided from this method must come from the signatures provided as part of
107
+// the manifest JWS package, obtained from unpackSignedManifest or libtrust.
108
+func (s *TagStore) verifyTrustedKeys(namespace string, keys []libtrust.PublicKey) (verified bool, err error) {
109
+	if namespace[0] != '/' {
110
+		namespace = "/" + namespace
111
+	}
112
+
70 113
 	for _, key := range keys {
71
-		namespace := manifest.Name
72
-		if namespace[0] != '/' {
73
-			namespace = "/" + namespace
74
-		}
75 114
 		b, err := key.MarshalJSON()
76 115
 		if err != nil {
77
-			return nil, false, fmt.Errorf("error marshalling public key: %s", err)
116
+			return false, fmt.Errorf("error marshalling public key: %s", err)
78 117
 		}
79 118
 		// Check key has read/write permission (0x03)
80 119
 		v, err := s.trustService.CheckKey(namespace, b, 0x03)
81 120
 		if err != nil {
82 121
 			vErr, ok := err.(trust.NotVerifiedError)
83 122
 			if !ok {
84
-				return nil, false, fmt.Errorf("error running key check: %s", err)
123
+				return false, fmt.Errorf("error running key check: %s", err)
85 124
 			}
86 125
 			logrus.Debugf("Key check result: %v", vErr)
87 126
 		}
88 127
 		verified = v
89
-		if verified {
90
-			logrus.Debug("Key check result: verified")
91
-		}
92 128
 	}
93
-	return &manifest, verified, nil
129
+
130
+	if verified {
131
+		logrus.Debug("Key check result: verified")
132
+	}
133
+
134
+	return
94 135
 }
95 136
 
96
-func checkValidManifest(manifest *registry.ManifestData) error {
137
+// verifyDigest checks the contents of p against the provided digest. Note
138
+// that for manifests, this is the signed payload and not the raw bytes with
139
+// signatures.
140
+func verifyDigest(dgst digest.Digest, p []byte) error {
141
+	if err := dgst.Validate(); err != nil {
142
+		return fmt.Errorf("error validating  digest %q: %v", dgst, err)
143
+	}
144
+
145
+	verifier, err := digest.NewDigestVerifier(dgst)
146
+	if err != nil {
147
+		// There are not many ways this can go wrong: if it does, its
148
+		// fatal. Likley, the cause would be poor validation of the
149
+		// incoming reference.
150
+		return fmt.Errorf("error creating verifier for digest %q: %v", dgst, err)
151
+	}
152
+
153
+	if _, err := verifier.Write(p); err != nil {
154
+		return fmt.Errorf("error writing payload to digest verifier (verifier target %q): %v", dgst, err)
155
+	}
156
+
157
+	if !verifier.Verified() {
158
+		return fmt.Errorf("verification against digest %q failed", dgst)
159
+	}
160
+
161
+	return nil
162
+}
163
+
164
+func validateManifest(manifest *registry.ManifestData) error {
165
+	if manifest.SchemaVersion != 1 {
166
+		return fmt.Errorf("unsupported schema version: %d", manifest.SchemaVersion)
167
+	}
168
+
97 169
 	if len(manifest.FSLayers) != len(manifest.History) {
98 170
 		return fmt.Errorf("length of history not equal to number of layers")
99 171
 	}
... ...
@@ -8,11 +8,13 @@ import (
8 8
 	"os"
9 9
 	"testing"
10 10
 
11
+	"github.com/docker/distribution/digest"
11 12
 	"github.com/docker/docker/image"
12 13
 	"github.com/docker/docker/pkg/tarsum"
13 14
 	"github.com/docker/docker/registry"
14 15
 	"github.com/docker/docker/runconfig"
15 16
 	"github.com/docker/docker/utils"
17
+	"github.com/docker/libtrust"
16 18
 )
17 19
 
18 20
 const (
... ...
@@ -181,3 +183,121 @@ func TestManifestTarsumCache(t *testing.T) {
181 181
 		t.Fatalf("Unexpected json value\nExpected:\n%s\nActual:\n%s", v1compat, manifest.History[0].V1Compatibility)
182 182
 	}
183 183
 }
184
+
185
+// TestManifestDigestCheck ensures that loadManifest properly verifies the
186
+// remote and local digest.
187
+func TestManifestDigestCheck(t *testing.T) {
188
+	tmp, err := utils.TestDirectory("")
189
+	if err != nil {
190
+		t.Fatal(err)
191
+	}
192
+	defer os.RemoveAll(tmp)
193
+	store := mkTestTagStore(tmp, t)
194
+	defer store.graph.driver.Cleanup()
195
+
196
+	archive, err := fakeTar()
197
+	if err != nil {
198
+		t.Fatal(err)
199
+	}
200
+	img := &image.Image{ID: testManifestImageID}
201
+	if err := store.graph.Register(img, archive); err != nil {
202
+		t.Fatal(err)
203
+	}
204
+	if err := store.Tag(testManifestImageName, testManifestTag, testManifestImageID, false); err != nil {
205
+		t.Fatal(err)
206
+	}
207
+
208
+	if cs, err := img.GetCheckSum(store.graph.ImageRoot(testManifestImageID)); err != nil {
209
+		t.Fatal(err)
210
+	} else if cs != "" {
211
+		t.Fatalf("Non-empty checksum file after register")
212
+	}
213
+
214
+	// Generate manifest
215
+	payload, err := store.newManifest(testManifestImageName, testManifestImageName, testManifestTag)
216
+	if err != nil {
217
+		t.Fatalf("unexpected error generating test manifest: %v", err)
218
+	}
219
+
220
+	pk, err := libtrust.GenerateECP256PrivateKey()
221
+	if err != nil {
222
+		t.Fatalf("unexpected error generating private key: %v", err)
223
+	}
224
+
225
+	sig, err := libtrust.NewJSONSignature(payload)
226
+	if err != nil {
227
+		t.Fatalf("error creating signature: %v", err)
228
+	}
229
+
230
+	if err := sig.Sign(pk); err != nil {
231
+		t.Fatalf("error signing manifest bytes: %v", err)
232
+	}
233
+
234
+	signedBytes, err := sig.PrettySignature("signatures")
235
+	if err != nil {
236
+		t.Fatalf("error getting signed bytes: %v", err)
237
+	}
238
+
239
+	dgst, err := digest.FromBytes(payload)
240
+	if err != nil {
241
+		t.Fatalf("error getting digest of manifest: %v", err)
242
+	}
243
+
244
+	// use this as the "bad" digest
245
+	zeroDigest, err := digest.FromBytes([]byte{})
246
+	if err != nil {
247
+		t.Fatalf("error making zero digest: %v", err)
248
+	}
249
+
250
+	// Remote and local match, everything should look good
251
+	local, _, _, err := store.loadManifest(signedBytes, dgst.String(), dgst)
252
+	if err != nil {
253
+		t.Fatalf("unexpected error verifying local and remote digest: %v", err)
254
+	}
255
+
256
+	if local != dgst {
257
+		t.Fatalf("local digest not correctly calculated: %v", err)
258
+	}
259
+
260
+	// remote and no local, since pulling by tag
261
+	local, _, _, err = store.loadManifest(signedBytes, "tag", dgst)
262
+	if err != nil {
263
+		t.Fatalf("unexpected error verifying tag pull and remote digest: %v", err)
264
+	}
265
+
266
+	if local != dgst {
267
+		t.Fatalf("local digest not correctly calculated: %v", err)
268
+	}
269
+
270
+	// remote and differing local, this is the most important to fail
271
+	local, _, _, err = store.loadManifest(signedBytes, zeroDigest.String(), dgst)
272
+	if err == nil {
273
+		t.Fatalf("error expected when verifying with differing local digest")
274
+	}
275
+
276
+	// no remote, no local (by tag)
277
+	local, _, _, err = store.loadManifest(signedBytes, "tag", "")
278
+	if err != nil {
279
+		t.Fatalf("unexpected error verifying manifest without remote digest: %v", err)
280
+	}
281
+
282
+	if local != dgst {
283
+		t.Fatalf("local digest not correctly calculated: %v", err)
284
+	}
285
+
286
+	// no remote, with local
287
+	local, _, _, err = store.loadManifest(signedBytes, dgst.String(), "")
288
+	if err != nil {
289
+		t.Fatalf("unexpected error verifying manifest without remote digest: %v", err)
290
+	}
291
+
292
+	if local != dgst {
293
+		t.Fatalf("local digest not correctly calculated: %v", err)
294
+	}
295
+
296
+	// bad remote, we fail the check.
297
+	local, _, _, err = store.loadManifest(signedBytes, dgst.String(), zeroDigest)
298
+	if err == nil {
299
+		t.Fatalf("error expected when verifying with differing remote digest")
300
+	}
301
+}
... ...
@@ -457,17 +457,6 @@ func WriteStatus(requestedTag string, out io.Writer, sf *streamformatter.StreamF
457 457
 	}
458 458
 }
459 459
 
460
-// downloadInfo is used to pass information from download to extractor
461
-type downloadInfo struct {
462
-	imgJSON    []byte
463
-	img        *image.Image
464
-	digest     digest.Digest
465
-	tmpFile    *os.File
466
-	length     int64
467
-	downloaded bool
468
-	err        chan error
469
-}
470
-
471 460
 func (s *TagStore) pullV2Repository(r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *streamformatter.StreamFormatter) error {
472 461
 	endpoint, err := r.V2RegistryEndpoint(repoInfo.Index)
473 462
 	if err != nil {
... ...
@@ -517,27 +506,34 @@ func (s *TagStore) pullV2Repository(r *registry.Session, out io.Writer, repoInfo
517 517
 func (s *TagStore) pullV2Tag(r *registry.Session, out io.Writer, endpoint *registry.Endpoint, repoInfo *registry.RepositoryInfo, tag string, sf *streamformatter.StreamFormatter, auth *registry.RequestAuthorization) (bool, error) {
518 518
 	logrus.Debugf("Pulling tag from V2 registry: %q", tag)
519 519
 
520
-	manifestBytes, manifestDigest, err := r.GetV2ImageManifest(endpoint, repoInfo.RemoteName, tag, auth)
520
+	remoteDigest, manifestBytes, err := r.GetV2ImageManifest(endpoint, repoInfo.RemoteName, tag, auth)
521 521
 	if err != nil {
522 522
 		return false, err
523 523
 	}
524 524
 
525 525
 	// loadManifest ensures that the manifest payload has the expected digest
526 526
 	// if the tag is a digest reference.
527
-	manifest, verified, err := s.loadManifest(manifestBytes, manifestDigest, tag)
527
+	localDigest, manifest, verified, err := s.loadManifest(manifestBytes, tag, remoteDigest)
528 528
 	if err != nil {
529 529
 		return false, fmt.Errorf("error verifying manifest: %s", err)
530 530
 	}
531 531
 
532
-	if err := checkValidManifest(manifest); err != nil {
533
-		return false, err
534
-	}
535
-
536 532
 	if verified {
537 533
 		logrus.Printf("Image manifest for %s has been verified", utils.ImageReference(repoInfo.CanonicalName, tag))
538 534
 	}
539 535
 	out.Write(sf.FormatStatus(tag, "Pulling from %s", repoInfo.CanonicalName))
540 536
 
537
+	// downloadInfo is used to pass information from download to extractor
538
+	type downloadInfo struct {
539
+		imgJSON    []byte
540
+		img        *image.Image
541
+		digest     digest.Digest
542
+		tmpFile    *os.File
543
+		length     int64
544
+		downloaded bool
545
+		err        chan error
546
+	}
547
+
541 548
 	downloads := make([]downloadInfo, len(manifest.FSLayers))
542 549
 
543 550
 	for i := len(manifest.FSLayers) - 1; i >= 0; i-- {
... ...
@@ -610,8 +606,7 @@ func (s *TagStore) pullV2Tag(r *registry.Session, out io.Writer, endpoint *regis
610 610
 				out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), "Verifying Checksum", nil))
611 611
 
612 612
 				if !verifier.Verified() {
613
-					logrus.Infof("Image verification failed: checksum mismatch for %q", di.digest.String())
614
-					verified = false
613
+					return fmt.Errorf("image layer digest verification failed for %q", di.digest)
615 614
 				}
616 615
 
617 616
 				out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), "Download complete", nil))
... ...
@@ -688,15 +683,33 @@ func (s *TagStore) pullV2Tag(r *registry.Session, out io.Writer, endpoint *regis
688 688
 		out.Write(sf.FormatStatus(utils.ImageReference(repoInfo.CanonicalName, tag), "The image you are pulling has been verified. Important: image verification is a tech preview feature and should not be relied on to provide security."))
689 689
 	}
690 690
 
691
-	if manifestDigest != "" {
692
-		out.Write(sf.FormatStatus("", "Digest: %s", manifestDigest))
691
+	if localDigest != remoteDigest { // this is not a verification check.
692
+		// NOTE(stevvooe): This is a very defensive branch and should never
693
+		// happen, since all manifest digest implementations use the same
694
+		// algorithm.
695
+		logrus.WithFields(
696
+			logrus.Fields{
697
+				"local":  localDigest,
698
+				"remote": remoteDigest,
699
+			}).Debugf("local digest does not match remote")
700
+
701
+		out.Write(sf.FormatStatus("", "Remote Digest: %s", remoteDigest))
693 702
 	}
694 703
 
695
-	if utils.DigestReference(tag) {
696
-		if err = s.SetDigest(repoInfo.LocalName, tag, downloads[0].img.ID); err != nil {
704
+	out.Write(sf.FormatStatus("", "Digest: %s", localDigest))
705
+
706
+	if tag == localDigest.String() {
707
+		// TODO(stevvooe): Ideally, we should always set the digest so we can
708
+		// use the digest whether we pull by it or not. Unfortunately, the tag
709
+		// store treats the digest as a separate tag, meaning there may be an
710
+		// untagged digest image that would seem to be dangling by a user.
711
+
712
+		if err = s.SetDigest(repoInfo.LocalName, localDigest.String(), downloads[0].img.ID); err != nil {
697 713
 			return false, err
698 714
 		}
699
-	} else {
715
+	}
716
+
717
+	if !utils.DigestReference(tag) {
700 718
 		// only set the repository/tag -> image ID mapping when pulling by tag (i.e. not by digest)
701 719
 		if err = s.Tag(repoInfo.LocalName, tag, downloads[0].img.ID, true); err != nil {
702 720
 			return false, err
... ...
@@ -413,7 +413,7 @@ func (s *TagStore) pushV2Repository(r *registry.Session, localRepo Repository, o
413 413
 			m.History[i] = &registry.ManifestHistory{V1Compatibility: string(jsonData)}
414 414
 		}
415 415
 
416
-		if err := checkValidManifest(m); err != nil {
416
+		if err := validateManifest(m); err != nil {
417 417
 			return fmt.Errorf("invalid manifest: %s", err)
418 418
 		}
419 419
 
... ...
@@ -12,6 +12,7 @@ import (
12 12
 	"github.com/docker/docker/daemon/graphdriver"
13 13
 	_ "github.com/docker/docker/daemon/graphdriver/vfs" // import the vfs driver so it is used in the tests
14 14
 	"github.com/docker/docker/image"
15
+	"github.com/docker/docker/trust"
15 16
 	"github.com/docker/docker/utils"
16 17
 )
17 18
 
... ...
@@ -60,9 +61,16 @@ func mkTestTagStore(root string, t *testing.T) *TagStore {
60 60
 	if err != nil {
61 61
 		t.Fatal(err)
62 62
 	}
63
+
64
+	trust, err := trust.NewTrustStore(root + "/trust")
65
+	if err != nil {
66
+		t.Fatal(err)
67
+	}
68
+
63 69
 	tagCfg := &TagStoreConfig{
64 70
 		Graph:  graph,
65 71
 		Events: events.New(),
72
+		Trust:  trust,
66 73
 	}
67 74
 	store, err := NewTagStore(path.Join(root, "tags"), tagCfg)
68 75
 	if err != nil {
... ...
@@ -60,7 +60,7 @@ clone git github.com/vishvananda/netns 008d17ae001344769b031375bdb38a86219154c6
60 60
 clone git github.com/vishvananda/netlink 8eb64238879fed52fd51c5b30ad20b928fb4c36c
61 61
 
62 62
 # get distribution packages
63
-clone git github.com/docker/distribution d957768537c5af40e4f4cd96871f7b2bde9e2923
63
+clone git github.com/docker/distribution b9eeb328080d367dbde850ec6e94f1e4ac2b5efe
64 64
 mv src/github.com/docker/distribution/digest tmp-digest
65 65
 mv src/github.com/docker/distribution/registry/api tmp-api
66 66
 rm -rf src/github.com/docker/distribution
... ...
@@ -68,10 +68,15 @@ func (r *Session) GetV2Authorization(ep *Endpoint, imageName string, readOnly bo
68 68
 //  1.c) if anything else, err
69 69
 // 2) PUT the created/signed manifest
70 70
 //
71
-func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, auth *RequestAuthorization) ([]byte, string, error) {
71
+
72
+// GetV2ImageManifest simply fetches the bytes of a manifest and the remote
73
+// digest, if available in the request. Note that the application shouldn't
74
+// rely on the untrusted remoteDigest, and should also verify against a
75
+// locally provided digest, if applicable.
76
+func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, auth *RequestAuthorization) (remoteDigest digest.Digest, p []byte, err error) {
72 77
 	routeURL, err := getV2Builder(ep).BuildManifestURL(imageName, tagName)
73 78
 	if err != nil {
74
-		return nil, "", err
79
+		return "", nil, err
75 80
 	}
76 81
 
77 82
 	method := "GET"
... ...
@@ -79,31 +84,45 @@ func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, au
79 79
 
80 80
 	req, err := http.NewRequest(method, routeURL, nil)
81 81
 	if err != nil {
82
-		return nil, "", err
82
+		return "", nil, err
83 83
 	}
84
+
84 85
 	if err := auth.Authorize(req); err != nil {
85
-		return nil, "", err
86
+		return "", nil, err
86 87
 	}
88
+
87 89
 	res, err := r.client.Do(req)
88 90
 	if err != nil {
89
-		return nil, "", err
91
+		return "", nil, err
90 92
 	}
91 93
 	defer res.Body.Close()
94
+
92 95
 	if res.StatusCode != 200 {
93 96
 		if res.StatusCode == 401 {
94
-			return nil, "", errLoginRequired
97
+			return "", nil, errLoginRequired
95 98
 		} else if res.StatusCode == 404 {
96
-			return nil, "", ErrDoesNotExist
99
+			return "", nil, ErrDoesNotExist
97 100
 		}
98
-		return nil, "", httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch for %s:%s", res.StatusCode, imageName, tagName), res)
101
+		return "", nil, httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch for %s:%s", res.StatusCode, imageName, tagName), res)
99 102
 	}
100 103
 
101
-	manifestBytes, err := ioutil.ReadAll(res.Body)
104
+	p, err = ioutil.ReadAll(res.Body)
102 105
 	if err != nil {
103
-		return nil, "", fmt.Errorf("Error while reading the http response: %s", err)
106
+		return "", nil, fmt.Errorf("Error while reading the http response: %s", err)
104 107
 	}
105 108
 
106
-	return manifestBytes, res.Header.Get(DockerDigestHeader), nil
109
+	dgstHdr := res.Header.Get(DockerDigestHeader)
110
+	if dgstHdr != "" {
111
+		remoteDigest, err = digest.ParseDigest(dgstHdr)
112
+		if err != nil {
113
+			// NOTE(stevvooe): Including the remote digest is optional. We
114
+			// don't need to verify against it, but it is good practice.
115
+			remoteDigest = ""
116
+			logrus.Debugf("error parsing remote digest when fetching %v: %v", routeURL, err)
117
+		}
118
+	}
119
+
120
+	return
107 121
 }
108 122
 
109 123
 // - Succeeded to head image blob (already exists)
... ...
@@ -2,7 +2,6 @@ package digest
2 2
 
3 3
 import (
4 4
 	"bytes"
5
-	"crypto/sha256"
6 5
 	"fmt"
7 6
 	"hash"
8 7
 	"io"
... ...
@@ -16,6 +15,7 @@ import (
16 16
 const (
17 17
 	// DigestTarSumV1EmptyTar is the digest for the empty tar file.
18 18
 	DigestTarSumV1EmptyTar = "tarsum.v1+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
19
+
19 20
 	// DigestSha256EmptyTar is the canonical sha256 digest of empty data
20 21
 	DigestSha256EmptyTar = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
21 22
 )
... ...
@@ -39,7 +39,7 @@ const (
39 39
 type Digest string
40 40
 
41 41
 // NewDigest returns a Digest from alg and a hash.Hash object.
42
-func NewDigest(alg string, h hash.Hash) Digest {
42
+func NewDigest(alg Algorithm, h hash.Hash) Digest {
43 43
 	return Digest(fmt.Sprintf("%s:%x", alg, h.Sum(nil)))
44 44
 }
45 45
 
... ...
@@ -72,13 +72,13 @@ func ParseDigest(s string) (Digest, error) {
72 72
 
73 73
 // FromReader returns the most valid digest for the underlying content.
74 74
 func FromReader(rd io.Reader) (Digest, error) {
75
-	h := sha256.New()
75
+	digester := Canonical.New()
76 76
 
77
-	if _, err := io.Copy(h, rd); err != nil {
77
+	if _, err := io.Copy(digester.Hash(), rd); err != nil {
78 78
 		return "", err
79 79
 	}
80 80
 
81
-	return NewDigest("sha256", h), nil
81
+	return digester.Digest(), nil
82 82
 }
83 83
 
84 84
 // FromTarArchive produces a tarsum digest from reader rd.
... ...
@@ -131,8 +131,8 @@ func (d Digest) Validate() error {
131 131
 		return ErrDigestInvalidFormat
132 132
 	}
133 133
 
134
-	switch s[:i] {
135
-	case "sha256", "sha384", "sha512":
134
+	switch Algorithm(s[:i]) {
135
+	case SHA256, SHA384, SHA512:
136 136
 		break
137 137
 	default:
138 138
 		return ErrDigestUnsupported
... ...
@@ -143,8 +143,8 @@ func (d Digest) Validate() error {
143 143
 
144 144
 // Algorithm returns the algorithm portion of the digest. This will panic if
145 145
 // the underlying digest is not in a valid format.
146
-func (d Digest) Algorithm() string {
147
-	return string(d[:d.sepIndex()])
146
+func (d Digest) Algorithm() Algorithm {
147
+	return Algorithm(d[:d.sepIndex()])
148 148
 }
149 149
 
150 150
 // Hex returns the hex digest portion of the digest. This will panic if the
... ...
@@ -10,7 +10,7 @@ func TestParseDigest(t *testing.T) {
10 10
 	for _, testcase := range []struct {
11 11
 		input     string
12 12
 		err       error
13
-		algorithm string
13
+		algorithm Algorithm
14 14
 		hex       string
15 15
 	}{
16 16
 		{
... ...
@@ -1,44 +1,95 @@
1 1
 package digest
2 2
 
3 3
 import (
4
-	"crypto/sha256"
4
+	"crypto"
5 5
 	"hash"
6 6
 )
7 7
 
8
-// Digester calculates the digest of written data. It is functionally
9
-// equivalent to hash.Hash but provides methods for returning the Digest type
10
-// rather than raw bytes.
11
-type Digester struct {
12
-	alg  string
13
-	hash hash.Hash
8
+// Algorithm identifies and implementation of a digester by an identifier.
9
+// Note the that this defines both the hash algorithm used and the string
10
+// encoding.
11
+type Algorithm string
12
+
13
+// supported digest types
14
+const (
15
+	SHA256         Algorithm = "sha256"           // sha256 with hex encoding
16
+	SHA384         Algorithm = "sha384"           // sha384 with hex encoding
17
+	SHA512         Algorithm = "sha512"           // sha512 with hex encoding
18
+	TarsumV1SHA256 Algorithm = "tarsum+v1+sha256" // supported tarsum version, verification only
19
+
20
+	// Canonical is the primary digest algorithm used with the distribution
21
+	// project. Other digests may be used but this one is the primary storage
22
+	// digest.
23
+	Canonical = SHA256
24
+)
25
+
26
+var (
27
+	// TODO(stevvooe): Follow the pattern of the standard crypto package for
28
+	// registration of digests. Effectively, we are a registerable set and
29
+	// common symbol access.
30
+
31
+	// algorithms maps values to hash.Hash implementations. Other algorithms
32
+	// may be available but they cannot be calculated by the digest package.
33
+	algorithms = map[Algorithm]crypto.Hash{
34
+		SHA256: crypto.SHA256,
35
+		SHA384: crypto.SHA384,
36
+		SHA512: crypto.SHA512,
37
+	}
38
+)
39
+
40
+// Available returns true if the digest type is available for use. If this
41
+// returns false, New and Hash will return nil.
42
+func (a Algorithm) Available() bool {
43
+	h, ok := algorithms[a]
44
+	if !ok {
45
+		return false
46
+	}
47
+
48
+	// check availability of the hash, as well
49
+	return h.Available()
14 50
 }
15 51
 
16
-// NewDigester create a new Digester with the given hashing algorithm and instance
17
-// of that algo's hasher.
18
-func NewDigester(alg string, h hash.Hash) Digester {
19
-	return Digester{
20
-		alg:  alg,
21
-		hash: h,
52
+// New returns a new digester for the specified algorithm. If the algorithm
53
+// does not have a digester implementation, nil will be returned. This can be
54
+// checked by calling Available before calling New.
55
+func (a Algorithm) New() Digester {
56
+	return &digester{
57
+		alg:  a,
58
+		hash: a.Hash(),
22 59
 	}
23 60
 }
24 61
 
25
-// NewCanonicalDigester is a convenience function to create a new Digester with
26
-// out default settings.
27
-func NewCanonicalDigester() Digester {
28
-	return NewDigester("sha256", sha256.New())
62
+// Hash returns a new hash as used by the algorithm. If not available, nil is
63
+// returned. Make sure to check Available before calling.
64
+func (a Algorithm) Hash() hash.Hash {
65
+	if !a.Available() {
66
+		return nil
67
+	}
68
+
69
+	return algorithms[a].New()
29 70
 }
30 71
 
31
-// Write data to the digester. These writes cannot fail.
32
-func (d *Digester) Write(p []byte) (n int, err error) {
33
-	return d.hash.Write(p)
72
+// TODO(stevvooe): Allow resolution of verifiers using the digest type and
73
+// this registration system.
74
+
75
+// Digester calculates the digest of written data. Writes should go directly
76
+// to the return value of Hash, while calling Digest will return the current
77
+// value of the digest.
78
+type Digester interface {
79
+	Hash() hash.Hash // provides direct access to underlying hash instance.
80
+	Digest() Digest
34 81
 }
35 82
 
36
-// Digest returns the current digest for this digester.
37
-func (d *Digester) Digest() Digest {
38
-	return NewDigest(d.alg, d.hash)
83
+// digester provides a simple digester definition that embeds a hasher.
84
+type digester struct {
85
+	alg  Algorithm
86
+	hash hash.Hash
87
+}
88
+
89
+func (d *digester) Hash() hash.Hash {
90
+	return d.hash
39 91
 }
40 92
 
41
-// Reset the state of the digester.
42
-func (d *Digester) Reset() {
43
-	d.hash.Reset()
93
+func (d *digester) Digest() Digest {
94
+	return NewDigest(d.alg, d.hash)
44 95
 }
45 96
new file mode 100644
... ...
@@ -0,0 +1,195 @@
0
+package digest
1
+
2
+import (
3
+	"errors"
4
+	"sort"
5
+	"strings"
6
+)
7
+
8
+var (
9
+	// ErrDigestNotFound is used when a matching digest
10
+	// could not be found in a set.
11
+	ErrDigestNotFound = errors.New("digest not found")
12
+
13
+	// ErrDigestAmbiguous is used when multiple digests
14
+	// are found in a set. None of the matching digests
15
+	// should be considered valid matches.
16
+	ErrDigestAmbiguous = errors.New("ambiguous digest string")
17
+)
18
+
19
+// Set is used to hold a unique set of digests which
20
+// may be easily referenced by easily  referenced by a string
21
+// representation of the digest as well as short representation.
22
+// The uniqueness of the short representation is based on other
23
+// digests in the set. If digests are ommited from this set,
24
+// collisions in a larger set may not be detected, therefore it
25
+// is important to always do short representation lookups on
26
+// the complete set of digests. To mitigate collisions, an
27
+// appropriately long short code should be used.
28
+type Set struct {
29
+	entries digestEntries
30
+}
31
+
32
+// NewSet creates an empty set of digests
33
+// which may have digests added.
34
+func NewSet() *Set {
35
+	return &Set{
36
+		entries: digestEntries{},
37
+	}
38
+}
39
+
40
+// checkShortMatch checks whether two digests match as either whole
41
+// values or short values. This function does not test equality,
42
+// rather whether the second value could match against the first
43
+// value.
44
+func checkShortMatch(alg Algorithm, hex, shortAlg, shortHex string) bool {
45
+	if len(hex) == len(shortHex) {
46
+		if hex != shortHex {
47
+			return false
48
+		}
49
+		if len(shortAlg) > 0 && string(alg) != shortAlg {
50
+			return false
51
+		}
52
+	} else if !strings.HasPrefix(hex, shortHex) {
53
+		return false
54
+	} else if len(shortAlg) > 0 && string(alg) != shortAlg {
55
+		return false
56
+	}
57
+	return true
58
+}
59
+
60
+// Lookup looks for a digest matching the given string representation.
61
+// If no digests could be found ErrDigestNotFound will be returned
62
+// with an empty digest value. If multiple matches are found
63
+// ErrDigestAmbiguous will be returned with an empty digest value.
64
+func (dst *Set) Lookup(d string) (Digest, error) {
65
+	if len(dst.entries) == 0 {
66
+		return "", ErrDigestNotFound
67
+	}
68
+	var (
69
+		searchFunc func(int) bool
70
+		alg        Algorithm
71
+		hex        string
72
+	)
73
+	dgst, err := ParseDigest(d)
74
+	if err == ErrDigestInvalidFormat {
75
+		hex = d
76
+		searchFunc = func(i int) bool {
77
+			return dst.entries[i].val >= d
78
+		}
79
+	} else {
80
+		hex = dgst.Hex()
81
+		alg = dgst.Algorithm()
82
+		searchFunc = func(i int) bool {
83
+			if dst.entries[i].val == hex {
84
+				return dst.entries[i].alg >= alg
85
+			}
86
+			return dst.entries[i].val >= hex
87
+		}
88
+	}
89
+	idx := sort.Search(len(dst.entries), searchFunc)
90
+	if idx == len(dst.entries) || !checkShortMatch(dst.entries[idx].alg, dst.entries[idx].val, string(alg), hex) {
91
+		return "", ErrDigestNotFound
92
+	}
93
+	if dst.entries[idx].alg == alg && dst.entries[idx].val == hex {
94
+		return dst.entries[idx].digest, nil
95
+	}
96
+	if idx+1 < len(dst.entries) && checkShortMatch(dst.entries[idx+1].alg, dst.entries[idx+1].val, string(alg), hex) {
97
+		return "", ErrDigestAmbiguous
98
+	}
99
+
100
+	return dst.entries[idx].digest, nil
101
+}
102
+
103
+// Add adds the given digests to the set. An error will be returned
104
+// if the given digest is invalid. If the digest already exists in the
105
+// table, this operation will be a no-op.
106
+func (dst *Set) Add(d Digest) error {
107
+	if err := d.Validate(); err != nil {
108
+		return err
109
+	}
110
+	entry := &digestEntry{alg: d.Algorithm(), val: d.Hex(), digest: d}
111
+	searchFunc := func(i int) bool {
112
+		if dst.entries[i].val == entry.val {
113
+			return dst.entries[i].alg >= entry.alg
114
+		}
115
+		return dst.entries[i].val >= entry.val
116
+	}
117
+	idx := sort.Search(len(dst.entries), searchFunc)
118
+	if idx == len(dst.entries) {
119
+		dst.entries = append(dst.entries, entry)
120
+		return nil
121
+	} else if dst.entries[idx].digest == d {
122
+		return nil
123
+	}
124
+
125
+	entries := append(dst.entries, nil)
126
+	copy(entries[idx+1:], entries[idx:len(entries)-1])
127
+	entries[idx] = entry
128
+	dst.entries = entries
129
+	return nil
130
+}
131
+
132
+// ShortCodeTable returns a map of Digest to unique short codes. The
133
+// length represents the minimum value, the maximum length may be the
134
+// entire value of digest if uniqueness cannot be achieved without the
135
+// full value. This function will attempt to make short codes as short
136
+// as possible to be unique.
137
+func ShortCodeTable(dst *Set, length int) map[Digest]string {
138
+	m := make(map[Digest]string, len(dst.entries))
139
+	l := length
140
+	resetIdx := 0
141
+	for i := 0; i < len(dst.entries); i++ {
142
+		var short string
143
+		extended := true
144
+		for extended {
145
+			extended = false
146
+			if len(dst.entries[i].val) <= l {
147
+				short = dst.entries[i].digest.String()
148
+			} else {
149
+				short = dst.entries[i].val[:l]
150
+				for j := i + 1; j < len(dst.entries); j++ {
151
+					if checkShortMatch(dst.entries[j].alg, dst.entries[j].val, "", short) {
152
+						if j > resetIdx {
153
+							resetIdx = j
154
+						}
155
+						extended = true
156
+					} else {
157
+						break
158
+					}
159
+				}
160
+				if extended {
161
+					l++
162
+				}
163
+			}
164
+		}
165
+		m[dst.entries[i].digest] = short
166
+		if i >= resetIdx {
167
+			l = length
168
+		}
169
+	}
170
+	return m
171
+}
172
+
173
+type digestEntry struct {
174
+	alg    Algorithm
175
+	val    string
176
+	digest Digest
177
+}
178
+
179
+type digestEntries []*digestEntry
180
+
181
+func (d digestEntries) Len() int {
182
+	return len(d)
183
+}
184
+
185
+func (d digestEntries) Less(i, j int) bool {
186
+	if d[i].val != d[j].val {
187
+		return d[i].val < d[j].val
188
+	}
189
+	return d[i].alg < d[j].alg
190
+}
191
+
192
+func (d digestEntries) Swap(i, j int) {
193
+	d[i], d[j] = d[j], d[i]
194
+}
0 195
new file mode 100644
... ...
@@ -0,0 +1,272 @@
0
+package digest
1
+
2
+import (
3
+	"crypto/sha256"
4
+	"encoding/binary"
5
+	"math/rand"
6
+	"testing"
7
+)
8
+
9
+func assertEqualDigests(t *testing.T, d1, d2 Digest) {
10
+	if d1 != d2 {
11
+		t.Fatalf("Digests do not match:\n\tActual: %s\n\tExpected: %s", d1, d2)
12
+	}
13
+}
14
+
15
+func TestLookup(t *testing.T) {
16
+	digests := []Digest{
17
+		"sha256:12345",
18
+		"sha256:1234",
19
+		"sha256:12346",
20
+		"sha256:54321",
21
+		"sha256:65431",
22
+		"sha256:64321",
23
+		"sha256:65421",
24
+		"sha256:65321",
25
+	}
26
+
27
+	dset := NewSet()
28
+	for i := range digests {
29
+		if err := dset.Add(digests[i]); err != nil {
30
+			t.Fatal(err)
31
+		}
32
+	}
33
+
34
+	dgst, err := dset.Lookup("54")
35
+	if err != nil {
36
+		t.Fatal(err)
37
+	}
38
+	assertEqualDigests(t, dgst, digests[3])
39
+
40
+	dgst, err = dset.Lookup("1234")
41
+	if err == nil {
42
+		t.Fatal("Expected ambiguous error looking up: 1234")
43
+	}
44
+	if err != ErrDigestAmbiguous {
45
+		t.Fatal(err)
46
+	}
47
+
48
+	dgst, err = dset.Lookup("9876")
49
+	if err == nil {
50
+		t.Fatal("Expected ambiguous error looking up: 9876")
51
+	}
52
+	if err != ErrDigestNotFound {
53
+		t.Fatal(err)
54
+	}
55
+
56
+	dgst, err = dset.Lookup("sha256:1234")
57
+	if err != nil {
58
+		t.Fatal(err)
59
+	}
60
+	assertEqualDigests(t, dgst, digests[1])
61
+
62
+	dgst, err = dset.Lookup("sha256:12345")
63
+	if err != nil {
64
+		t.Fatal(err)
65
+	}
66
+	assertEqualDigests(t, dgst, digests[0])
67
+
68
+	dgst, err = dset.Lookup("sha256:12346")
69
+	if err != nil {
70
+		t.Fatal(err)
71
+	}
72
+	assertEqualDigests(t, dgst, digests[2])
73
+
74
+	dgst, err = dset.Lookup("12346")
75
+	if err != nil {
76
+		t.Fatal(err)
77
+	}
78
+	assertEqualDigests(t, dgst, digests[2])
79
+
80
+	dgst, err = dset.Lookup("12345")
81
+	if err != nil {
82
+		t.Fatal(err)
83
+	}
84
+	assertEqualDigests(t, dgst, digests[0])
85
+}
86
+
87
+func TestAddDuplication(t *testing.T) {
88
+	digests := []Digest{
89
+		"sha256:1234",
90
+		"sha256:12345",
91
+		"sha256:12346",
92
+		"sha256:54321",
93
+		"sha256:65431",
94
+		"sha512:65431",
95
+		"sha512:65421",
96
+		"sha512:65321",
97
+	}
98
+
99
+	dset := NewSet()
100
+	for i := range digests {
101
+		if err := dset.Add(digests[i]); err != nil {
102
+			t.Fatal(err)
103
+		}
104
+	}
105
+
106
+	if len(dset.entries) != 8 {
107
+		t.Fatal("Invalid dset size")
108
+	}
109
+
110
+	if err := dset.Add(Digest("sha256:12345")); err != nil {
111
+		t.Fatal(err)
112
+	}
113
+
114
+	if len(dset.entries) != 8 {
115
+		t.Fatal("Duplicate digest insert allowed")
116
+	}
117
+
118
+	if err := dset.Add(Digest("sha384:12345")); err != nil {
119
+		t.Fatal(err)
120
+	}
121
+
122
+	if len(dset.entries) != 9 {
123
+		t.Fatal("Insert with different algorithm not allowed")
124
+	}
125
+}
126
+
127
+func assertEqualShort(t *testing.T, actual, expected string) {
128
+	if actual != expected {
129
+		t.Fatalf("Unexpected short value:\n\tExpected: %s\n\tActual: %s", expected, actual)
130
+	}
131
+}
132
+
133
+func TestShortCodeTable(t *testing.T) {
134
+	digests := []Digest{
135
+		"sha256:1234",
136
+		"sha256:12345",
137
+		"sha256:12346",
138
+		"sha256:54321",
139
+		"sha256:65431",
140
+		"sha256:64321",
141
+		"sha256:65421",
142
+		"sha256:65321",
143
+	}
144
+
145
+	dset := NewSet()
146
+	for i := range digests {
147
+		if err := dset.Add(digests[i]); err != nil {
148
+			t.Fatal(err)
149
+		}
150
+	}
151
+
152
+	dump := ShortCodeTable(dset, 2)
153
+
154
+	if len(dump) < len(digests) {
155
+		t.Fatalf("Error unexpected size: %d, expecting %d", len(dump), len(digests))
156
+	}
157
+
158
+	assertEqualShort(t, dump[digests[0]], "sha256:1234")
159
+	assertEqualShort(t, dump[digests[1]], "sha256:12345")
160
+	assertEqualShort(t, dump[digests[2]], "sha256:12346")
161
+	assertEqualShort(t, dump[digests[3]], "54")
162
+	assertEqualShort(t, dump[digests[4]], "6543")
163
+	assertEqualShort(t, dump[digests[5]], "64")
164
+	assertEqualShort(t, dump[digests[6]], "6542")
165
+	assertEqualShort(t, dump[digests[7]], "653")
166
+}
167
+
168
+func createDigests(count int) ([]Digest, error) {
169
+	r := rand.New(rand.NewSource(25823))
170
+	digests := make([]Digest, count)
171
+	for i := range digests {
172
+		h := sha256.New()
173
+		if err := binary.Write(h, binary.BigEndian, r.Int63()); err != nil {
174
+			return nil, err
175
+		}
176
+		digests[i] = NewDigest("sha256", h)
177
+	}
178
+	return digests, nil
179
+}
180
+
181
+func benchAddNTable(b *testing.B, n int) {
182
+	digests, err := createDigests(n)
183
+	if err != nil {
184
+		b.Fatal(err)
185
+	}
186
+	b.ResetTimer()
187
+	for i := 0; i < b.N; i++ {
188
+		dset := &Set{entries: digestEntries(make([]*digestEntry, 0, n))}
189
+		for j := range digests {
190
+			if err = dset.Add(digests[j]); err != nil {
191
+				b.Fatal(err)
192
+			}
193
+		}
194
+	}
195
+}
196
+
197
+func benchLookupNTable(b *testing.B, n int, shortLen int) {
198
+	digests, err := createDigests(n)
199
+	if err != nil {
200
+		b.Fatal(err)
201
+	}
202
+	dset := &Set{entries: digestEntries(make([]*digestEntry, 0, n))}
203
+	for i := range digests {
204
+		if err := dset.Add(digests[i]); err != nil {
205
+			b.Fatal(err)
206
+		}
207
+	}
208
+	shorts := make([]string, 0, n)
209
+	for _, short := range ShortCodeTable(dset, shortLen) {
210
+		shorts = append(shorts, short)
211
+	}
212
+
213
+	b.ResetTimer()
214
+	for i := 0; i < b.N; i++ {
215
+		if _, err = dset.Lookup(shorts[i%n]); err != nil {
216
+			b.Fatal(err)
217
+		}
218
+	}
219
+}
220
+
221
+func benchShortCodeNTable(b *testing.B, n int, shortLen int) {
222
+	digests, err := createDigests(n)
223
+	if err != nil {
224
+		b.Fatal(err)
225
+	}
226
+	dset := &Set{entries: digestEntries(make([]*digestEntry, 0, n))}
227
+	for i := range digests {
228
+		if err := dset.Add(digests[i]); err != nil {
229
+			b.Fatal(err)
230
+		}
231
+	}
232
+
233
+	b.ResetTimer()
234
+	for i := 0; i < b.N; i++ {
235
+		ShortCodeTable(dset, shortLen)
236
+	}
237
+}
238
+
239
+func BenchmarkAdd10(b *testing.B) {
240
+	benchAddNTable(b, 10)
241
+}
242
+
243
+func BenchmarkAdd100(b *testing.B) {
244
+	benchAddNTable(b, 100)
245
+}
246
+
247
+func BenchmarkAdd1000(b *testing.B) {
248
+	benchAddNTable(b, 1000)
249
+}
250
+
251
+func BenchmarkLookup10(b *testing.B) {
252
+	benchLookupNTable(b, 10, 12)
253
+}
254
+
255
+func BenchmarkLookup100(b *testing.B) {
256
+	benchLookupNTable(b, 100, 12)
257
+}
258
+
259
+func BenchmarkLookup1000(b *testing.B) {
260
+	benchLookupNTable(b, 1000, 12)
261
+}
262
+
263
+func BenchmarkShortCode10(b *testing.B) {
264
+	benchShortCodeNTable(b, 10, 12)
265
+}
266
+func BenchmarkShortCode100(b *testing.B) {
267
+	benchShortCodeNTable(b, 100, 12)
268
+}
269
+func BenchmarkShortCode1000(b *testing.B) {
270
+	benchShortCodeNTable(b, 1000, 12)
271
+}
... ...
@@ -6,10 +6,10 @@ import (
6 6
 	"regexp"
7 7
 )
8 8
 
9
-// TarSumRegexp defines a reguler expression to match tarsum identifiers.
9
+// TarSumRegexp defines a regular expression to match tarsum identifiers.
10 10
 var TarsumRegexp = regexp.MustCompile("tarsum(?:.[a-z0-9]+)?\\+[a-zA-Z0-9]+:[A-Fa-f0-9]+")
11 11
 
12
-// TarsumRegexpCapturing defines a reguler expression to match tarsum identifiers with
12
+// TarsumRegexpCapturing defines a regular expression to match tarsum identifiers with
13 13
 // capture groups corresponding to each component.
14 14
 var TarsumRegexpCapturing = regexp.MustCompile("(tarsum)(.([a-z0-9]+))?\\+([a-zA-Z0-9]+):([A-Fa-f0-9]+)")
15 15
 
... ...
@@ -1,8 +1,6 @@
1 1
 package digest
2 2
 
3 3
 import (
4
-	"crypto/sha256"
5
-	"crypto/sha512"
6 4
 	"hash"
7 5
 	"io"
8 6
 	"io/ioutil"
... ...
@@ -33,7 +31,7 @@ func NewDigestVerifier(d Digest) (Verifier, error) {
33 33
 	switch alg {
34 34
 	case "sha256", "sha384", "sha512":
35 35
 		return hashVerifier{
36
-			hash:   newHash(alg),
36
+			hash:   alg.Hash(),
37 37
 			digest: d,
38 38
 		}, nil
39 39
 	default:
... ...
@@ -95,19 +93,6 @@ func (lv *lengthVerifier) Verified() bool {
95 95
 	return lv.expected == lv.len
96 96
 }
97 97
 
98
-func newHash(name string) hash.Hash {
99
-	switch name {
100
-	case "sha256":
101
-		return sha256.New()
102
-	case "sha384":
103
-		return sha512.New384()
104
-	case "sha512":
105
-		return sha512.New()
106
-	default:
107
-		panic("unsupport algorithm: " + name)
108
-	}
109
-}
110
-
111 98
 type hashVerifier struct {
112 99
 	digest Digest
113 100
 	hash   hash.Hash
... ...
@@ -80,7 +80,7 @@ func TestVerifierUnsupportedDigest(t *testing.T) {
80 80
 	}
81 81
 
82 82
 	if err != ErrDigestUnsupported {
83
-		t.Fatalf("incorrect error for unsupported digest: %v %p %p", err, ErrDigestUnsupported, err)
83
+		t.Fatalf("incorrect error for unsupported digest: %v", err)
84 84
 	}
85 85
 }
86 86
 
... ...
@@ -28,7 +28,7 @@ var (
28 28
 		Name:        "uuid",
29 29
 		Type:        "opaque",
30 30
 		Required:    true,
31
-		Description: `A uuid identifying the upload. This field can accept almost anything.`,
31
+		Description: "A uuid identifying the upload. This field can accept characters that match `[a-zA-Z0-9-_.=]+`.",
32 32
 	}
33 33
 
34 34
 	digestPathParameter = ParameterDescriptor{
... ...
@@ -135,7 +135,7 @@ const (
135 135
    "tag": <tag>,
136 136
    "fsLayers": [
137 137
       {
138
-         "blobSum": <tarsum>
138
+         "blobSum": "<digest>"
139 139
       },
140 140
       ...
141 141
     ]
... ...
@@ -606,7 +606,7 @@ var routeDescriptors = []RouteDescriptor{
606 606
             "code": "BLOB_UNKNOWN",
607 607
             "message": "blob unknown to registry",
608 608
             "detail": {
609
-                "digest": <tarsum>
609
+                "digest": "<digest>"
610 610
             }
611 611
         },
612 612
         ...
... ...
@@ -712,7 +712,7 @@ var routeDescriptors = []RouteDescriptor{
712 712
 		Name:        RouteNameBlob,
713 713
 		Path:        "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/{digest:" + digest.DigestRegexp.String() + "}",
714 714
 		Entity:      "Blob",
715
-		Description: "Fetch the blob identified by `name` and `digest`. Used to fetch layers by tarsum digest.",
715
+		Description: "Fetch the blob identified by `name` and `digest`. Used to fetch layers by digest.",
716 716
 		Methods: []MethodDescriptor{
717 717
 
718 718
 			{
... ...
@@ -898,7 +898,7 @@ var routeDescriptors = []RouteDescriptor{
898 898
 							{
899 899
 								Name:        "digest",
900 900
 								Type:        "query",
901
-								Format:      "<tarsum>",
901
+								Format:      "<digest>",
902 902
 								Regexp:      digest.DigestRegexp,
903 903
 								Description: `Digest of uploaded blob. If present, the upload will be completed, in a single request, with contents of the request body as the resulting blob.`,
904 904
 							},
... ...
@@ -985,7 +985,7 @@ var routeDescriptors = []RouteDescriptor{
985 985
 
986 986
 	{
987 987
 		Name:        RouteNameBlobUploadChunk,
988
-		Path:        "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid}",
988
+		Path:        "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid:[a-zA-Z0-9-_.=]+}",
989 989
 		Entity:      "Blob Upload",
990 990
 		Description: "Interact with blob uploads. Clients should never assemble URLs for this endpoint and should only take it through the `Location` header on related API requests. The `Location` header and its parameters should be preserved by clients, using the latest value returned via upload related API calls.",
991 991
 		Methods: []MethodDescriptor{
... ...
@@ -1055,7 +1055,74 @@ var routeDescriptors = []RouteDescriptor{
1055 1055
 				Description: "Upload a chunk of data for the specified upload.",
1056 1056
 				Requests: []RequestDescriptor{
1057 1057
 					{
1058
-						Description: "Upload a chunk of data to specified upload without completing the upload.",
1058
+						Name:        "Stream upload",
1059
+						Description: "Upload a stream of data to upload without completing the upload.",
1060
+						PathParameters: []ParameterDescriptor{
1061
+							nameParameterDescriptor,
1062
+							uuidParameterDescriptor,
1063
+						},
1064
+						Headers: []ParameterDescriptor{
1065
+							hostHeader,
1066
+							authHeader,
1067
+						},
1068
+						Body: BodyDescriptor{
1069
+							ContentType: "application/octet-stream",
1070
+							Format:      "<binary data>",
1071
+						},
1072
+						Successes: []ResponseDescriptor{
1073
+							{
1074
+								Name:        "Data Accepted",
1075
+								Description: "The stream of data has been accepted and the current progress is available in the range header. The updated upload location is available in the `Location` header.",
1076
+								StatusCode:  http.StatusNoContent,
1077
+								Headers: []ParameterDescriptor{
1078
+									{
1079
+										Name:        "Location",
1080
+										Type:        "url",
1081
+										Format:      "/v2/<name>/blobs/uploads/<uuid>",
1082
+										Description: "The location of the upload. Clients should assume this changes after each request. Clients should use the contents verbatim to complete the upload, adding parameters where required.",
1083
+									},
1084
+									{
1085
+										Name:        "Range",
1086
+										Type:        "header",
1087
+										Format:      "0-<offset>",
1088
+										Description: "Range indicating the current progress of the upload.",
1089
+									},
1090
+									contentLengthZeroHeader,
1091
+									dockerUploadUUIDHeader,
1092
+								},
1093
+							},
1094
+						},
1095
+						Failures: []ResponseDescriptor{
1096
+							{
1097
+								Description: "There was an error processing the upload and it must be restarted.",
1098
+								StatusCode:  http.StatusBadRequest,
1099
+								ErrorCodes: []ErrorCode{
1100
+									ErrorCodeDigestInvalid,
1101
+									ErrorCodeNameInvalid,
1102
+									ErrorCodeBlobUploadInvalid,
1103
+								},
1104
+								Body: BodyDescriptor{
1105
+									ContentType: "application/json; charset=utf-8",
1106
+									Format:      errorsBody,
1107
+								},
1108
+							},
1109
+							unauthorizedResponsePush,
1110
+							{
1111
+								Description: "The upload is unknown to the registry. The upload must be restarted.",
1112
+								StatusCode:  http.StatusNotFound,
1113
+								ErrorCodes: []ErrorCode{
1114
+									ErrorCodeBlobUploadUnknown,
1115
+								},
1116
+								Body: BodyDescriptor{
1117
+									ContentType: "application/json; charset=utf-8",
1118
+									Format:      errorsBody,
1119
+								},
1120
+							},
1121
+						},
1122
+					},
1123
+					{
1124
+						Name:        "Chunked upload",
1125
+						Description: "Upload a chunk of data to specified upload without completing the upload. The data will be uploaded to the specified Content Range.",
1059 1126
 						PathParameters: []ParameterDescriptor{
1060 1127
 							nameParameterDescriptor,
1061 1128
 							uuidParameterDescriptor,
... ...
@@ -1143,26 +1210,15 @@ var routeDescriptors = []RouteDescriptor{
1143 1143
 				Description: "Complete the upload specified by `uuid`, optionally appending the body as the final chunk.",
1144 1144
 				Requests: []RequestDescriptor{
1145 1145
 					{
1146
-						// TODO(stevvooe): Break this down into three separate requests:
1147
-						// 	1. Complete an upload where all data has already been sent.
1148
-						// 	2. Complete an upload where the entire body is in the PUT.
1149
-						// 	3. Complete an upload where the final, partial chunk is the body.
1150
-
1151
-						Description: "Complete the upload, providing the _final_ chunk of data, if necessary. This method may take a body with all the data. If the `Content-Range` header is specified, it may include the final chunk. A request without a body will just complete the upload with previously uploaded content.",
1146
+						Description: "Complete the upload, providing all the data in the body, if necessary. A request without a body will just complete the upload with previously uploaded content.",
1152 1147
 						Headers: []ParameterDescriptor{
1153 1148
 							hostHeader,
1154 1149
 							authHeader,
1155 1150
 							{
1156
-								Name:        "Content-Range",
1157
-								Type:        "header",
1158
-								Format:      "<start of range>-<end of range, inclusive>",
1159
-								Description: "Range of bytes identifying the block of content represented by the body. Start must the end offset retrieved via status check plus one. Note that this is a non-standard use of the `Content-Range` header. May be omitted if no data is provided.",
1160
-							},
1161
-							{
1162 1151
 								Name:        "Content-Length",
1163 1152
 								Type:        "integer",
1164
-								Format:      "<length of chunk>",
1165
-								Description: "Length of the chunk being uploaded, corresponding to the length of the request body. May be zero if no data is provided.",
1153
+								Format:      "<length of data>",
1154
+								Description: "Length of the data being uploaded, corresponding to the length of the request body. May be zero if no data is provided.",
1166 1155
 							},
1167 1156
 						},
1168 1157
 						PathParameters: []ParameterDescriptor{
... ...
@@ -1173,7 +1229,7 @@ var routeDescriptors = []RouteDescriptor{
1173 1173
 							{
1174 1174
 								Name:        "digest",
1175 1175
 								Type:        "string",
1176
-								Format:      "<tarsum>",
1176
+								Format:      "<digest>",
1177 1177
 								Regexp:      digest.DigestRegexp,
1178 1178
 								Required:    true,
1179 1179
 								Description: `Digest of uploaded blob.`,
... ...
@@ -1181,7 +1237,7 @@ var routeDescriptors = []RouteDescriptor{
1181 1181
 						},
1182 1182
 						Body: BodyDescriptor{
1183 1183
 							ContentType: "application/octet-stream",
1184
-							Format:      "<binary chunk>",
1184
+							Format:      "<binary data>",
1185 1185
 						},
1186 1186
 						Successes: []ResponseDescriptor{
1187 1187
 							{
... ...
@@ -1190,9 +1246,10 @@ var routeDescriptors = []RouteDescriptor{
1190 1190
 								StatusCode:  http.StatusNoContent,
1191 1191
 								Headers: []ParameterDescriptor{
1192 1192
 									{
1193
-										Name:   "Location",
1194
-										Type:   "url",
1195
-										Format: "<blob location>",
1193
+										Name:        "Location",
1194
+										Type:        "url",
1195
+										Format:      "<blob location>",
1196
+										Description: "The canonical location of the blob for retrieval",
1196 1197
 									},
1197 1198
 									{
1198 1199
 										Name:        "Content-Range",
... ...
@@ -1200,12 +1257,7 @@ var routeDescriptors = []RouteDescriptor{
1200 1200
 										Format:      "<start of range>-<end of range, inclusive>",
1201 1201
 										Description: "Range of bytes identifying the desired block of content represented by the body. Start must match the end of offset retrieved via status check. Note that this is a non-standard use of the `Content-Range` header.",
1202 1202
 									},
1203
-									{
1204
-										Name:        "Content-Length",
1205
-										Type:        "integer",
1206
-										Format:      "<length of chunk>",
1207
-										Description: "Length of the chunk being uploaded, corresponding the length of the request body.",
1208
-									},
1203
+									contentLengthZeroHeader,
1209 1204
 									digestHeader,
1210 1205
 								},
1211 1206
 							},
... ...
@@ -1236,24 +1288,6 @@ var routeDescriptors = []RouteDescriptor{
1236 1236
 									Format:      errorsBody,
1237 1237
 								},
1238 1238
 							},
1239
-							{
1240
-								Description: "The `Content-Range` specification cannot be accepted, either because it does not overlap with the current progress or it is invalid. The contents of the `Range` header may be used to resolve the condition.",
1241
-								StatusCode:  http.StatusRequestedRangeNotSatisfiable,
1242
-								Headers: []ParameterDescriptor{
1243
-									{
1244
-										Name:        "Location",
1245
-										Type:        "url",
1246
-										Format:      "/v2/<name>/blobs/uploads/<uuid>",
1247
-										Description: "The location of the upload. Clients should assume this changes after each request. Clients should use the contents verbatim to complete the upload, adding parameters where required.",
1248
-									},
1249
-									{
1250
-										Name:        "Range",
1251
-										Type:        "header",
1252
-										Format:      "0-<offset>",
1253
-										Description: "Range indicating the current progress of the upload.",
1254
-									},
1255
-								},
1256
-							},
1257 1239
 						},
1258 1240
 					},
1259 1241
 				},
... ...
@@ -46,7 +46,7 @@ var (
46 46
 	// ErrRepositoryNameComponentShort is returned when a repository name
47 47
 	// contains a component which is shorter than
48 48
 	// RepositoryNameComponentMinLength
49
-	ErrRepositoryNameComponentShort = fmt.Errorf("respository name component must be %v or more characters", RepositoryNameComponentMinLength)
49
+	ErrRepositoryNameComponentShort = fmt.Errorf("repository name component must be %v or more characters", RepositoryNameComponentMinLength)
50 50
 
51 51
 	// ErrRepositoryNameMissingComponents is returned when a repository name
52 52
 	// contains fewer than RepositoryNameMinComponents components
... ...
@@ -61,7 +61,7 @@ var (
61 61
 	ErrRepositoryNameComponentInvalid = fmt.Errorf("repository name component must match %q", RepositoryNameComponentRegexp.String())
62 62
 )
63 63
 
64
-// ValidateRespositoryName ensures the repository name is valid for use in the
64
+// ValidateRepositoryName ensures the repository name is valid for use in the
65 65
 // registry. This function accepts a superset of what might be accepted by
66 66
 // docker core or docker hub. If the name does not pass validation, an error,
67 67
 // describing the conditions, is returned.
... ...
@@ -75,7 +75,7 @@ var (
75 75
 //
76 76
 // The result of the production, known as the "namespace", should be limited
77 77
 // to 255 characters.
78
-func ValidateRespositoryName(name string) error {
78
+func ValidateRepositoryName(name string) error {
79 79
 	if len(name) > RepositoryNameTotalLengthMax {
80 80
 		return ErrRepositoryNameLong
81 81
 	}
... ...
@@ -80,7 +80,7 @@ func TestRepositoryNameRegexp(t *testing.T) {
80 80
 			t.Fail()
81 81
 		}
82 82
 
83
-		if err := ValidateRespositoryName(testcase.input); err != testcase.err {
83
+		if err := ValidateRepositoryName(testcase.input); err != testcase.err {
84 84
 			if testcase.err != nil {
85 85
 				if err != nil {
86 86
 					failf("unexpected error for invalid repository: got %v, expected %v", err, testcase.err)
... ...
@@ -98,6 +98,7 @@ func TestRouter(t *testing.T) {
98 98
 			},
99 99
 		},
100 100
 		{
101
+			// support uuid proper
101 102
 			RouteName:  RouteNameBlobUploadChunk,
102 103
 			RequestURI: "/v2/foo/bar/blobs/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
103 104
 			Vars: map[string]string{
... ...
@@ -114,6 +115,21 @@ func TestRouter(t *testing.T) {
114 114
 			},
115 115
 		},
116 116
 		{
117
+			// supports urlsafe base64
118
+			RouteName:  RouteNameBlobUploadChunk,
119
+			RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA_-==",
120
+			Vars: map[string]string{
121
+				"name": "foo/bar",
122
+				"uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA_-==",
123
+			},
124
+		},
125
+		{
126
+			// does not match
127
+			RouteName:  RouteNameBlobUploadChunk,
128
+			RequestURI: "/v2/foo/bar/blobs/uploads/totalandcompletejunk++$$-==",
129
+			StatusCode: http.StatusNotFound,
130
+		},
131
+		{
117 132
 			// Check ambiguity: ensure we can distinguish between tags for
118 133
 			// "foo/bar/image/image" and image for "foo/bar/image" with tag
119 134
 			// "tags"
... ...
@@ -62,7 +62,12 @@ func NewURLBuilderFromRequest(r *http.Request) *URLBuilder {
62 62
 	host := r.Host
63 63
 	forwardedHost := r.Header.Get("X-Forwarded-Host")
64 64
 	if len(forwardedHost) > 0 {
65
-		host = forwardedHost
65
+		// According to the Apache mod_proxy docs, X-Forwarded-Host can be a
66
+		// comma-separated list of hosts, to which each proxy appends the
67
+		// requested host. We want to grab the first from this comma-separated
68
+		// list.
69
+		hosts := strings.SplitN(forwardedHost, ",", 2)
70
+		host = strings.TrimSpace(hosts[0])
66 71
 	}
67 72
 
68 73
 	basePath := routeDescriptorsMap[RouteNameBase].Path
... ...
@@ -151,6 +151,12 @@ func TestBuilderFromRequest(t *testing.T) {
151 151
 	forwardedProtoHeader := make(http.Header, 1)
152 152
 	forwardedProtoHeader.Set("X-Forwarded-Proto", "https")
153 153
 
154
+	forwardedHostHeader1 := make(http.Header, 1)
155
+	forwardedHostHeader1.Set("X-Forwarded-Host", "first.example.com")
156
+
157
+	forwardedHostHeader2 := make(http.Header, 1)
158
+	forwardedHostHeader2.Set("X-Forwarded-Host", "first.example.com, proxy1.example.com")
159
+
154 160
 	testRequests := []struct {
155 161
 		request *http.Request
156 162
 		base    string
... ...
@@ -163,6 +169,14 @@ func TestBuilderFromRequest(t *testing.T) {
163 163
 			request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader},
164 164
 			base:    "https://example.com",
165 165
 		},
166
+		{
167
+			request: &http.Request{URL: u, Host: u.Host, Header: forwardedHostHeader1},
168
+			base:    "http://first.example.com",
169
+		},
170
+		{
171
+			request: &http.Request{URL: u, Host: u.Host, Header: forwardedHostHeader2},
172
+			base:    "http://first.example.com",
173
+		},
166 174
 	}
167 175
 
168 176
 	for _, tr := range testRequests {