Skip to content

Commit dd4b1c9

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 dd4b1c9

File tree

1 file changed

+44
-59
lines changed

1 file changed

+44
-59
lines changed

cmd/skopeo/proxy.go

Lines changed: 44 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -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,20 +255,58 @@ 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 (as some signing
290+
// systems are known to do this).
291+
if targetVerificationErr != nil && toplevelVerificationErr != nil {
292+
return ret, toplevelVerificationErr
293+
}
294+
} else {
295+
target = unparsedTopLevel
296+
}
297+
298+
cachedimg, err := image.FromUnparsedImage(ctx, h.sysctx, target)
299+
if err != nil {
300+
return ret, err
263301
}
264302

265303
// Note that we never return zero as an imageid; this code doesn't yet
266304
// handle overflow though.
267305
h.imageSerial++
268306
openimg := &openImage{
269-
id: h.imageSerial,
270-
src: imgsrc,
307+
id: h.imageSerial,
308+
src: imgsrc,
309+
cachedimg: cachedimg,
271310
}
272311
h.images[openimg.id] = openimg
273312
ret.value = openimg.id
@@ -367,44 +406,6 @@ func (h *proxyHandler) returnBytes(retval any, buf []byte) (replyBuf, error) {
367406
return ret, nil
368407
}
369408

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-
408409
// GetManifest returns a copy of the manifest, converted to OCI format, along with the original digest.
409410
// Manifest lists are resolved to the current operating system and architecture.
410411
func (h *proxyHandler) GetManifest(args []any) (replyBuf, error) {
@@ -424,10 +425,6 @@ func (h *proxyHandler) GetManifest(args []any) (replyBuf, error) {
424425
return ret, err
425426
}
426427

427-
err = h.cacheTargetManifest(imgref)
428-
if err != nil {
429-
return ret, err
430-
}
431428
img := imgref.cachedimg
432429

433430
ctx := context.Background()
@@ -494,10 +491,6 @@ func (h *proxyHandler) GetFullConfig(args []any) (replyBuf, error) {
494491
if err != nil {
495492
return ret, err
496493
}
497-
err = h.cacheTargetManifest(imgref)
498-
if err != nil {
499-
return ret, err
500-
}
501494
img := imgref.cachedimg
502495

503496
ctx := context.TODO()
@@ -531,10 +524,6 @@ func (h *proxyHandler) GetConfig(args []any) (replyBuf, error) {
531524
if err != nil {
532525
return ret, err
533526
}
534-
err = h.cacheTargetManifest(imgref)
535-
if err != nil {
536-
return ret, err
537-
}
538527
img := imgref.cachedimg
539528

540529
ctx := context.TODO()
@@ -641,10 +630,6 @@ 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-
}
648633
img := imgref.cachedimg
649634

650635
layerInfos, err := img.LayerInfosForCopy(ctx)

0 commit comments

Comments
 (0)