Skip to content

Commit 47ee7e4

Browse files
committed
PolicyContext: add new RequireSignatureVerification 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 `RequireSignatureVerification` method on the `PolicyContext` object which would allow this. Add a new `isSigned` method on the `PolicyRequirement` interface which then allows `IsRunningImageAllowed` to detect if at least one requirement performed signature verification. Test generation was `Assisted-by: Claude Code v1.0.120`. Part of containers/skopeo#1829. Signed-off-by: Jonathan Lebon <[email protected]>
1 parent 397182c commit 47ee7e4

File tree

6 files changed

+117
-2
lines changed

6 files changed

+117
-2
lines changed

image/signature/policy_eval.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ type PolicyRequirement interface {
6565
// WARNING: This validates signatures and the manifest, but does not download or validate the
6666
// layers. Users must validate that the layers match their expected digests.
6767
isRunningImageAllowed(ctx context.Context, image private.UnparsedImage) (bool, error)
68+
69+
// verifiesSignatures returns true if and only if the requirement performs cryptographic
70+
// signature verification on the entire contents of the image before allowing it.
71+
verifiesSignatures() 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+
requireSigned 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+
// RequireSignatureVerification modifies policy requirement handling. If passed
141+
// `true`, at least one policy requirement which performs signature verification
142+
// on the entire image contents must be present.
143+
func (pc *PolicyContext) RequireSignatureVerification(val bool) {
144+
pc.requireSigned = 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+
wasSignatureVerified := 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.verifiesSignatures() {
303+
wasSignatureVerified = true
304+
}
289305
}
306+
307+
if pc.requireSigned && !wasSignatureVerified {
308+
return false, PolicyRequirementError(fmt.Sprintf("No signature verification policy found for image %s", policyIdentityLogName(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) verifiesSignatures() 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) verifiesSignatures() bool {
119+
return true
120+
}

image/signature/policy_eval_sigstore.go

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

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) verifiesSignatures() bool {
24+
return false
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) verifiesSignatures() bool {
36+
return false
37+
}

image/signature/policy_eval_test.go

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

0 commit comments

Comments
 (0)