We noticed a regression since the 1.7.1 patch after some refactoring. This
patch corrects the behavior and adds integration tests for modified manifest
and rootfs layer blobs.
Docker-DCO-1.1-Signed-off-by: Josh Hawn <josh.hawn@docker.com> (github: jlhawn)
| ... | ... |
@@ -102,13 +102,12 @@ func (p *v2Puller) pullV2Repository(tag string) (err error) {
|
| 102 | 102 |
|
| 103 | 103 |
// downloadInfo is used to pass information from download to extractor |
| 104 | 104 |
type downloadInfo struct {
|
| 105 |
- img *image.Image |
|
| 106 |
- tmpFile *os.File |
|
| 107 |
- digest digest.Digest |
|
| 108 |
- layer distribution.ReadSeekCloser |
|
| 109 |
- size int64 |
|
| 110 |
- err chan error |
|
| 111 |
- verified bool |
|
| 105 |
+ img *image.Image |
|
| 106 |
+ tmpFile *os.File |
|
| 107 |
+ digest digest.Digest |
|
| 108 |
+ layer distribution.ReadSeekCloser |
|
| 109 |
+ size int64 |
|
| 110 |
+ err chan error |
|
| 112 | 111 |
} |
| 113 | 112 |
|
| 114 | 113 |
type errVerification struct{}
|
| ... | ... |
@@ -176,9 +175,11 @@ func (p *v2Puller) download(di *downloadInfo) {
|
| 176 | 176 |
|
| 177 | 177 |
out.Write(p.sf.FormatProgress(stringid.TruncateID(di.img.ID), "Verifying Checksum", nil)) |
| 178 | 178 |
|
| 179 |
- di.verified = verifier.Verified() |
|
| 180 |
- if !di.verified {
|
|
| 181 |
- logrus.Infof("Image verification failed for layer %s", di.digest)
|
|
| 179 |
+ if !verifier.Verified() {
|
|
| 180 |
+ err = fmt.Errorf("filesystem layer verification failed for digest %s", di.digest)
|
|
| 181 |
+ logrus.Error(err) |
|
| 182 |
+ di.err <- err |
|
| 183 |
+ return |
|
| 182 | 184 |
} |
| 183 | 185 |
|
| 184 | 186 |
out.Write(p.sf.FormatProgress(stringid.TruncateID(di.img.ID), "Download complete", nil)) |
| ... | ... |
@@ -252,7 +253,6 @@ func (p *v2Puller) pullV2Tag(tag, taggedName string) (bool, error) {
|
| 252 | 252 |
return false, err |
| 253 | 253 |
} |
| 254 | 254 |
} |
| 255 |
- verified = verified && d.verified |
|
| 256 | 255 |
if d.layer != nil {
|
| 257 | 256 |
// if tmpFile is empty assume download and extracted elsewhere |
| 258 | 257 |
defer os.Remove(d.tmpFile.Name()) |
| ... | ... |
@@ -368,6 +368,28 @@ func (p *v2Puller) verifyTrustedKeys(namespace string, keys []libtrust.PublicKey |
| 368 | 368 |
} |
| 369 | 369 |
|
| 370 | 370 |
func (p *v2Puller) validateManifest(m *manifest.SignedManifest, tag string) (verified bool, err error) {
|
| 371 |
+ // If pull by digest, then verify the manifest digest. NOTE: It is |
|
| 372 |
+ // important to do this first, before any other content validation. If the |
|
| 373 |
+ // digest cannot be verified, don't even bother with those other things. |
|
| 374 |
+ if manifestDigest, err := digest.ParseDigest(tag); err == nil {
|
|
| 375 |
+ verifier, err := digest.NewDigestVerifier(manifestDigest) |
|
| 376 |
+ if err != nil {
|
|
| 377 |
+ return false, err |
|
| 378 |
+ } |
|
| 379 |
+ payload, err := m.Payload() |
|
| 380 |
+ if err != nil {
|
|
| 381 |
+ return false, err |
|
| 382 |
+ } |
|
| 383 |
+ if _, err := verifier.Write(payload); err != nil {
|
|
| 384 |
+ return false, err |
|
| 385 |
+ } |
|
| 386 |
+ if !verifier.Verified() {
|
|
| 387 |
+ err := fmt.Errorf("image verification failed for digest %s", manifestDigest)
|
|
| 388 |
+ logrus.Error(err) |
|
| 389 |
+ return false, err |
|
| 390 |
+ } |
|
| 391 |
+ } |
|
| 392 |
+ |
|
| 371 | 393 |
// TODO(tiborvass): what's the usecase for having manifest == nil and err == nil ? Shouldn't be the error be "DoesNotExist" ? |
| 372 | 394 |
if m == nil {
|
| 373 | 395 |
return false, fmt.Errorf("image manifest does not exist for tag %q", tag)
|
| ... | ... |
@@ -389,21 +411,5 @@ func (p *v2Puller) validateManifest(m *manifest.SignedManifest, tag string) (ver |
| 389 | 389 |
if err != nil {
|
| 390 | 390 |
return false, fmt.Errorf("error verifying manifest keys: %v", err)
|
| 391 | 391 |
} |
| 392 |
- localDigest, err := digest.ParseDigest(tag) |
|
| 393 |
- // if pull by digest, then verify |
|
| 394 |
- if err == nil {
|
|
| 395 |
- verifier, err := digest.NewDigestVerifier(localDigest) |
|
| 396 |
- if err != nil {
|
|
| 397 |
- return false, err |
|
| 398 |
- } |
|
| 399 |
- payload, err := m.Payload() |
|
| 400 |
- if err != nil {
|
|
| 401 |
- return false, err |
|
| 402 |
- } |
|
| 403 |
- if _, err := verifier.Write(payload); err != nil {
|
|
| 404 |
- return false, err |
|
| 405 |
- } |
|
| 406 |
- verified = verified && verifier.Verified() |
|
| 407 |
- } |
|
| 408 | 392 |
return verified, nil |
| 409 | 393 |
} |
| ... | ... |
@@ -1,25 +1,29 @@ |
| 1 | 1 |
package main |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "encoding/json" |
|
| 4 | 5 |
"fmt" |
| 5 | 6 |
"regexp" |
| 6 | 7 |
"strings" |
| 7 | 8 |
|
| 9 |
+ "github.com/docker/distribution/digest" |
|
| 10 |
+ "github.com/docker/distribution/manifest" |
|
| 8 | 11 |
"github.com/docker/docker/utils" |
| 9 | 12 |
"github.com/go-check/check" |
| 10 | 13 |
) |
| 11 | 14 |
|
| 12 | 15 |
var ( |
| 13 |
- repoName = fmt.Sprintf("%v/dockercli/busybox-by-dgst", privateRegistryURL)
|
|
| 16 |
+ remoteRepoName = "dockercli/busybox-by-dgst" |
|
| 17 |
+ repoName = fmt.Sprintf("%v/%s", privateRegistryURL, remoteRepoName)
|
|
| 14 | 18 |
pushDigestRegex = regexp.MustCompile("[\\S]+: digest: ([\\S]+) size: [0-9]+")
|
| 15 | 19 |
digestRegex = regexp.MustCompile("Digest: ([\\S]+)")
|
| 16 | 20 |
) |
| 17 | 21 |
|
| 18 |
-func setupImage(c *check.C) (string, error) {
|
|
| 22 |
+func setupImage(c *check.C) (digest.Digest, error) {
|
|
| 19 | 23 |
return setupImageWithTag(c, "latest") |
| 20 | 24 |
} |
| 21 | 25 |
|
| 22 |
-func setupImageWithTag(c *check.C, tag string) (string, error) {
|
|
| 26 |
+func setupImageWithTag(c *check.C, tag string) (digest.Digest, error) {
|
|
| 23 | 27 |
containerName := "busyboxbydigest" |
| 24 | 28 |
|
| 25 | 29 |
dockerCmd(c, "run", "-d", "-e", "digest=1", "--name", containerName, "busybox") |
| ... | ... |
@@ -52,7 +56,7 @@ func setupImageWithTag(c *check.C, tag string) (string, error) {
|
| 52 | 52 |
} |
| 53 | 53 |
pushDigest := matches[1] |
| 54 | 54 |
|
| 55 |
- return pushDigest, nil |
|
| 55 |
+ return digest.Digest(pushDigest), nil |
|
| 56 | 56 |
} |
| 57 | 57 |
|
| 58 | 58 |
func (s *DockerRegistrySuite) TestPullByTagDisplaysDigest(c *check.C) {
|
| ... | ... |
@@ -72,7 +76,7 @@ func (s *DockerRegistrySuite) TestPullByTagDisplaysDigest(c *check.C) {
|
| 72 | 72 |
pullDigest := matches[1] |
| 73 | 73 |
|
| 74 | 74 |
// make sure the pushed and pull digests match |
| 75 |
- if pushDigest != pullDigest {
|
|
| 75 |
+ if pushDigest.String() != pullDigest {
|
|
| 76 | 76 |
c.Fatalf("push digest %q didn't match pull digest %q", pushDigest, pullDigest)
|
| 77 | 77 |
} |
| 78 | 78 |
} |
| ... | ... |
@@ -95,7 +99,7 @@ func (s *DockerRegistrySuite) TestPullByDigest(c *check.C) {
|
| 95 | 95 |
pullDigest := matches[1] |
| 96 | 96 |
|
| 97 | 97 |
// make sure the pushed and pull digests match |
| 98 |
- if pushDigest != pullDigest {
|
|
| 98 |
+ if pushDigest.String() != pullDigest {
|
|
| 99 | 99 |
c.Fatalf("push digest %q didn't match pull digest %q", pushDigest, pullDigest)
|
| 100 | 100 |
} |
| 101 | 101 |
} |
| ... | ... |
@@ -291,7 +295,7 @@ func (s *DockerRegistrySuite) TestListImagesWithDigests(c *check.C) {
|
| 291 | 291 |
out, _ := dockerCmd(c, "images", "--digests") |
| 292 | 292 |
|
| 293 | 293 |
// make sure repo shown, tag=<none>, digest = $digest1 |
| 294 |
- re1 := regexp.MustCompile(`\s*` + repoName + `\s*<none>\s*` + digest1 + `\s`) |
|
| 294 |
+ re1 := regexp.MustCompile(`\s*` + repoName + `\s*<none>\s*` + digest1.String() + `\s`) |
|
| 295 | 295 |
if !re1.MatchString(out) {
|
| 296 | 296 |
c.Fatalf("expected %q: %s", re1.String(), out)
|
| 297 | 297 |
} |
| ... | ... |
@@ -319,7 +323,7 @@ func (s *DockerRegistrySuite) TestListImagesWithDigests(c *check.C) {
|
| 319 | 319 |
} |
| 320 | 320 |
|
| 321 | 321 |
// make sure repo shown, tag=<none>, digest = $digest2 |
| 322 |
- re2 := regexp.MustCompile(`\s*` + repoName + `\s*<none>\s*` + digest2 + `\s`) |
|
| 322 |
+ re2 := regexp.MustCompile(`\s*` + repoName + `\s*<none>\s*` + digest2.String() + `\s`) |
|
| 323 | 323 |
if !re2.MatchString(out) {
|
| 324 | 324 |
c.Fatalf("expected %q: %s", re2.String(), out)
|
| 325 | 325 |
} |
| ... | ... |
@@ -332,7 +336,7 @@ func (s *DockerRegistrySuite) TestListImagesWithDigests(c *check.C) {
|
| 332 | 332 |
|
| 333 | 333 |
// make sure image 1 has repo, tag, <none> AND repo, <none>, digest |
| 334 | 334 |
reWithTag1 := regexp.MustCompile(`\s*` + repoName + `\s*tag1\s*<none>\s`) |
| 335 |
- reWithDigest1 := regexp.MustCompile(`\s*` + repoName + `\s*<none>\s*` + digest1 + `\s`) |
|
| 335 |
+ reWithDigest1 := regexp.MustCompile(`\s*` + repoName + `\s*<none>\s*` + digest1.String() + `\s`) |
|
| 336 | 336 |
if !reWithTag1.MatchString(out) {
|
| 337 | 337 |
c.Fatalf("expected %q: %s", reWithTag1.String(), out)
|
| 338 | 338 |
} |
| ... | ... |
@@ -357,7 +361,7 @@ func (s *DockerRegistrySuite) TestListImagesWithDigests(c *check.C) {
|
| 357 | 357 |
|
| 358 | 358 |
// make sure image 2 has repo, tag, digest |
| 359 | 359 |
reWithTag2 := regexp.MustCompile(`\s*` + repoName + `\s*tag2\s*<none>\s`) |
| 360 |
- reWithDigest2 := regexp.MustCompile(`\s*` + repoName + `\s*<none>\s*` + digest2 + `\s`) |
|
| 360 |
+ reWithDigest2 := regexp.MustCompile(`\s*` + repoName + `\s*<none>\s*` + digest2.String() + `\s`) |
|
| 361 | 361 |
if !reWithTag2.MatchString(out) {
|
| 362 | 362 |
c.Fatalf("expected %q: %s", reWithTag2.String(), out)
|
| 363 | 363 |
} |
| ... | ... |
@@ -401,3 +405,95 @@ func (s *DockerRegistrySuite) TestDeleteImageByIDOnlyPulledByDigest(c *check.C) |
| 401 | 401 |
|
| 402 | 402 |
dockerCmd(c, "rmi", imageID) |
| 403 | 403 |
} |
| 404 |
+ |
|
| 405 |
+// TestPullFailsWithAlteredManifest tests that a `docker pull` fails when |
|
| 406 |
+// we have modified a manifest blob and its digest cannot be verified. |
|
| 407 |
+func (s *DockerRegistrySuite) TestPullFailsWithAlteredManifest(c *check.C) {
|
|
| 408 |
+ manifestDigest, err := setupImage(c) |
|
| 409 |
+ if err != nil {
|
|
| 410 |
+ c.Fatalf("error setting up image: %v", err)
|
|
| 411 |
+ } |
|
| 412 |
+ |
|
| 413 |
+ // Load the target manifest blob. |
|
| 414 |
+ manifestBlob := s.reg.readBlobContents(c, manifestDigest) |
|
| 415 |
+ |
|
| 416 |
+ var imgManifest manifest.Manifest |
|
| 417 |
+ if err := json.Unmarshal(manifestBlob, &imgManifest); err != nil {
|
|
| 418 |
+ c.Fatalf("unable to decode image manifest from blob: %s", err)
|
|
| 419 |
+ } |
|
| 420 |
+ |
|
| 421 |
+ // Add a malicious layer digest to the list of layers in the manifest. |
|
| 422 |
+ imgManifest.FSLayers = append(imgManifest.FSLayers, manifest.FSLayer{
|
|
| 423 |
+ BlobSum: digest.Digest("sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"),
|
|
| 424 |
+ }) |
|
| 425 |
+ |
|
| 426 |
+ // Move the existing data file aside, so that we can replace it with a |
|
| 427 |
+ // malicious blob of data. NOTE: we defer the returned undo func. |
|
| 428 |
+ undo := s.reg.tempMoveBlobData(c, manifestDigest) |
|
| 429 |
+ defer undo() |
|
| 430 |
+ |
|
| 431 |
+ alteredManifestBlob, err := json.Marshal(imgManifest) |
|
| 432 |
+ if err != nil {
|
|
| 433 |
+ c.Fatalf("unable to encode altered image manifest to JSON: %s", err)
|
|
| 434 |
+ } |
|
| 435 |
+ |
|
| 436 |
+ s.reg.writeBlobContents(c, manifestDigest, alteredManifestBlob) |
|
| 437 |
+ |
|
| 438 |
+ // Now try pulling that image by digest. We should get an error about |
|
| 439 |
+ // digest verification for the manifest digest. |
|
| 440 |
+ |
|
| 441 |
+ // Pull from the registry using the <name>@<digest> reference. |
|
| 442 |
+ imageReference := fmt.Sprintf("%s@%s", repoName, manifestDigest)
|
|
| 443 |
+ out, exitStatus, _ := dockerCmdWithError("pull", imageReference)
|
|
| 444 |
+ if exitStatus == 0 {
|
|
| 445 |
+ c.Fatalf("expected a zero exit status but got %d: %s", exitStatus, out)
|
|
| 446 |
+ } |
|
| 447 |
+ |
|
| 448 |
+ expectedErrorMsg := fmt.Sprintf("image verification failed for digest %s", manifestDigest)
|
|
| 449 |
+ if !strings.Contains(out, expectedErrorMsg) {
|
|
| 450 |
+ c.Fatalf("expected error message %q in output: %s", expectedErrorMsg, out)
|
|
| 451 |
+ } |
|
| 452 |
+} |
|
| 453 |
+ |
|
| 454 |
+// TestPullFailsWithAlteredLayer tests that a `docker pull` fails when |
|
| 455 |
+// we have modified a layer blob and its digest cannot be verified. |
|
| 456 |
+func (s *DockerRegistrySuite) TestPullFailsWithAlteredLayer(c *check.C) {
|
|
| 457 |
+ manifestDigest, err := setupImage(c) |
|
| 458 |
+ if err != nil {
|
|
| 459 |
+ c.Fatalf("error setting up image: %v", err)
|
|
| 460 |
+ } |
|
| 461 |
+ |
|
| 462 |
+ // Load the target manifest blob. |
|
| 463 |
+ manifestBlob := s.reg.readBlobContents(c, manifestDigest) |
|
| 464 |
+ |
|
| 465 |
+ var imgManifest manifest.Manifest |
|
| 466 |
+ if err := json.Unmarshal(manifestBlob, &imgManifest); err != nil {
|
|
| 467 |
+ c.Fatalf("unable to decode image manifest from blob: %s", err)
|
|
| 468 |
+ } |
|
| 469 |
+ |
|
| 470 |
+ // Next, get the digest of one of the layers from the manifest. |
|
| 471 |
+ targetLayerDigest := imgManifest.FSLayers[0].BlobSum |
|
| 472 |
+ |
|
| 473 |
+ // Move the existing data file aside, so that we can replace it with a |
|
| 474 |
+ // malicious blob of data. NOTE: we defer the returned undo func. |
|
| 475 |
+ undo := s.reg.tempMoveBlobData(c, targetLayerDigest) |
|
| 476 |
+ defer undo() |
|
| 477 |
+ |
|
| 478 |
+ // Now make a fake data blob in this directory. |
|
| 479 |
+ s.reg.writeBlobContents(c, targetLayerDigest, []byte("This is not the data you are looking for."))
|
|
| 480 |
+ |
|
| 481 |
+ // Now try pulling that image by digest. We should get an error about |
|
| 482 |
+ // digest verification for the target layer digest. |
|
| 483 |
+ |
|
| 484 |
+ // Pull from the registry using the <name>@<digest> reference. |
|
| 485 |
+ imageReference := fmt.Sprintf("%s@%s", repoName, manifestDigest)
|
|
| 486 |
+ out, exitStatus, _ := dockerCmdWithError("pull", imageReference)
|
|
| 487 |
+ if exitStatus == 0 {
|
|
| 488 |
+ c.Fatalf("expected a zero exit status but got: %d", exitStatus)
|
|
| 489 |
+ } |
|
| 490 |
+ |
|
| 491 |
+ expectedErrorMsg := fmt.Sprintf("filesystem layer verification failed for digest %s", targetLayerDigest)
|
|
| 492 |
+ if !strings.Contains(out, expectedErrorMsg) {
|
|
| 493 |
+ c.Fatalf("expected error message %q in output: %s", expectedErrorMsg, out)
|
|
| 494 |
+ } |
|
| 495 |
+} |
| ... | ... |
@@ -8,6 +8,7 @@ import ( |
| 8 | 8 |
"os/exec" |
| 9 | 9 |
"path/filepath" |
| 10 | 10 |
|
| 11 |
+ "github.com/docker/distribution/digest" |
|
| 11 | 12 |
"github.com/go-check/check" |
| 12 | 13 |
) |
| 13 | 14 |
|
| ... | ... |
@@ -70,3 +71,50 @@ func (t *testRegistryV2) Close() {
|
| 70 | 70 |
t.cmd.Process.Kill() |
| 71 | 71 |
os.RemoveAll(t.dir) |
| 72 | 72 |
} |
| 73 |
+ |
|
| 74 |
+func (t *testRegistryV2) getBlobFilename(blobDigest digest.Digest) string {
|
|
| 75 |
+ // Split the digest into it's algorithm and hex components. |
|
| 76 |
+ dgstAlg, dgstHex := blobDigest.Algorithm(), blobDigest.Hex() |
|
| 77 |
+ |
|
| 78 |
+ // The path to the target blob data looks something like: |
|
| 79 |
+ // baseDir + "docker/registry/v2/blobs/sha256/a3/a3ed...46d4/data" |
|
| 80 |
+ return fmt.Sprintf("%s/docker/registry/v2/blobs/%s/%s/%s/data", t.dir, dgstAlg, dgstHex[:2], dgstHex)
|
|
| 81 |
+} |
|
| 82 |
+ |
|
| 83 |
+func (t *testRegistryV2) readBlobContents(c *check.C, blobDigest digest.Digest) []byte {
|
|
| 84 |
+ // Load the target manifest blob. |
|
| 85 |
+ manifestBlob, err := ioutil.ReadFile(t.getBlobFilename(blobDigest)) |
|
| 86 |
+ if err != nil {
|
|
| 87 |
+ c.Fatalf("unable to read blob: %s", err)
|
|
| 88 |
+ } |
|
| 89 |
+ |
|
| 90 |
+ return manifestBlob |
|
| 91 |
+} |
|
| 92 |
+ |
|
| 93 |
+func (t *testRegistryV2) writeBlobContents(c *check.C, blobDigest digest.Digest, data []byte) {
|
|
| 94 |
+ if err := ioutil.WriteFile(t.getBlobFilename(blobDigest), data, os.FileMode(0644)); err != nil {
|
|
| 95 |
+ c.Fatalf("unable to write malicious data blob: %s", err)
|
|
| 96 |
+ } |
|
| 97 |
+} |
|
| 98 |
+ |
|
| 99 |
+func (t *testRegistryV2) tempMoveBlobData(c *check.C, blobDigest digest.Digest) (undo func()) {
|
|
| 100 |
+ tempFile, err := ioutil.TempFile("", "registry-temp-blob-")
|
|
| 101 |
+ if err != nil {
|
|
| 102 |
+ c.Fatalf("unable to get temporary blob file: %s", err)
|
|
| 103 |
+ } |
|
| 104 |
+ tempFile.Close() |
|
| 105 |
+ |
|
| 106 |
+ blobFilename := t.getBlobFilename(blobDigest) |
|
| 107 |
+ |
|
| 108 |
+ // Move the existing data file aside, so that we can replace it with a |
|
| 109 |
+ // another blob of data. |
|
| 110 |
+ if err := os.Rename(blobFilename, tempFile.Name()); err != nil {
|
|
| 111 |
+ os.Remove(tempFile.Name()) |
|
| 112 |
+ c.Fatalf("unable to move data blob: %s", err)
|
|
| 113 |
+ } |
|
| 114 |
+ |
|
| 115 |
+ return func() {
|
|
| 116 |
+ os.Rename(tempFile.Name(), blobFilename) |
|
| 117 |
+ os.Remove(tempFile.Name()) |
|
| 118 |
+ } |
|
| 119 |
+} |