Skip to content

Commit 0a7803c

Browse files
Merge pull request #12307 from soltysh/first-blobs-then-manifest-13
Verify manifest before accepting
2 parents d024d12 + f9cfb9b commit 0a7803c

9 files changed

+476
-225
lines changed

pkg/dockerregistry/server/blobdescriptorservice_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func TestBlobDescriptorServiceIsApplied(t *testing.T) {
9090
}
9191
os.Setenv("DOCKER_REGISTRY_URL", serverURL.Host)
9292

93-
desc, _, err := registrytest.UploadTestBlob(serverURL, "user/app")
93+
desc, _, err := registrytest.UploadTestBlob(serverURL, nil, "user/app")
9494
if err != nil {
9595
t.Fatal(err)
9696
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package server
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/docker/distribution"
7+
"github.com/docker/distribution/context"
8+
"github.com/docker/distribution/manifest/schema1"
9+
"github.com/docker/distribution/manifest/schema2"
10+
11+
imageapi "github.com/openshift/origin/pkg/image/api"
12+
)
13+
14+
// A ManifestHandler defines a common set of operations on all versions of manifest schema.
15+
type ManifestHandler interface {
16+
// FillImageMetadata fills a given image with metadata parsed from manifest. It also corrects layer sizes
17+
// with blob sizes. Newer Docker client versions don't set layer sizes in the manifest schema 1 at all.
18+
// Origin master needs correct layer sizes for proper image quota support. That's why we need to fill the
19+
// metadata in the registry.
20+
FillImageMetadata(ctx context.Context, image *imageapi.Image) error
21+
22+
// Manifest returns a deserialized manifest object.
23+
Manifest() distribution.Manifest
24+
25+
// Payload returns manifest's media type, complete payload with signatures and canonical payload without
26+
// signatures or an error if the information could not be fetched.
27+
Payload() (mediaType string, payload []byte, canonical []byte, err error)
28+
29+
// Verify returns an error if the contained manifest is not valid or has missing dependencies.
30+
Verify(ctx context.Context, skipDependencyVerification bool) error
31+
}
32+
33+
// NewManifestHandler creates a manifest handler for the given manifest.
34+
func NewManifestHandler(repo *repository, manifest distribution.Manifest) (ManifestHandler, error) {
35+
switch t := manifest.(type) {
36+
case *schema1.SignedManifest:
37+
return &manifestSchema1Handler{repo: repo, manifest: t}, nil
38+
case *schema2.DeserializedManifest:
39+
return &manifestSchema2Handler{repo: repo, manifest: t}, nil
40+
default:
41+
return nil, fmt.Errorf("unsupported manifest type %T", manifest)
42+
}
43+
}
44+
45+
// NewManifestHandlerFromImage creates a new manifest handler for a manifest stored in the given image.
46+
func NewManifestHandlerFromImage(repo *repository, image *imageapi.Image) (ManifestHandler, error) {
47+
var (
48+
manifest distribution.Manifest
49+
err error
50+
)
51+
52+
switch image.DockerImageManifestMediaType {
53+
case "", schema1.MediaTypeManifest:
54+
manifest, err = unmarshalManifestSchema1([]byte(image.DockerImageManifest), image.DockerImageSignatures)
55+
case schema2.MediaTypeManifest:
56+
manifest, err = unmarshalManifestSchema2([]byte(image.DockerImageManifest))
57+
default:
58+
return nil, fmt.Errorf("unsupported manifest media type %s", image.DockerImageManifestMediaType)
59+
}
60+
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
return NewManifestHandler(repo, manifest)
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package server
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"path"
7+
8+
"github.com/docker/distribution"
9+
"github.com/docker/distribution/context"
10+
"github.com/docker/distribution/manifest/schema1"
11+
"github.com/docker/distribution/reference"
12+
"github.com/docker/libtrust"
13+
14+
"k8s.io/kubernetes/pkg/util/sets"
15+
16+
imageapi "github.com/openshift/origin/pkg/image/api"
17+
)
18+
19+
func unmarshalManifestSchema1(content []byte, signatures [][]byte) (distribution.Manifest, error) {
20+
// prefer signatures from the manifest
21+
if _, err := libtrust.ParsePrettySignature(content, "signatures"); err == nil {
22+
sm := schema1.SignedManifest{Canonical: content}
23+
if err = json.Unmarshal(content, &sm); err == nil {
24+
return &sm, nil
25+
}
26+
}
27+
28+
jsig, err := libtrust.NewJSONSignature(content, signatures...)
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
// Extract the pretty JWS
34+
content, err = jsig.PrettySignature("signatures")
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
var sm schema1.SignedManifest
40+
if err = json.Unmarshal(content, &sm); err != nil {
41+
return nil, err
42+
}
43+
return &sm, err
44+
}
45+
46+
type manifestSchema1Handler struct {
47+
repo *repository
48+
manifest *schema1.SignedManifest
49+
}
50+
51+
var _ ManifestHandler = &manifestSchema1Handler{}
52+
53+
func (h *manifestSchema1Handler) FillImageMetadata(ctx context.Context, image *imageapi.Image) error {
54+
signatures, err := h.manifest.Signatures()
55+
if err != nil {
56+
return err
57+
}
58+
59+
for _, signDigest := range signatures {
60+
image.DockerImageSignatures = append(image.DockerImageSignatures, signDigest)
61+
}
62+
63+
if err := imageapi.ImageWithMetadata(image); err != nil {
64+
return err
65+
}
66+
67+
refs := h.manifest.References()
68+
69+
blobSet := sets.NewString()
70+
image.DockerImageMetadata.Size = int64(0)
71+
72+
blobs := h.repo.Blobs(ctx)
73+
for i := range image.DockerImageLayers {
74+
layer := &image.DockerImageLayers[i]
75+
// DockerImageLayers represents h.manifest.Manifest.FSLayers in reversed order
76+
desc, err := blobs.Stat(ctx, refs[len(image.DockerImageLayers)-i-1].Digest)
77+
if err != nil {
78+
context.GetLogger(ctx).Errorf("failed to stat blob %s of image %s", layer.Name, image.DockerImageReference)
79+
return err
80+
}
81+
if layer.MediaType == "" {
82+
if desc.MediaType != "" {
83+
layer.MediaType = desc.MediaType
84+
} else {
85+
layer.MediaType = schema1.MediaTypeManifestLayer
86+
}
87+
}
88+
layer.LayerSize = desc.Size
89+
// count empty layer just once (empty layer may actually have non-zero size)
90+
if !blobSet.Has(layer.Name) {
91+
image.DockerImageMetadata.Size += desc.Size
92+
blobSet.Insert(layer.Name)
93+
}
94+
}
95+
96+
return nil
97+
}
98+
99+
func (h *manifestSchema1Handler) Manifest() distribution.Manifest {
100+
return h.manifest
101+
}
102+
103+
func (h *manifestSchema1Handler) Payload() (mediaType string, payload []byte, canonical []byte, err error) {
104+
mt, payload, err := h.manifest.Payload()
105+
return mt, payload, h.manifest.Canonical, err
106+
}
107+
108+
func (h *manifestSchema1Handler) Verify(ctx context.Context, skipDependencyVerification bool) error {
109+
var errs distribution.ErrManifestVerification
110+
111+
// we want to verify that referenced blobs exist locally - thus using upstream repository object directly
112+
repo := h.repo.Repository
113+
114+
if len(path.Join(h.repo.registryAddr, h.manifest.Name)) > reference.NameTotalLengthMax {
115+
errs = append(errs,
116+
distribution.ErrManifestNameInvalid{
117+
Name: h.manifest.Name,
118+
Reason: fmt.Errorf("<registry-host>/<manifest-name> must not be more than %d characters", reference.NameTotalLengthMax),
119+
})
120+
}
121+
122+
if !reference.NameRegexp.MatchString(h.manifest.Name) {
123+
errs = append(errs,
124+
distribution.ErrManifestNameInvalid{
125+
Name: h.manifest.Name,
126+
Reason: fmt.Errorf("invalid manifest name format"),
127+
})
128+
}
129+
130+
if len(h.manifest.History) != len(h.manifest.FSLayers) {
131+
errs = append(errs, fmt.Errorf("mismatched history and fslayer cardinality %d != %d",
132+
len(h.manifest.History), len(h.manifest.FSLayers)))
133+
}
134+
135+
if _, err := schema1.Verify(h.manifest); err != nil {
136+
switch err {
137+
case libtrust.ErrMissingSignatureKey, libtrust.ErrInvalidJSONContent, libtrust.ErrMissingSignatureKey:
138+
errs = append(errs, distribution.ErrManifestUnverified{})
139+
default:
140+
if err.Error() == "invalid signature" {
141+
errs = append(errs, distribution.ErrManifestUnverified{})
142+
} else {
143+
errs = append(errs, err)
144+
}
145+
}
146+
}
147+
148+
if skipDependencyVerification {
149+
if len(errs) > 0 {
150+
return errs
151+
}
152+
return nil
153+
}
154+
155+
for _, fsLayer := range h.manifest.References() {
156+
_, err := repo.Blobs(ctx).Stat(ctx, fsLayer.Digest)
157+
if err != nil {
158+
if err != distribution.ErrBlobUnknown {
159+
errs = append(errs, err)
160+
}
161+
162+
// On error here, we always append unknown blob errors.
163+
errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: fsLayer.Digest})
164+
}
165+
}
166+
167+
if len(errs) > 0 {
168+
return errs
169+
}
170+
return nil
171+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package server
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
7+
"github.com/docker/distribution"
8+
"github.com/docker/distribution/context"
9+
"github.com/docker/distribution/manifest/schema2"
10+
11+
imageapi "github.com/openshift/origin/pkg/image/api"
12+
)
13+
14+
var (
15+
errMissingURL = errors.New("missing URL on layer")
16+
errUnexpectedURL = errors.New("unexpected URL on layer")
17+
)
18+
19+
func unmarshalManifestSchema2(content []byte) (distribution.Manifest, error) {
20+
var m schema2.DeserializedManifest
21+
if err := json.Unmarshal(content, &m); err != nil {
22+
return nil, err
23+
}
24+
25+
return &m, nil
26+
}
27+
28+
type manifestSchema2Handler struct {
29+
repo *repository
30+
manifest *schema2.DeserializedManifest
31+
}
32+
33+
var _ ManifestHandler = &manifestSchema2Handler{}
34+
35+
func (h *manifestSchema2Handler) FillImageMetadata(ctx context.Context, image *imageapi.Image) error {
36+
configBytes, err := h.repo.Blobs(ctx).Get(ctx, h.manifest.Config.Digest)
37+
if err != nil {
38+
context.GetLogger(ctx).Errorf("failed to get image config %s: %v", h.manifest.Config.Digest.String(), err)
39+
return err
40+
}
41+
image.DockerImageConfig = string(configBytes)
42+
43+
if err := imageapi.ImageWithMetadata(image); err != nil {
44+
return err
45+
}
46+
47+
return nil
48+
}
49+
50+
func (h *manifestSchema2Handler) Manifest() distribution.Manifest {
51+
return h.manifest
52+
}
53+
54+
func (h *manifestSchema2Handler) Payload() (mediaType string, payload []byte, canonical []byte, err error) {
55+
mt, p, err := h.manifest.Payload()
56+
return mt, p, p, err
57+
}
58+
59+
func (h *manifestSchema2Handler) Verify(ctx context.Context, skipDependencyVerification bool) error {
60+
var errs distribution.ErrManifestVerification
61+
62+
if skipDependencyVerification {
63+
return nil
64+
}
65+
66+
// we want to verify that referenced blobs exist locally - thus using upstream repository object directly
67+
repo := h.repo.Repository
68+
69+
target := h.manifest.Target()
70+
_, err := repo.Blobs(ctx).Stat(ctx, target.Digest)
71+
if err != nil {
72+
if err != distribution.ErrBlobUnknown {
73+
errs = append(errs, err)
74+
}
75+
76+
// On error here, we always append unknown blob errors.
77+
errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: target.Digest})
78+
}
79+
80+
for _, fsLayer := range h.manifest.References() {
81+
var err error
82+
if fsLayer.MediaType != schema2.MediaTypeForeignLayer {
83+
if len(fsLayer.URLs) == 0 {
84+
_, err = repo.Blobs(ctx).Stat(ctx, fsLayer.Digest)
85+
} else {
86+
err = errUnexpectedURL
87+
}
88+
} else {
89+
// Clients download this layer from an external URL, so do not check for
90+
// its presense.
91+
if len(fsLayer.URLs) == 0 {
92+
err = errMissingURL
93+
}
94+
}
95+
if err != nil {
96+
if err != distribution.ErrBlobUnknown {
97+
errs = append(errs, err)
98+
}
99+
100+
// On error here, we always append unknown blob errors.
101+
errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: fsLayer.Digest})
102+
}
103+
}
104+
105+
if len(errs) > 0 {
106+
return errs
107+
}
108+
return nil
109+
}

pkg/dockerregistry/server/pullthroughblobstore_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,11 @@ func TestPullthroughServeBlob(t *testing.T) {
8282
testImageStream := registrytest.TestNewImageStreamObject("user", "app", "latest", testImage.Name, testImage.DockerImageReference)
8383
client.AddReactor("get", "imagestreams", imagetest.GetFakeImageStreamGetHandler(t, *testImageStream))
8484

85-
blob1Desc, blob1Content, err := registrytest.UploadTestBlob(serverURL, "user/app")
85+
blob1Desc, blob1Content, err := registrytest.UploadTestBlob(serverURL, nil, "user/app")
8686
if err != nil {
8787
t.Fatal(err)
8888
}
89-
blob2Desc, blob2Content, err := registrytest.UploadTestBlob(serverURL, "user/app")
89+
blob2Desc, blob2Content, err := registrytest.UploadTestBlob(serverURL, nil, "user/app")
9090
if err != nil {
9191
t.Fatal(err)
9292
}

0 commit comments

Comments
 (0)