Validate manifest parent-child relationships

Since 1.8 may push bad manifests, we've added some validation to ensure that
the parent-child relationships represented by image json are correct. If the
relationship is not correct, we reject the push.

Signed-off-by: Stephen J Day <stephen.day@docker.com>
This commit is contained in:
Stephen J Day 2015-08-12 19:37:43 -07:00
parent ca16795e85
commit e57c13f3cb
6 changed files with 238 additions and 5 deletions

View file

@ -74,6 +74,16 @@ func (ErrManifestUnverified) Error() string {
return fmt.Sprintf("unverified manifest") return fmt.Sprintf("unverified manifest")
} }
// ErrManifestValidation is returned during manifest verification if a common
// validation error is encountered.
type ErrManifestValidation struct {
Reason string
}
func (err ErrManifestValidation) Error() string {
return fmt.Sprintf("invalid manifest: %s", err.Reason)
}
// ErrManifestVerification provides a type to collect errors encountered // ErrManifestVerification provides a type to collect errors encountered
// during manifest verification. Currently, it accepts errors of all types, // during manifest verification. Currently, it accepts errors of all types,
// but it may be narrowed to those involving manifest verification. // but it may be narrowed to those involving manifest verification.

View file

@ -1,10 +1,12 @@
package notifications package notifications
import ( import (
"encoding/json"
"io" "io"
"reflect" "reflect"
"testing" "testing"
"code.google.com/p/go-uuid/uuid"
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
@ -132,6 +134,8 @@ func checkExerciseRepository(t *testing.T, repository distribution.Repository) {
} }
} }
m.History = generateHistory(t, len(m.FSLayers))
pk, err := libtrust.GenerateECP256PrivateKey() pk, err := libtrust.GenerateECP256PrivateKey()
if err != nil { if err != nil {
t.Fatalf("unexpected error generating key: %v", err) t.Fatalf("unexpected error generating key: %v", err)
@ -176,3 +180,37 @@ func checkExerciseRepository(t *testing.T, repository distribution.Repository) {
t.Fatalf("retrieved unexpected manifest: %v", err) t.Fatalf("retrieved unexpected manifest: %v", err)
} }
} }
// generateHistory creates a valid history entry of length n.
func generateHistory(t *testing.T, n int) []manifest.History {
var images []map[string]interface{}
// first pass: create images entries.
for i := 0; i < n; i++ {
// simulate correct id -> parent links in v1Compatibility, using uuids.
image := map[string]interface{}{
"id": uuid.New(),
}
images = append(images, image)
}
var history []manifest.History
for i, image := range images {
if i+1 < len(images) {
image["parent"] = images[i+1]["id"]
}
p, err := json.Marshal(image)
if err != nil {
t.Fatalf("error generating image json: %v", err)
}
history = append(history, manifest.History{
V1Compatibility: string(p),
})
}
return history
}

View file

@ -16,6 +16,7 @@ import (
"strings" "strings"
"testing" "testing"
"code.google.com/p/go-uuid/uuid"
"github.com/docker/distribution/configuration" "github.com/docker/distribution/configuration"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
@ -323,10 +324,12 @@ func TestManifestAPI(t *testing.T) {
}, },
} }
unsignedManifest.History = generateHistory(t, len(unsignedManifest.FSLayers), false)
resp = putManifest(t, "putting unsigned manifest", manifestURL, unsignedManifest) resp = putManifest(t, "putting unsigned manifest", manifestURL, unsignedManifest)
defer resp.Body.Close() defer resp.Body.Close()
checkResponse(t, "posting unsigned manifest", resp, http.StatusBadRequest) checkResponse(t, "posting unsigned manifest", resp, http.StatusBadRequest)
_, p, counts := checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, _, p, counts := checkBodyHasErrorCodes(t, "putting unsigned manifest", resp,
v2.ErrorCodeManifestUnverified, v2.ErrorCodeBlobUnknown, v2.ErrorCodeDigestInvalid) v2.ErrorCodeManifestUnverified, v2.ErrorCodeBlobUnknown, v2.ErrorCodeDigestInvalid)
expectedCounts := map[v2.ErrorCode]int{ expectedCounts := map[v2.ErrorCode]int{
@ -361,8 +364,10 @@ func TestManifestAPI(t *testing.T) {
pushLayer(t, env.builder, imageName, dgst, uploadURLBase, rs) pushLayer(t, env.builder, imageName, dgst, uploadURLBase, rs)
} }
// ------------------- // -----------------------
// Push the signed manifest with all layers pushed. // mostly valid, but we have an extra parent point in history.
unsignedManifest.History = generateHistory(t, len(unsignedManifest.FSLayers), true)
signedManifest, err := manifest.Sign(unsignedManifest, env.pk) signedManifest, err := manifest.Sign(unsignedManifest, env.pk)
if err != nil { if err != nil {
t.Fatalf("unexpected error signing manifest: %v", err) t.Fatalf("unexpected error signing manifest: %v", err)
@ -377,6 +382,36 @@ func TestManifestAPI(t *testing.T) {
manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String()) manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String())
checkErr(t, err, "building manifest url") checkErr(t, err, "building manifest url")
resp = putManifest(t, "putting signed manifest with bad parent", manifestURL, signedManifest)
checkResponse(t, "putting signed manifest with bad parent", resp, http.StatusBadRequest)
_, p, counts = checkBodyHasErrorCodes(t, "putting unsigned manifest with bad parent", resp, v2.ErrorCodeManifestInvalid)
expectedCounts = map[v2.ErrorCode]int{
v2.ErrorCodeManifestInvalid: 1,
}
if !reflect.DeepEqual(counts, expectedCounts) {
t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p))
}
// -------------------
// Push the signed manifest with all layers pushed.
unsignedManifest.History = generateHistory(t, len(unsignedManifest.FSLayers), false)
signedManifest, err = manifest.Sign(unsignedManifest, env.pk)
if err != nil {
t.Fatalf("unexpected error signing manifest: %v", err)
}
payload, err = signedManifest.Payload()
checkErr(t, err, "getting manifest payload")
dgst, err = digest.FromBytes(payload)
checkErr(t, err, "digesting manifest")
manifestDigestURL, err = env.builder.BuildManifestURL(imageName, dgst.String())
checkErr(t, err, "building manifest url")
resp = putManifest(t, "putting signed manifest", manifestURL, signedManifest) resp = putManifest(t, "putting signed manifest", manifestURL, signedManifest)
checkResponse(t, "putting signed manifest", resp, http.StatusAccepted) checkResponse(t, "putting signed manifest", resp, http.StatusAccepted)
checkHeaders(t, resp, http.Header{ checkHeaders(t, resp, http.Header{
@ -791,3 +826,41 @@ func checkErr(t *testing.T, err error, msg string) {
t.Fatalf("unexpected error %s: %v", msg, err) t.Fatalf("unexpected error %s: %v", msg, err)
} }
} }
// generateHistory creates a valid history entry of length n.
func generateHistory(t *testing.T, n int, extraParent bool) []manifest.History {
var images []map[string]interface{}
// first pass: create images entries.
for i := 0; i < n; i++ {
// simulate correct id -> parent links in v1Compatibility, using uuids.
image := map[string]interface{}{
"id": uuid.New(),
}
images = append(images, image)
}
var history []manifest.History
for i, image := range images {
if i+1 < len(images) {
image["parent"] = images[i+1]["id"]
}
if extraParent && i == len(images)-1 {
image["parent"] = uuid.New()
}
p, err := json.Marshal(image)
if err != nil {
t.Fatalf("error generating image json: %v", err)
}
history = append(history, manifest.History{
V1Compatibility: string(p),
})
}
return history
}

View file

@ -140,6 +140,8 @@ func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http
imh.Errors.Push(v2.ErrorCodeBlobUnknown, verificationError.FSLayer) imh.Errors.Push(v2.ErrorCodeBlobUnknown, verificationError.FSLayer)
case distribution.ErrManifestUnverified: case distribution.ErrManifestUnverified:
imh.Errors.Push(v2.ErrorCodeManifestUnverified) imh.Errors.Push(v2.ErrorCodeManifestUnverified)
case distribution.ErrManifestValidation:
imh.Errors.Push(v2.ErrorCodeManifestInvalid, verificationError.Error())
default: default:
if verificationError == digest.ErrDigestInvalidFormat { if verificationError == digest.ErrDigestInvalidFormat {
// TODO(stevvooe): We need to really need to move all // TODO(stevvooe): We need to really need to move all

View file

@ -1,6 +1,7 @@
package storage package storage
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/docker/distribution" "github.com/docker/distribution"
@ -102,6 +103,48 @@ func (ms *manifestStore) verifyManifest(mnfst *manifest.SignedManifest) error {
} }
} }
if len(mnfst.FSLayers) == 0 || len(mnfst.History) == 0 {
errs = append(errs, distribution.ErrManifestValidation{
Reason: "no layers present"})
}
if len(mnfst.FSLayers) != len(mnfst.History) {
errs = append(errs, distribution.ErrManifestValidation{
Reason: "mismatched layers and history"})
}
// image provides a local type for validating the image relationship.
type image struct {
ID string `json:"id"`
Parent string `json:"parent"`
}
// Process the history portion to ensure that the parent links are
// correctly represented. We serialize the image json, then walk the
// entries, checking the parent link.
var images []image
for _, entry := range mnfst.History {
var im image
if err := json.Unmarshal([]byte(entry.V1Compatibility), &im); err != nil {
errs = append(errs, err)
}
images = append(images, im)
}
// go back through each image, checking the parent link and rank
var parentID string
for i := len(images) - 1; i >= 0; i-- {
// ensure that the parent id matches but only if there is a parent.
// There are cases where successive layers don't fill in the parents.
if images[i].Parent != parentID {
errs = append(errs, distribution.ErrManifestValidation{
Reason: "parent not adjacent in manifest"})
}
parentID = images[i].ID
}
for _, fsLayer := range mnfst.FSLayers { for _, fsLayer := range mnfst.FSLayers {
exists, err := ms.repository.Layers().Exists(fsLayer.BlobSum) exists, err := ms.repository.Layers().Exists(fsLayer.BlobSum)
if err != nil { if err != nil {

View file

@ -2,15 +2,16 @@ package storage
import ( import (
"bytes" "bytes"
"encoding/json"
"io" "io"
"reflect" "reflect"
"testing" "testing"
"github.com/docker/distribution/registry/storage/cache" "code.google.com/p/go-uuid/uuid"
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
"github.com/docker/distribution/registry/storage/cache"
"github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver"
"github.com/docker/distribution/registry/storage/driver/inmemory" "github.com/docker/distribution/registry/storage/driver/inmemory"
"github.com/docker/distribution/testutil" "github.com/docker/distribution/testutil"
@ -103,6 +104,38 @@ func TestManifestStorage(t *testing.T) {
t.Fatalf("error signing manifest: %v", err) t.Fatalf("error signing manifest: %v", err)
} }
// try to put the manifest initially. this will fail since we have not
// included history or pushed any layers.
err = ms.Put(sm)
if err == nil {
t.Fatalf("expected errors putting manifest with full verification")
}
switch err := err.(type) {
case distribution.ErrManifestVerification:
if len(err) != 4 {
t.Fatalf("expected 4 verification errors: %#v", err)
}
for _, err := range err {
switch err := err.(type) {
case distribution.ErrUnknownLayer, distribution.ErrManifestValidation:
// noop: we expect these errors
default:
t.Fatalf("unexpected error type: %v", err)
}
}
default:
t.Fatalf("unexpected error verifying manifest: %v", err)
}
m.History = generateHistory(t, len(m.FSLayers))
sm, err = manifest.Sign(&m, pk)
if err != nil {
t.Fatalf("unexpected error signing manfiest with history: %v", err)
}
// we've fixed the missing history, try the push and fail on layer checks.
err = ms.Put(sm) err = ms.Put(sm)
if err == nil { if err == nil {
t.Fatalf("expected errors putting manifest") t.Fatalf("expected errors putting manifest")
@ -288,3 +321,37 @@ func TestManifestStorage(t *testing.T) {
t.Fatalf("unexpected an error deleting manifest by digest: %v", err) t.Fatalf("unexpected an error deleting manifest by digest: %v", err)
} }
} }
// generateHistory creates a valid history entry of length n.
func generateHistory(t *testing.T, n int) []manifest.History {
var images []map[string]interface{}
// first pass: create images entries.
for i := 0; i < n; i++ {
// simulate correct id -> parent links in v1Compatibility, using uuids.
image := map[string]interface{}{
"id": uuid.New(),
}
images = append(images, image)
}
var history []manifest.History
for i, image := range images {
if i+1 < len(images) {
image["parent"] = images[i+1]["id"]
}
p, err := json.Marshal(image)
if err != nil {
t.Fatalf("error generating image json: %v", err)
}
history = append(history, manifest.History{
V1Compatibility: string(p),
})
}
return history
}