Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions image/signature/policy_eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/sirupsen/logrus"
"go.podman.io/image/v5/internal/private"
"go.podman.io/image/v5/internal/unparsedimage"
"go.podman.io/image/v5/transports"
"go.podman.io/image/v5/types"
)

Expand Down Expand Up @@ -65,6 +66,9 @@ type PolicyRequirement interface {
// WARNING: This validates signatures and the manifest, but does not download or validate the
// layers. Users must validate that the layers match their expected digests.
isRunningImageAllowed(ctx context.Context, image private.UnparsedImage) (bool, error)

// isInsecure returns true if the requirement allows images without any signatures.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“allows images with unauthenticated contents”, per #355 (comment) ?

(A possible hypothetical to think about is a PolicyRequirement that “the input reference is a digested reference” — in that case the contents of the image would be authenticated, but by the caller-provided input, not cryptographically. “allows images with unsigned contents”, maybe? Which of the two?)

isInsecure() bool
}

// PolicyReferenceMatch specifies a set of image identities accepted in PolicyRequirement.
Expand All @@ -79,8 +83,9 @@ type PolicyReferenceMatch interface {
// PolicyContext encapsulates a policy and possible cached state
// for speeding up its evaluation.
type PolicyContext struct {
Policy *Policy
state policyContextState // Internal consistency checking
Policy *Policy
state policyContextState // Internal consistency checking
rejectInsecure bool
}

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

// SetRejectInsecure modifies insecure policy requirement handling. If
// passed `true`, policy checking by IsRunningImageAllowed will ignore the
// "insecureAcceptAnything" policy type.
func (pc *PolicyContext) SetRejectInsecure(val bool) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description (and name?) will need updating per the “authenticated contents”/“signed contents” discussion elsewhere.

(Choose the semantics that bootc needs; we can always add one more option with some other semantics in the future if it turned out to be necessary for other users.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Thinking whether this should reject repeated calls, as an indication of a confused caller … we’d have to add an error return, and all callers would need to check it for all of this to make a difference — that wouldn’t really work, let’s not do that.)

pc.rejectInsecure = val
}

// requirementsForImageRef selects the appropriate requirements for ref.
func (pc *PolicyContext) requirementsForImageRef(ref types.ImageReference) PolicyRequirements {
// Do we have a PolicyTransportScopes for this transport?
Expand Down Expand Up @@ -278,6 +290,7 @@ func (pc *PolicyContext) IsRunningImageAllowed(ctx context.Context, publicImage
return false, PolicyRequirementError("List of verification policy requirements must not be empty")
}

wasSecure := false
for reqNumber, req := range reqs {
// FIXME: supply state
allowed, err := req.isRunningImageAllowed(ctx, image)
Expand All @@ -286,7 +299,15 @@ func (pc *PolicyContext) IsRunningImageAllowed(ctx context.Context, publicImage
return false, err
}
logrus.Debugf(" Requirement %d: allowed", reqNumber)
if !req.isInsecure() {
wasSecure = true
}
}

if pc.rejectInsecure && !wasSecure {
return false, PolicyRequirementError(fmt.Sprintf("No secure policy found for image %s.", transports.ImageName(image.Reference())))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

policyIdentityLogName would be a bit more appropriate, it would more directly point at the relevant policy.json scope.

}

// We have tested that len(reqs) != 0, so at least one req must have explicitly allowed this image.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Is it needed to update this comment? It seems that having at least one req allowed is not enough to get to R312, since R299 will do a early return if at least one req is not allowed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Either way, I don’t think this PR changes the situation.)

Copy link
Contributor

@mtrmac mtrmac Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is trying to say “if we get to this point, len(reqs) > 0 and there was at least one req where allowed == true”, not “if len(reqs) > 0 and there was at least one req where allowed == true, we always get to this point”.

logrus.Debugf("Overall: allowed")
return true, nil
Expand Down
4 changes: 4 additions & 0 deletions image/signature/policy_eval_baselayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ func (pr *prSignedBaseLayer) isRunningImageAllowed(ctx context.Context, image pr
logrus.Errorf("signedBaseLayer not implemented yet!")
return false, PolicyRequirementError("signedBaseLayer not implemented yet!")
}

func (pr *prSignedBaseLayer) isInsecure() bool {
return false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deciding whether “signed base layer” is “secure” is … a weird question. Not really an interesting question, given that this is an unusable stub…

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I guess I should probably just flip this to true for now and add a FIXME here too to reconsider once it's implemented. It definitely stresses the binary secure vs insecure logic, so it might have to be reworked a bit at that point based on what semantics we want.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading #355 (comment) , if the semantics is “we authenticated the image at least once”, prSignedBaseLayer does not meaningfully do that — a child layer on top of the signed ones can change the image in unrestricted ways.

So I think this should just be true, and no FIXME necessary.

}
4 changes: 4 additions & 0 deletions image/signature/policy_eval_signedby.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,7 @@ func (pr *prSignedBy) isRunningImageAllowed(ctx context.Context, image private.U
}
return false, summary
}

func (pr *prSignedBy) isInsecure() bool {
return false
}
4 changes: 4 additions & 0 deletions image/signature/policy_eval_sigstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,3 +433,7 @@ func (pr *prSigstoreSigned) isRunningImageAllowed(ctx context.Context, image pri
}
return false, summary
}

func (pr *prSigstoreSigned) isInsecure() bool {
return false
}
8 changes: 8 additions & 0 deletions image/signature/policy_eval_simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,18 @@ func (pr *prInsecureAcceptAnything) isRunningImageAllowed(ctx context.Context, i
return true, nil
}

func (pr *prInsecureAcceptAnything) isInsecure() bool {
return true
}

func (pr *prReject) isSignatureAuthorAccepted(ctx context.Context, image private.UnparsedImage, sig []byte) (signatureAcceptanceResult, *Signature, error) {
return sarRejected, nil, PolicyRequirementError(fmt.Sprintf("Any signatures for image %s are rejected by policy.", transports.ImageName(image.Reference())))
}

func (pr *prReject) isRunningImageAllowed(ctx context.Context, image private.UnparsedImage) (bool, error) {
return false, PolicyRequirementError(fmt.Sprintf("Running image %s is rejected by policy.", transports.ImageName(image.Reference())))
}

func (pr *prReject) isInsecure() bool {
return false
}
76 changes: 76 additions & 0 deletions image/signature/policy_eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,3 +497,79 @@ func assertRunningRejectedPolicyRequirement(t *testing.T, allowed bool, err erro
assertRunningRejected(t, allowed, err)
assert.IsType(t, PolicyRequirementError(""), err)
}

func TestPolicyContextSetRejectInsecure(t *testing.T) {
pc, err := NewPolicyContext(&Policy{Default: PolicyRequirements{NewPRReject()}})
require.NoError(t, err)
defer func() {
err := pc.Destroy()
require.NoError(t, err)
}()

// Test default value is false
assert.False(t, pc.rejectInsecure)

// Test setting to true
pc.SetRejectInsecure(true)
assert.True(t, pc.rejectInsecure)

// Test setting back to false
pc.SetRejectInsecure(false)
assert.False(t, pc.rejectInsecure)
}

func TestPolicyContextIsRunningImageAllowedWithRejectInsecure(t *testing.T) {
pc, err := NewPolicyContext(&Policy{
Default: PolicyRequirements{NewPRReject()},
Transports: map[string]PolicyTransportScopes{
"docker": {
"docker.io/testing/manifest:insecureOnly": {
NewPRInsecureAcceptAnything(),
},
"docker.io/testing/manifest:insecureWithOther": {
NewPRInsecureAcceptAnything(),
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
},
"docker.io/testing/manifest:signedOnly": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
},
},
},
})
require.NoError(t, err)
defer func() {
err := pc.Destroy()
require.NoError(t, err)
}()

// Test with rejectInsecure=false (default behavior)
// insecureAcceptAnything should be accepted
img := pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:insecureOnly")
res, err := pc.IsRunningImageAllowed(context.Background(), img)
assertRunningAllowed(t, res, err)

// Test with rejectInsecure=true
pc.SetRejectInsecure(true)

// insecureAcceptAnything only: should be rejected (leaves no secure requirements)
img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:insecureOnly")
res, err = pc.IsRunningImageAllowed(context.Background(), img)
assert.Equal(t, false, res)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assertRunningRejectedPolicyRequirement please, in both of the “error” situations.

assert.Error(t, err)

// insecureAcceptAnything + signed requirement: first requirement has no effect, second is secure and valid
img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:insecureWithOther")
res, err = pc.IsRunningImageAllowed(context.Background(), img)
assertRunningAllowed(t, res, err)

// signed requirement only: should work normally
img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:signedOnly")
res, err = pc.IsRunningImageAllowed(context.Background(), img)
assertRunningAllowed(t, res, err)

// Test with unsigned image and insecureAcceptAnything + signed requirement: first requirement has no effect, second is secure but rejects
img = pcImageMock(t, "fixtures/dir-img-unsigned", "testing/manifest:insecureWithOther")
res, err = pc.IsRunningImageAllowed(context.Background(), img)
assert.Equal(t, false, res)
assert.Error(t, err)
}
Loading