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.
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) {
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())))
}

// We have tested that len(reqs) != 0, so at least one req must have explicitly allowed this image.
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.

}
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)
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