Skip to content

Commit 30a0e99

Browse files
committed
proxy: Verify *either* toplevel or target
(Only compile tested locally) An issue was raised in that a current Red Hat internal build system was performing a signature just on the per-architecture manifest, but the current proxy code is expecting a signature on the manifest list. To quote Miloslav from that bug: > Podman signs both the per-platform items and the top level, > and enforces signatures only on per-platform items. cosign, > by default, signs only the top level (and has an option to > sign everything), I’m not sure which one it enforces. > I don’t immediately recall other platforms. We believe the current proxy code is secure since we always require signatures (if configured) on the manifest list, and the manifest list covers the individual manifests via sha256 digest. However, we want to support signatures only being present on the per-arch manifest too in order to be flexible. Yet, we can't hard switch to requiring signatures on the per-arch manifests as that would immediately break anyone relying on the current behavior of validating the toplevel. Change the logic here to: - Verify signature on the manifest list, and cache the error (if any) - Fetch the manifest - Verify signature on the manifest - Allow if *either* signature was accepted; conversely, only error if signature validation failed on *both* the manifest list and manifest This also switches things to cache the manifest upfront instead of doing it lazily on `GetManifest/GetConfig`; in practice callers were always immediately requesting those anyways. The use case of just fetching blobs exists (e.g. to resume an interrupted fetch), but is relatively obscure and in general I think it's good to re-verify signatures on each operation. Signed-off-by: Colin Walters <[email protected]>
1 parent 5e88bb0 commit 30a0e99

File tree

2 files changed

+101
-78
lines changed

2 files changed

+101
-78
lines changed

cmd/skopeo/proxy.go

Lines changed: 56 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,9 @@ type activePipe struct {
153153
// openImage is an opened image reference
154154
type openImage struct {
155155
// id is an opaque integer handle
156-
id uint64
157-
src types.ImageSource
158-
cachedimg types.Image
156+
id uint64
157+
src types.ImageSource
158+
img types.Image
159159
}
160160

161161
// proxyHandler is the state associated with our socket.
@@ -225,6 +225,7 @@ func (h *proxyHandler) OpenImage(args []any) (replyBuf, error) {
225225
}
226226

227227
func (h *proxyHandler) openImageImpl(args []any, allowNotFound bool) (retReplyBuf replyBuf, retErr error) {
228+
ctx := context.Background()
228229
h.lock.Lock()
229230
defer h.lock.Unlock()
230231
var ret replyBuf
@@ -254,12 +255,53 @@ func (h *proxyHandler) openImageImpl(args []any, allowNotFound bool) (retReplyBu
254255
}
255256

256257
unparsedTopLevel := image.UnparsedInstance(imgsrc, nil)
257-
allowed, err := h.policyctx.IsRunningImageAllowed(context.Background(), unparsedTopLevel)
258+
// Check the signature on the toplevel (possibly multi-arch) manifest, but we don't
259+
// yet propagate the error here.
260+
allowed, toplevelVerificationErr := h.policyctx.IsRunningImageAllowed(context.Background(), unparsedTopLevel)
261+
if toplevelVerificationErr == nil && !allowed {
262+
return ret, fmt.Errorf("internal inconsistency: policy verification failed without returning an error")
263+
}
264+
265+
mfest, manifestType, err := unparsedTopLevel.Manifest(ctx)
258266
if err != nil {
259267
return ret, err
260268
}
261-
if !allowed {
262-
return ret, fmt.Errorf("internal inconsistency: policy verification failed without returning an error")
269+
var target *image.UnparsedImage
270+
if manifest.MIMETypeIsMultiImage(manifestType) {
271+
manifestList, err := manifest.ListFromBlob(mfest, manifestType)
272+
if err != nil {
273+
return ret, err
274+
}
275+
instanceDigest, err := manifestList.ChooseInstance(h.sysctx)
276+
if err != nil {
277+
return ret, err
278+
}
279+
target = image.UnparsedInstance(imgsrc, &instanceDigest)
280+
281+
allowed, targetVerificationErr := h.policyctx.IsRunningImageAllowed(context.Background(), target)
282+
if targetVerificationErr == nil && !allowed {
283+
return ret, fmt.Errorf("internal inconsistency: policy verification failed without returning an error")
284+
}
285+
286+
// Now, we only error if *both* the toplevel and target verification failed.
287+
// If either succeeded, that's OK. We want to support a case where the manifest
288+
// list is signed, but the target is not (because we previously supported that behavior),
289+
// and we want to support the case where only the target is signed (consistent with what c/image enforces).
290+
if targetVerificationErr != nil && toplevelVerificationErr != nil {
291+
return ret, targetVerificationErr
292+
}
293+
} else {
294+
target = unparsedTopLevel
295+
296+
// We're not using a manifest list, so require verification of the single arch manifest.
297+
if toplevelVerificationErr != nil {
298+
return ret, toplevelVerificationErr
299+
}
300+
}
301+
302+
img, err := image.FromUnparsedImage(ctx, h.sysctx, target)
303+
if err != nil {
304+
return ret, err
263305
}
264306

265307
// Note that we never return zero as an imageid; this code doesn't yet
@@ -268,6 +310,7 @@ func (h *proxyHandler) openImageImpl(args []any, allowNotFound bool) (retReplyBu
268310
openimg := &openImage{
269311
id: h.imageSerial,
270312
src: imgsrc,
313+
img: img,
271314
}
272315
h.images[openimg.id] = openimg
273316
ret.value = openimg.id
@@ -367,44 +410,6 @@ func (h *proxyHandler) returnBytes(retval any, buf []byte) (replyBuf, error) {
367410
return ret, nil
368411
}
369412

370-
// cacheTargetManifest is invoked when GetManifest or GetConfig is invoked
371-
// the first time for a given image. If the requested image is a manifest
372-
// list, this function resolves it to the image matching the calling process'
373-
// operating system and architecture.
374-
//
375-
// TODO: Add GetRawManifest or so that exposes manifest lists
376-
func (h *proxyHandler) cacheTargetManifest(img *openImage) error {
377-
ctx := context.Background()
378-
if img.cachedimg != nil {
379-
return nil
380-
}
381-
unparsedToplevel := image.UnparsedInstance(img.src, nil)
382-
mfest, manifestType, err := unparsedToplevel.Manifest(ctx)
383-
if err != nil {
384-
return err
385-
}
386-
var target *image.UnparsedImage
387-
if manifest.MIMETypeIsMultiImage(manifestType) {
388-
manifestList, err := manifest.ListFromBlob(mfest, manifestType)
389-
if err != nil {
390-
return err
391-
}
392-
instanceDigest, err := manifestList.ChooseInstance(h.sysctx)
393-
if err != nil {
394-
return err
395-
}
396-
target = image.UnparsedInstance(img.src, &instanceDigest)
397-
} else {
398-
target = unparsedToplevel
399-
}
400-
cachedimg, err := image.FromUnparsedImage(ctx, h.sysctx, target)
401-
if err != nil {
402-
return err
403-
}
404-
img.cachedimg = cachedimg
405-
return nil
406-
}
407-
408413
// GetManifest returns a copy of the manifest, converted to OCI format, along with the original digest.
409414
// Manifest lists are resolved to the current operating system and architecture.
410415
func (h *proxyHandler) GetManifest(args []any) (replyBuf, error) {
@@ -424,14 +429,8 @@ func (h *proxyHandler) GetManifest(args []any) (replyBuf, error) {
424429
return ret, err
425430
}
426431

427-
err = h.cacheTargetManifest(imgref)
428-
if err != nil {
429-
return ret, err
430-
}
431-
img := imgref.cachedimg
432-
433432
ctx := context.Background()
434-
rawManifest, manifestType, err := img.Manifest(ctx)
433+
rawManifest, manifestType, err := imgref.img.Manifest(ctx)
435434
if err != nil {
436435
return ret, err
437436
}
@@ -460,7 +459,7 @@ func (h *proxyHandler) GetManifest(args []any) (replyBuf, error) {
460459
// docker schema and MIME types.
461460
if manifestType != imgspecv1.MediaTypeImageManifest {
462461
manifestUpdates := types.ManifestUpdateOptions{ManifestMIMEType: imgspecv1.MediaTypeImageManifest}
463-
ociImage, err := img.UpdatedImage(ctx, manifestUpdates)
462+
ociImage, err := imgref.img.UpdatedImage(ctx, manifestUpdates)
464463
if err != nil {
465464
return ret, err
466465
}
@@ -494,14 +493,9 @@ func (h *proxyHandler) GetFullConfig(args []any) (replyBuf, error) {
494493
if err != nil {
495494
return ret, err
496495
}
497-
err = h.cacheTargetManifest(imgref)
498-
if err != nil {
499-
return ret, err
500-
}
501-
img := imgref.cachedimg
502496

503497
ctx := context.TODO()
504-
config, err := img.OCIConfig(ctx)
498+
config, err := imgref.img.OCIConfig(ctx)
505499
if err != nil {
506500
return ret, err
507501
}
@@ -531,14 +525,9 @@ func (h *proxyHandler) GetConfig(args []any) (replyBuf, error) {
531525
if err != nil {
532526
return ret, err
533527
}
534-
err = h.cacheTargetManifest(imgref)
535-
if err != nil {
536-
return ret, err
537-
}
538-
img := imgref.cachedimg
539528

540529
ctx := context.TODO()
541-
config, err := img.OCIConfig(ctx)
530+
config, err := imgref.img.OCIConfig(ctx)
542531
if err != nil {
543532
return ret, err
544533
}
@@ -641,19 +630,13 @@ func (h *proxyHandler) GetLayerInfo(args []any) (replyBuf, error) {
641630

642631
ctx := context.TODO()
643632

644-
err = h.cacheTargetManifest(imgref)
645-
if err != nil {
646-
return ret, err
647-
}
648-
img := imgref.cachedimg
649-
650-
layerInfos, err := img.LayerInfosForCopy(ctx)
633+
layerInfos, err := imgref.img.LayerInfosForCopy(ctx)
651634
if err != nil {
652635
return ret, err
653636
}
654637

655638
if layerInfos == nil {
656-
layerInfos = img.LayerInfos()
639+
layerInfos = imgref.img.LayerInfos()
657640
}
658641

659642
layers := make([]convertedLayerInfo, 0, len(layerInfos))
@@ -699,7 +682,7 @@ func (h *proxyHandler) close() {
699682
err := image.src.Close()
700683
if err != nil {
701684
// This shouldn't be fatal
702-
logrus.Warnf("Failed to close image %s: %v", transports.ImageName(image.cachedimg.Reference()), err)
685+
logrus.Warnf("Failed to close image %s: %v", transports.ImageName(image.img.Reference()), err)
703686
}
704687
}
705688

integration/proxy_test.go

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ import (
1919
"github.com/stretchr/testify/suite"
2020
)
2121

22-
// This image is known to be x86_64 only right now
23-
const knownNotManifestListedImageX8664 = "docker://quay.io/coreos/11bot"
22+
// This image is known to be x86_64 only right now; TODO push
23+
// a non-manifest-listed fixture to quay.io/libpod
24+
const knownNotManifestListedImageX8664 = "docker://quay.io/cgwalters/ostest"
2425

2526
// knownNotExtantImage would be very surprising if it did exist
2627
const knownNotExtantImage = "docker://quay.io/centos/centos:opensusewindowsubuntu"
@@ -173,7 +174,12 @@ func (p *proxy) callReadAllBytes(method string, args []any) (rval any, buf []byt
173174
return
174175
}
175176

176-
func newProxy() (*proxy, error) {
177+
type proxyConfig struct {
178+
// policyFilePath is equivalent to skopeo --policy, an empty value is treated as unset
179+
policyFilePath string
180+
}
181+
182+
func newProxy(config proxyConfig) (*proxy, error) {
177183
fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_SEQPACKET, 0)
178184
if err != nil {
179185
return nil, err
@@ -189,7 +195,12 @@ func newProxy() (*proxy, error) {
189195
}
190196

191197
// Note ExtraFiles starts at 3
192-
proc := exec.Command("skopeo", "experimental-image-proxy", "--sockfd", "3")
198+
args := []string{}
199+
if config.policyFilePath != "" {
200+
args = append(args, "--policy", config.policyFilePath)
201+
}
202+
args = append(args, "experimental-image-proxy", "--sockfd", "3")
203+
proc := exec.Command("skopeo", args...)
193204
proc.Stderr = os.Stderr
194205
cmdLifecycleToParentIfPossible(proc)
195206
proc.ExtraFiles = append(proc.ExtraFiles, theirfd)
@@ -334,7 +345,10 @@ func runTestOpenImageOptionalNotFound(p *proxy, img string) error {
334345

335346
func (s *proxySuite) TestProxy() {
336347
t := s.T()
337-
p, err := newProxy()
348+
config := proxyConfig{
349+
policyFilePath: "",
350+
}
351+
p, err := newProxy(config)
338352
require.NoError(t, err)
339353

340354
err = runTestGetManifestAndConfig(p, knownNotManifestListedImageX8664)
@@ -355,3 +369,29 @@ func (s *proxySuite) TestProxy() {
355369
}
356370
assert.NoError(t, err)
357371
}
372+
373+
// Verify that a policy that denies all images is correctly rejected
374+
// for both manifest listed and direct per-arch images.
375+
func (s *proxySuite) TestProxyPolicyRejectAll() {
376+
t := s.T()
377+
tempd := t.TempDir()
378+
config := proxyConfig{
379+
policyFilePath: tempd + "/policy.json",
380+
}
381+
os.WriteFile(config.policyFilePath, []byte("{ \"default\": [ { \"type\":\"reject\"} ] }"), 0o644)
382+
p, err := newProxy(config)
383+
require.NoError(t, err)
384+
385+
err = runTestGetManifestAndConfig(p, knownNotManifestListedImageX8664)
386+
assert.ErrorContains(t, err, "is rejected by policy")
387+
388+
err = runTestGetManifestAndConfig(p, knownListImage)
389+
assert.ErrorContains(t, err, "is rejected by policy")
390+
391+
// This one should continue to be fine.
392+
err = runTestOpenImageOptionalNotFound(p, knownNotExtantImage)
393+
if err != nil {
394+
err = fmt.Errorf("Testing optional image %s: %v", knownNotExtantImage, err)
395+
}
396+
assert.NoError(t, err)
397+
}

0 commit comments

Comments
 (0)