Skip to content

Commit ef7b873

Browse files
committed
PolicyContext: add new SetRejectInsecure method
In bootc, we want the ability to assert that signature verification is enforced, but there are no mechanisms for this in the library. Add a new `SetRejectInsecure` method on the `PolicyContext` object which would allow this. Add a new `isInsecure` method on the `PolicyRequirement` interface which then allows `IsRunningImageAllowed` to detect if at least one secure requirement was present. Test generation was `Assisted-by: Claude Code v1.0.120`. Part of containers/skopeo#1829. Signed-off-by: Jonathan Lebon <[email protected]>
1 parent 92222dc commit ef7b873

File tree

6 files changed

+119
-2
lines changed

6 files changed

+119
-2
lines changed

image/signature/policy_eval.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/sirupsen/logrus"
1313
"go.podman.io/image/v5/internal/private"
1414
"go.podman.io/image/v5/internal/unparsedimage"
15+
"go.podman.io/image/v5/transports"
1516
"go.podman.io/image/v5/types"
1617
)
1718

@@ -65,6 +66,9 @@ type PolicyRequirement interface {
6566
// WARNING: This validates signatures and the manifest, but does not download or validate the
6667
// layers. Users must validate that the layers match their expected digests.
6768
isRunningImageAllowed(ctx context.Context, image private.UnparsedImage) (bool, error)
69+
70+
// isInsecure returns true if the requirement allows images without any signatures.
71+
isInsecure() bool
6872
}
6973

7074
// PolicyReferenceMatch specifies a set of image identities accepted in PolicyRequirement.
@@ -79,8 +83,9 @@ type PolicyReferenceMatch interface {
7983
// PolicyContext encapsulates a policy and possible cached state
8084
// for speeding up its evaluation.
8185
type PolicyContext struct {
82-
Policy *Policy
83-
state policyContextState // Internal consistency checking
86+
Policy *Policy
87+
state policyContextState // Internal consistency checking
88+
rejectInsecure bool
8489
}
8590

8691
// policyContextState is used internally to verify the users are not misusing a PolicyContext.
@@ -132,6 +137,13 @@ func policyIdentityLogName(ref types.ImageReference) string {
132137
return ref.Transport().Name() + ":" + ref.PolicyConfigurationIdentity()
133138
}
134139

140+
// SetRejectInsecure modifies insecure policy requirement handling. If
141+
// passed `true`, policy checking by IsRunningImageAllowed will ignore the
142+
// "insecureAcceptAnything" policy type.
143+
func (pc *PolicyContext) SetRejectInsecure(val bool) {
144+
pc.rejectInsecure = val
145+
}
146+
135147
// requirementsForImageRef selects the appropriate requirements for ref.
136148
func (pc *PolicyContext) requirementsForImageRef(ref types.ImageReference) PolicyRequirements {
137149
// Do we have a PolicyTransportScopes for this transport?
@@ -278,6 +290,7 @@ func (pc *PolicyContext) IsRunningImageAllowed(ctx context.Context, publicImage
278290
return false, PolicyRequirementError("List of verification policy requirements must not be empty")
279291
}
280292

293+
wasSecure := false
281294
for reqNumber, req := range reqs {
282295
// FIXME: supply state
283296
allowed, err := req.isRunningImageAllowed(ctx, image)
@@ -286,7 +299,15 @@ func (pc *PolicyContext) IsRunningImageAllowed(ctx context.Context, publicImage
286299
return false, err
287300
}
288301
logrus.Debugf(" Requirement %d: allowed", reqNumber)
302+
if !req.isInsecure() {
303+
wasSecure = true
304+
}
289305
}
306+
307+
if pc.rejectInsecure && !wasSecure {
308+
return false, PolicyRequirementError(fmt.Sprintf("No secure policy found for image %s.", transports.ImageName(image.Reference())))
309+
}
310+
290311
// We have tested that len(reqs) != 0, so at least one req must have explicitly allowed this image.
291312
logrus.Debugf("Overall: allowed")
292313
return true, nil

image/signature/policy_eval_baselayer.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ func (pr *prSignedBaseLayer) isRunningImageAllowed(ctx context.Context, image pr
1818
logrus.Errorf("signedBaseLayer not implemented yet!")
1919
return false, PolicyRequirementError("signedBaseLayer not implemented yet!")
2020
}
21+
22+
func (pr *prSignedBaseLayer) isInsecure() bool {
23+
return false
24+
}

image/signature/policy_eval_signedby.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,7 @@ func (pr *prSignedBy) isRunningImageAllowed(ctx context.Context, image private.U
114114
}
115115
return false, summary
116116
}
117+
118+
func (pr *prSignedBy) isInsecure() bool {
119+
return false
120+
}

image/signature/policy_eval_sigstore.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,3 +433,7 @@ func (pr *prSigstoreSigned) isRunningImageAllowed(ctx context.Context, image pri
433433
}
434434
return false, summary
435435
}
436+
437+
func (pr *prSigstoreSigned) isInsecure() bool {
438+
return false
439+
}

image/signature/policy_eval_simple.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,18 @@ func (pr *prInsecureAcceptAnything) isRunningImageAllowed(ctx context.Context, i
2020
return true, nil
2121
}
2222

23+
func (pr *prInsecureAcceptAnything) isInsecure() bool {
24+
return true
25+
}
26+
2327
func (pr *prReject) isSignatureAuthorAccepted(ctx context.Context, image private.UnparsedImage, sig []byte) (signatureAcceptanceResult, *Signature, error) {
2428
return sarRejected, nil, PolicyRequirementError(fmt.Sprintf("Any signatures for image %s are rejected by policy.", transports.ImageName(image.Reference())))
2529
}
2630

2731
func (pr *prReject) isRunningImageAllowed(ctx context.Context, image private.UnparsedImage) (bool, error) {
2832
return false, PolicyRequirementError(fmt.Sprintf("Running image %s is rejected by policy.", transports.ImageName(image.Reference())))
2933
}
34+
35+
func (pr *prReject) isInsecure() bool {
36+
return false
37+
}

image/signature/policy_eval_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,3 +497,79 @@ func assertRunningRejectedPolicyRequirement(t *testing.T, allowed bool, err erro
497497
assertRunningRejected(t, allowed, err)
498498
assert.IsType(t, PolicyRequirementError(""), err)
499499
}
500+
501+
func TestPolicyContextSetRejectInsecure(t *testing.T) {
502+
pc, err := NewPolicyContext(&Policy{Default: PolicyRequirements{NewPRReject()}})
503+
require.NoError(t, err)
504+
defer func() {
505+
err := pc.Destroy()
506+
require.NoError(t, err)
507+
}()
508+
509+
// Test default value is false
510+
assert.False(t, pc.rejectInsecure)
511+
512+
// Test setting to true
513+
pc.SetRejectInsecure(true)
514+
assert.True(t, pc.rejectInsecure)
515+
516+
// Test setting back to false
517+
pc.SetRejectInsecure(false)
518+
assert.False(t, pc.rejectInsecure)
519+
}
520+
521+
func TestPolicyContextIsRunningImageAllowedWithRejectInsecure(t *testing.T) {
522+
pc, err := NewPolicyContext(&Policy{
523+
Default: PolicyRequirements{NewPRReject()},
524+
Transports: map[string]PolicyTransportScopes{
525+
"docker": {
526+
"docker.io/testing/manifest:insecureOnly": {
527+
NewPRInsecureAcceptAnything(),
528+
},
529+
"docker.io/testing/manifest:insecureWithOther": {
530+
NewPRInsecureAcceptAnything(),
531+
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
532+
},
533+
"docker.io/testing/manifest:signedOnly": {
534+
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
535+
},
536+
},
537+
},
538+
})
539+
require.NoError(t, err)
540+
defer func() {
541+
err := pc.Destroy()
542+
require.NoError(t, err)
543+
}()
544+
545+
// Test with rejectInsecure=false (default behavior)
546+
// insecureAcceptAnything should be accepted
547+
img := pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:insecureOnly")
548+
res, err := pc.IsRunningImageAllowed(context.Background(), img)
549+
assertRunningAllowed(t, res, err)
550+
551+
// Test with rejectInsecure=true
552+
pc.SetRejectInsecure(true)
553+
554+
// insecureAcceptAnything only: should be rejected (leaves no secure requirements)
555+
img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:insecureOnly")
556+
res, err = pc.IsRunningImageAllowed(context.Background(), img)
557+
assert.Equal(t, false, res)
558+
assert.Error(t, err)
559+
560+
// insecureAcceptAnything + signed requirement: first requirement has no effect, second is secure and valid
561+
img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:insecureWithOther")
562+
res, err = pc.IsRunningImageAllowed(context.Background(), img)
563+
assertRunningAllowed(t, res, err)
564+
565+
// signed requirement only: should work normally
566+
img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:signedOnly")
567+
res, err = pc.IsRunningImageAllowed(context.Background(), img)
568+
assertRunningAllowed(t, res, err)
569+
570+
// Test with unsigned image and insecureAcceptAnything + signed requirement: first requirement has no effect, second is secure but rejects
571+
img = pcImageMock(t, "fixtures/dir-img-unsigned", "testing/manifest:insecureWithOther")
572+
res, err = pc.IsRunningImageAllowed(context.Background(), img)
573+
assert.Equal(t, false, res)
574+
assert.Error(t, err)
575+
}

0 commit comments

Comments
 (0)