Properly verify manifests and layer digests on pull
| ... | ... |
@@ -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] = ®istry.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 |
| ... | ... |
@@ -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 {
|