Add PolicyContext, with GetSignaturesWithAcceptedAuthor and IsRunningImageAllowed

PolicyContext is intended to be the primary API for skopeo/signature:
supply a policy and an image, and ask specific, well-defined
(preferably yes/no) questions.
This commit is contained in:
Miloslav Trmač 2016-05-31 17:09:43 +02:00
parent fd9c615d88
commit 21229685cf
2 changed files with 719 additions and 1 deletions

View File

@ -1,6 +1,19 @@
// This defines the top-level policy evaluation API.
// To the extent possible, the interface of the fuctions provided
// here is intended to be completely unambiguous, and stable for users
// to rely on.
package signature
import "github.com/projectatomic/skopeo/types"
import (
"fmt"
"strings"
"github.com/Sirupsen/logrus"
distreference "github.com/docker/distribution/reference"
"github.com/projectatomic/skopeo/reference"
"github.com/projectatomic/skopeo/types"
)
// PolicyRequirementError is an explanatory text for rejecting a signature or an image.
type PolicyRequirementError string
@ -49,6 +62,8 @@ type PolicyRequirement interface {
// isRunningImageAllowed returns true if the requirement allows running an image.
// If it returns false, err must be non-nil, and should be an PolicyRequirementError if evaluation
// succeeded but the result was rejection.
// 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(image types.Image) (bool, error)
}
@ -59,3 +74,261 @@ type PolicyReferenceMatch interface {
// (or, usually, for the image's IntendedDockerReference()),
matchesDockerReference(image types.Image, signatureDockerReference string) bool
}
// PolicyContext encapsulates a policy and possible cached state
// for speeding up its evaluation.
type PolicyContext struct {
Policy *Policy
state policyContextState // Internal consistency checking
}
// policyContextState is used internally to verify the users are not misusing a PolicyContext.
type policyContextState string
const (
pcInvalid policyContextState = ""
pcInitializing policyContextState = "Initializing"
pcReady policyContextState = "Ready"
pcInUse policyContextState = "InUse"
pcDestroying policyContextState = "Destroying"
pcDestroyed policyContextState = "Destroyed"
)
// changeContextState changes pc.state, or fails if the state is unexpected
func (pc *PolicyContext) changeState(expected, new policyContextState) error {
if pc.state != expected {
return fmt.Errorf(`"Invalid PolicyContext state, expected "%s", found "%s"`, expected, pc.state)
}
pc.state = new
return nil
}
// NewPolicyContext sets up and initializes a context for the specified policy.
// The policy must not be modified while the context exists. FIXME: make a deep copy?
// If this function succeeds, the caller should call PolicyContext.Destroy() when done.
func NewPolicyContext(policy *Policy) (*PolicyContext, error) {
pc := &PolicyContext{Policy: policy, state: pcInitializing}
// FIXME: initialize
if err := pc.changeState(pcInitializing, pcReady); err != nil {
// Huh?! This should never fail, we didn't give the pointer to anybody.
// Just give up and leave unclean state around.
return nil, err
}
return pc, nil
}
// Destroy should be called when the user of the context is done with it.
func (pc *PolicyContext) Destroy() error {
if err := pc.changeState(pcReady, pcDestroying); err != nil {
return err
}
// FIXME: destroy
return pc.changeState(pcDestroying, pcDestroyed)
}
// fullyExpandedDockerReference converts a reference.Named into a fully expanded format;
// i.e. soft of an opposite to ref.String(), which is a fully canonicalized/minimized format.
// This is guaranteed to be the same as reference.FullName(), with a tag or digest appended, if available.
// FIXME? This feels like it should be provided by skopeo/reference.
func fullyExpandedDockerReference(ref reference.Named) (string, error) {
res := ref.FullName()
tagged, isTagged := ref.(distreference.Tagged)
digested, isDigested := ref.(distreference.Digested)
// A github.com/distribution/reference value can have a tag and a digest at the same time!
// skopeo/reference does not handle that, so fail.
// FIXME? Should we support that?
switch {
case isTagged && isDigested:
// Coverage: This should currently not happen, the way skopeo/reference sets up types,
// isTagged and isDigested is mutually exclusive.
return "", fmt.Errorf("Names with both a tag and digest are not currently supported")
case isTagged:
res = res + ":" + tagged.Tag()
case isDigested:
res = res + "@" + digested.Digest().String()
default:
// res is already OK.
}
return res, nil
}
// requirementsForImage selects the appropriate requirements for image.
func (pc *PolicyContext) requirementsForImage(image types.Image) (PolicyRequirements, error) {
imageIdentity := image.IntendedDockerReference()
// We don't technically need to parse it first in order to match the full name:tag,
// but do so anyway to ensure that the intended identity really does follow that
// format, or at least that it is not demonstrably wrong.
ref, err := reference.ParseNamed(imageIdentity)
if err != nil {
return nil, err
}
ref = reference.WithDefaultTag(ref)
// Look for a full match.
fullyExpanded, err := fullyExpandedDockerReference(ref)
if err != nil { // Coverage: This cannot currently happen.
return nil, err
}
if req, ok := pc.Policy.Specific[fullyExpanded]; ok {
logrus.Debugf(" Using specific policy section %s", fullyExpanded)
return req, nil
}
// Look for a match of the repository, and then of the possible parent
// namespaces. Note that this only happens on the expanded host names
// and repository names, i.e. "busybox" is looked up as "docker.io/library/busybox",
// then in its parent "docker.io/library"; in none of "busybox",
// un-namespaced "library" nor in "" implicitly representing "library/".
//
// ref.FullName() == ref.Hostname() + "/" + ref.RemoteName(), so the last
// iteration matches the host name (for any namespace).
name := ref.FullName()
for {
if req, ok := pc.Policy.Specific[name]; ok {
logrus.Debugf(" Using specific policy section %s", name)
return req, nil
}
lastSlash := strings.LastIndex(name, "/")
if lastSlash == -1 {
break
}
name = name[:lastSlash]
}
logrus.Debugf(" Using default policy section")
return pc.Policy.Default, nil
}
// GetSignaturesWithAcceptedAuthor returns those signatures from an image
// for which the policy accepts the author (and which have been successfully
// verified).
// NOTE: This may legitimately return an empty list and no error, if the image
// has no signatures or only invalid signatures.
// WARNING: This makes the signature contents acceptable for futher processing,
// but it does not necessarily mean that the contents of the signature are
// consistent with local policy.
// For example:
// - Do not use a an existence of an accepted signature to determine whether to run
// a container based on this image; use IsRunningImageAllowed instead.
// - Just because a signature is accepted does not automatically mean the contents of the
// signature are authorized to run code as root, or to affect system or cluster configuration.
func (pc *PolicyContext) GetSignaturesWithAcceptedAuthor(image types.Image) (sigs []*Signature, finalErr error) {
if err := pc.changeState(pcReady, pcInUse); err != nil {
return nil, err
}
defer func() {
if err := pc.changeState(pcInUse, pcReady); err != nil {
sigs = nil
finalErr = err
}
}()
logrus.Debugf("GetSignaturesWithAcceptedAuthor for image %s", image.IntendedDockerReference())
reqs, err := pc.requirementsForImage(image)
if err != nil {
return nil, err
}
// FIXME: rename Signatures to UnverifiedSignatures
unverifiedSignatures, err := image.Signatures()
if err != nil {
return nil, err
}
res := make([]*Signature, 0, len(unverifiedSignatures))
for sigNumber, sig := range unverifiedSignatures {
var acceptedSig *Signature // non-nil if accepted
rejected := false
// FIXME? Say more about the contents of the signature, i.e. parse it even before verification?!
logrus.Debugf("Evaluating signature %d:", sigNumber)
interpretingReqs:
for reqNumber, req := range reqs {
// FIXME: Log the requirement itself? For now, we use just the number.
// FIXME: supply state
switch res, as, err := req.isSignatureAuthorAccepted(image, sig); res {
case sarAccepted:
if as == nil { // Coverage: this should never happen
logrus.Debugf(" Requirement %d: internal inconsistency: sarAccepted but no parsed contents", reqNumber)
rejected = true
break interpretingReqs
}
logrus.Debugf(" Requirement %d: signature accepted", reqNumber)
if acceptedSig == nil {
acceptedSig = as
} else if *as != *acceptedSig { // Coverage: this should never happen
// Huh?! Two ways of verifying the same signature blob resulted in two different parses of its already accepted contents?
logrus.Debugf(" Requirement %d: internal inconsistency: sarAccepted but different parsed contents", reqNumber)
rejected = true
acceptedSig = nil
break interpretingReqs
}
case sarRejected:
logrus.Debugf(" Requirement %d: signature rejected: %s", reqNumber, err.Error())
rejected = true
break interpretingReqs
case sarUnknown:
if err != nil { // Coverage: this should never happen
logrus.Debugf(" Requirement %d: internal inconsistency: sarUnknown but an error message %s", reqNumber, err.Error())
rejected = true
break interpretingReqs
}
logrus.Debugf(" Requirement %d: signature state unknown, continuing", reqNumber)
default: // Coverage: this should never happen
logrus.Debugf(" Requirement %d: internal inconsistency: unknown result %#v", reqNumber, string(res))
rejected = true
break interpretingReqs
}
}
// This also handles the (invalid) case of empty reqs, by rejecting the signature.
if acceptedSig != nil && !rejected {
logrus.Debugf(" Overall: OK, signature accepted")
res = append(res, acceptedSig)
} else {
logrus.Debugf(" Overall: Signature not accepted")
}
}
return res, nil
}
// IsRunningImageAllowed returns true iff the policy allows running the image.
// If it returns false, err must be non-nil, and should be an PolicyRequirementError if evaluation
// succeeded but the result was rejection.
// 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.
func (pc *PolicyContext) IsRunningImageAllowed(image types.Image) (res bool, finalErr error) {
if err := pc.changeState(pcReady, pcInUse); err != nil {
return false, err
}
defer func() {
if err := pc.changeState(pcInUse, pcReady); err != nil {
res = false
finalErr = err
}
}()
logrus.Debugf("IsRunningImageAllowed for image %s", image.IntendedDockerReference())
reqs, err := pc.requirementsForImage(image)
if err != nil {
return false, err
}
if len(reqs) == 0 {
return false, PolicyRequirementError("List of verification policy requirements must not be empty")
}
for reqNumber, req := range reqs {
// FIXME: supply state
allowed, err := req.isRunningImageAllowed(image)
if !allowed {
logrus.Debugf("Requirement %d: denied, done", reqNumber)
return false, err
}
logrus.Debugf(" Requirement %d: allowed", reqNumber)
}
// 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
}

View File

@ -1,11 +1,456 @@
package signature
import (
"fmt"
"os"
"testing"
"github.com/projectatomic/skopeo/reference"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPolicyRequirementError(t *testing.T) {
// A stupid test just to keep code coverage
s := "test"
err := PolicyRequirementError(s)
assert.Equal(t, s, err.Error())
}
func TestPolicyContextChangeState(t *testing.T) {
pc, err := NewPolicyContext(&Policy{Default: PolicyRequirements{NewPRReject()}})
require.NoError(t, err)
defer pc.Destroy()
require.Equal(t, pcReady, pc.state)
err = pc.changeState(pcReady, pcInUse)
require.NoError(t, err)
err = pc.changeState(pcReady, pcInUse)
require.Error(t, err)
// Return state to pcReady to allow pc.Destroy to clean up.
err = pc.changeState(pcInUse, pcReady)
require.NoError(t, err)
}
func TestPolicyContextNewDestroy(t *testing.T) {
pc, err := NewPolicyContext(&Policy{Default: PolicyRequirements{NewPRReject()}})
require.NoError(t, err)
assert.Equal(t, pcReady, pc.state)
err = pc.Destroy()
require.NoError(t, err)
assert.Equal(t, pcDestroyed, pc.state)
// Trying to destroy when not pcReady
pc, err = NewPolicyContext(&Policy{Default: PolicyRequirements{NewPRReject()}})
require.NoError(t, err)
err = pc.changeState(pcReady, pcInUse)
require.NoError(t, err)
err = pc.Destroy()
require.Error(t, err)
assert.Equal(t, pcInUse, pc.state) // The state, and hopefully nothing else, has changed.
err = pc.changeState(pcInUse, pcReady)
require.NoError(t, err)
err = pc.Destroy()
assert.NoError(t, err)
}
func TestFullyExpandedDockerReference(t *testing.T) {
sha256Digest := "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
// Test both that fullyExpandedDockerReference returns the expected value (fullName+suffix),
// and that .FullName returns the expected value (fullName), i.e. that the two functions are
// consistent.
for inputName, fullName := range map[string]string{
"example.com/ns/repo": "example.com/ns/repo",
"example.com/repo": "example.com/repo",
"localhost/ns/repo": "localhost/ns/repo",
// Note that "localhost" is special here: notlocalhost/repo is be parsed as docker.io/notlocalhost.repo:
"localhost/repo": "localhost/repo",
"notlocalhost/repo": "docker.io/notlocalhost/repo",
"docker.io/ns/repo": "docker.io/ns/repo",
"docker.io/library/repo": "docker.io/library/repo",
"docker.io/repo": "docker.io/library/repo",
"ns/repo": "docker.io/ns/repo",
"library/repo": "docker.io/library/repo",
"repo": "docker.io/library/repo",
} {
for inputSuffix, mappedSuffix := range map[string]string{
":tag": ":tag",
sha256Digest: sha256Digest,
"": "",
// A github.com/distribution/reference value can have a tag and a digest at the same time!
// github.com/skopeo/reference handles that by dropping the tag. That is not obviously the
// right thing to do, but it is at least reasonable, so test that we keep behaving reasonably.
// This test case should not be construed to make this an API promise.
":tag" + sha256Digest: sha256Digest,
} {
fullInput := inputName + inputSuffix
ref, err := reference.ParseNamed(fullInput)
require.NoError(t, err)
assert.Equal(t, fullName, ref.FullName(), fullInput)
expanded, err := fullyExpandedDockerReference(ref)
require.NoError(t, err)
assert.Equal(t, fullName+mappedSuffix, expanded, fullInput)
}
}
}
func TestPolicyContextRequirementsForImage(t *testing.T) {
ktGPG := SBKeyTypeGPGKeys
prm := NewPRMMatchExact()
policy := &Policy{
Default: PolicyRequirements{NewPRReject()},
Specific: map[string]PolicyRequirements{},
}
// Just put _something_ into the Specific map for the keys we care about, and make it pairwise
// distinct so that we can compare the values and show them when debugging the tests.
for _, scope := range []string{
"unmatched",
"hostname.com",
"hostname.com/namespace",
"hostname.com/namespace/repo",
"hostname.com/namespace/repo:latest",
"hostname.com/namespace/repo:tag2",
"localhost",
"localhost/namespace",
"localhost/namespace/repo",
"localhost/namespace/repo:latest",
"localhost/namespace/repo:tag2",
"deep.com",
"deep.com/n1",
"deep.com/n1/n2",
"deep.com/n1/n2/n3",
"deep.com/n1/n2/n3/repo",
"deep.com/n1/n2/n3/repo:tag2",
"docker.io",
"docker.io/library",
"docker.io/library/busybox",
"docker.io/namespaceindocker",
"docker.io/namespaceindocker/repo",
"docker.io/namespaceindocker/repo:tag2",
// Note: these non-fully-expanded repository names are not matched against canonical (shortened)
// Docker names; they are instead parsed as starting with hostnames.
"busybox",
"library/busybox",
"namespaceindocker",
"namespaceindocker/repo",
"namespaceindocker/repo:tag2",
} {
policy.Specific[scope] = PolicyRequirements{xNewPRSignedByKeyData(ktGPG, []byte(scope), prm)}
}
pc, err := NewPolicyContext(policy)
require.NoError(t, err)
for input, matched := range map[string]string{
// Full match
"hostname.com/namespace/repo:latest": "hostname.com/namespace/repo:latest",
"hostname.com/namespace/repo:tag2": "hostname.com/namespace/repo:tag2",
"hostname.com/namespace/repo": "hostname.com/namespace/repo:latest",
"localhost/namespace/repo:latest": "localhost/namespace/repo:latest",
"localhost/namespace/repo:tag2": "localhost/namespace/repo:tag2",
"localhost/namespace/repo": "localhost/namespace/repo:latest",
"deep.com/n1/n2/n3/repo:tag2": "deep.com/n1/n2/n3/repo:tag2",
// Repository match
"hostname.com/namespace/repo:notlatest": "hostname.com/namespace/repo",
"localhost/namespace/repo:notlatest": "localhost/namespace/repo",
"deep.com/n1/n2/n3/repo:nottag2": "deep.com/n1/n2/n3/repo",
// Namespace match
"hostname.com/namespace/notrepo:latest": "hostname.com/namespace",
"localhost/namespace/notrepo:latest": "localhost/namespace",
"deep.com/n1/n2/n3/notrepo:tag2": "deep.com/n1/n2/n3",
"deep.com/n1/n2/notn3/repo:tag2": "deep.com/n1/n2",
"deep.com/n1/notn2/n3/repo:tag2": "deep.com/n1",
// Host name match
"hostname.com/notnamespace/repo:latest": "hostname.com",
"localhost/notnamespace/repo:latest": "localhost",
"deep.com/notn1/n2/n3/repo:tag2": "deep.com",
// Default
"this.doesnt/match:anything": "",
"this.doesnt/match-anything/defaulttag": "",
// docker.io canonizalication effects
"docker.io/library/busybox": "docker.io/library/busybox",
"library/busybox": "docker.io/library/busybox",
"busybox": "docker.io/library/busybox",
"docker.io/library/somethinginlibrary": "docker.io/library",
"library/somethinginlibrary": "docker.io/library",
"somethinginlibrary": "docker.io/library",
"docker.io/namespaceindocker/repo:tag2": "docker.io/namespaceindocker/repo:tag2",
"namespaceindocker/repo:tag2": "docker.io/namespaceindocker/repo:tag2",
"docker.io/namespaceindocker/repo:nottag2": "docker.io/namespaceindocker/repo",
"namespaceindocker/repo:nottag2": "docker.io/namespaceindocker/repo",
"docker.io/namespaceindocker/notrepo:tag2": "docker.io/namespaceindocker",
"namespaceindocker/notrepo:tag2": "docker.io/namespaceindocker",
"docker.io/notnamespaceindocker/repo:tag2": "docker.io",
"notnamespaceindocker/repo:tag2": "docker.io",
} {
var expected PolicyRequirements
if matched != "" {
e, ok := policy.Specific[matched]
require.True(t, ok, fmt.Sprintf("case %s: expected reqs not found", input))
expected = e
} else {
expected = policy.Default
}
reqs, err := pc.requirementsForImage(refImageMock(input))
require.NoError(t, err)
comment := fmt.Sprintf("case %s: %#v", input, reqs[0])
// Do not sue assert.Equal, which would do a deep contents comparison; we want to compare
// the pointers. Also, == does not work on slices; so test that the slices start at the
// same element and have the same length.
assert.True(t, &(reqs[0]) == &(expected[0]), comment)
assert.True(t, len(reqs) == len(expected), comment)
}
// Invalid reference format
_, err = pc.requirementsForImage(refImageMock("UPPERCASEISINVALID"))
assert.Error(t, err)
}
func TestPolicyContextGetSignaturesWithAcceptedAuthor(t *testing.T) {
expectedSig := &Signature{
DockerManifestDigest: TestImageManifestDigest,
DockerReference: "testing/manifest:latest",
}
pc, err := NewPolicyContext(&Policy{
Default: PolicyRequirements{NewPRReject()},
Specific: map[string]PolicyRequirements{
"docker.io/testing/manifest:latest": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchExact()),
},
"docker.io/testing/manifest:twoAccepts": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
},
"docker.io/testing/manifest:acceptReject": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
NewPRReject(),
},
"docker.io/testing/manifest:acceptUnknown": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
xNewPRSignedBaseLayer(NewPRMMatchRepository()),
},
"docker.io/testing/manifest:rejectUnknown": {
NewPRReject(),
xNewPRSignedBaseLayer(NewPRMMatchRepository()),
},
"docker.io/testing/manifest:unknown": {
xNewPRSignedBaseLayer(NewPRMMatchRepository()),
},
"docker.io/testing/manifest:unknown2": {
NewPRInsecureAcceptAnything(),
},
"docker.io/testing/manifest:invalidEmptyRequirements": {},
},
})
require.NoError(t, err)
defer pc.Destroy()
// Success
image := dirImageMock("fixtures/dir-img-valid", "testing/manifest:latest")
sigs, err := pc.GetSignaturesWithAcceptedAuthor(image)
require.NoError(t, err)
assert.Equal(t, []*Signature{expectedSig}, sigs)
// Two signatures
// FIXME? Use really different signatures for this?
image = dirImageMock("fixtures/dir-img-valid-2", "testing/manifest:latest")
sigs, err = pc.GetSignaturesWithAcceptedAuthor(image)
require.NoError(t, err)
assert.Equal(t, []*Signature{expectedSig, expectedSig}, sigs)
// No signatures
image = dirImageMock("fixtures/dir-img-unsigned", "testing/manifest:latest")
sigs, err = pc.GetSignaturesWithAcceptedAuthor(image)
require.NoError(t, err)
assert.Empty(t, sigs)
// Only invalid signatures
image = dirImageMock("fixtures/dir-img-modified-manifest", "testing/manifest:latest")
sigs, err = pc.GetSignaturesWithAcceptedAuthor(image)
require.NoError(t, err)
assert.Empty(t, sigs)
// 1 invalid, 1 valid signature (in this order)
image = dirImageMock("fixtures/dir-img-mixed", "testing/manifest:latest")
sigs, err = pc.GetSignaturesWithAcceptedAuthor(image)
require.NoError(t, err)
assert.Equal(t, []*Signature{expectedSig}, sigs)
// Two sarAccepted results for one signature
image = dirImageMock("fixtures/dir-img-valid", "testing/manifest:twoAccepts")
sigs, err = pc.GetSignaturesWithAcceptedAuthor(image)
require.NoError(t, err)
assert.Equal(t, []*Signature{expectedSig}, sigs)
// sarAccepted+sarRejected for a signature
image = dirImageMock("fixtures/dir-img-valid", "testing/manifest:acceptReject")
sigs, err = pc.GetSignaturesWithAcceptedAuthor(image)
require.NoError(t, err)
assert.Empty(t, sigs)
// sarAccepted+sarUnknown for a signature
image = dirImageMock("fixtures/dir-img-valid", "testing/manifest:acceptUnknown")
sigs, err = pc.GetSignaturesWithAcceptedAuthor(image)
require.NoError(t, err)
assert.Equal(t, []*Signature{expectedSig}, sigs)
// sarRejected+sarUnknown for a signature
image = dirImageMock("fixtures/dir-img-valid", "testing/manifest:rejectUnknown")
sigs, err = pc.GetSignaturesWithAcceptedAuthor(image)
require.NoError(t, err)
assert.Empty(t, sigs)
// sarUnknown only
image = dirImageMock("fixtures/dir-img-valid", "testing/manifest:unknown")
sigs, err = pc.GetSignaturesWithAcceptedAuthor(image)
require.NoError(t, err)
assert.Empty(t, sigs)
image = dirImageMock("fixtures/dir-img-valid", "testing/manifest:unknown2")
sigs, err = pc.GetSignaturesWithAcceptedAuthor(image)
require.NoError(t, err)
assert.Empty(t, sigs)
// Empty list of requirements (invalid)
image = dirImageMock("fixtures/dir-img-valid", "testing/manifest:invalidEmptyRequirements")
sigs, err = pc.GetSignaturesWithAcceptedAuthor(image)
require.NoError(t, err)
assert.Empty(t, sigs)
// Failures: Make sure we return nil sigs.
// Unexpected state (context already destroyed)
destroyedPC, err := NewPolicyContext(pc.Policy)
require.NoError(t, err)
err = destroyedPC.Destroy()
require.NoError(t, err)
image = dirImageMock("fixtures/dir-img-valid", "testing/manifest:latest")
sigs, err = destroyedPC.GetSignaturesWithAcceptedAuthor(image)
assert.Error(t, err)
assert.Nil(t, sigs)
// Not testing the pcInUse->pcReady transition, that would require custom PolicyRequirement
// implementations meddling with the state, or threads. This is for catching trivial programmer
// mistakes only, anyway.
// Invalid IntendedDockerReference value
image = dirImageMock("fixtures/dir-img-valid", "UPPERCASEISINVALID")
sigs, err = pc.GetSignaturesWithAcceptedAuthor(image)
assert.Error(t, err)
assert.Nil(t, sigs)
// Error reading signatures.
invalidSigDir := createInvalidSigDir(t)
defer os.RemoveAll(invalidSigDir)
image = dirImageMock(invalidSigDir, "testing/manifest:latest")
sigs, err = pc.GetSignaturesWithAcceptedAuthor(image)
assert.Error(t, err)
assert.Nil(t, sigs)
}
func TestPolicyContextIsRunningImageAllowed(t *testing.T) {
pc, err := NewPolicyContext(&Policy{
Default: PolicyRequirements{NewPRReject()},
Specific: map[string]PolicyRequirements{
"docker.io/testing/manifest:latest": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchExact()),
},
"docker.io/testing/manifest:twoAllows": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
},
"docker.io/testing/manifest:allowDeny": {
xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()),
NewPRReject(),
},
"docker.io/testing/manifest:reject": {
NewPRReject(),
},
"docker.io/testing/manifest:acceptAnything": {
NewPRInsecureAcceptAnything(),
},
"docker.io/testing/manifest:invalidEmptyRequirements": {},
},
})
require.NoError(t, err)
defer pc.Destroy()
// Success
image := dirImageMock("fixtures/dir-img-valid", "testing/manifest:latest")
res, err := pc.IsRunningImageAllowed(image)
assertRunningAllowed(t, res, err)
// Two signatures
// FIXME? Use really different signatures for this?
image = dirImageMock("fixtures/dir-img-valid-2", "testing/manifest:latest")
res, err = pc.IsRunningImageAllowed(image)
assertRunningAllowed(t, res, err)
// No signatures
image = dirImageMock("fixtures/dir-img-unsigned", "testing/manifest:latest")
res, err = pc.IsRunningImageAllowed(image)
assertRunningRejectedPolicyRequirement(t, res, err)
// Only invalid signatures
image = dirImageMock("fixtures/dir-img-modified-manifest", "testing/manifest:latest")
res, err = pc.IsRunningImageAllowed(image)
assertRunningRejectedPolicyRequirement(t, res, err)
// 1 invalid, 1 valid signature (in this order)
image = dirImageMock("fixtures/dir-img-mixed", "testing/manifest:latest")
res, err = pc.IsRunningImageAllowed(image)
assertRunningAllowed(t, res, err)
// Two allowed results
image = dirImageMock("fixtures/dir-img-mixed", "testing/manifest:twoAllows")
res, err = pc.IsRunningImageAllowed(image)
assertRunningAllowed(t, res, err)
// Allow + deny results
image = dirImageMock("fixtures/dir-img-mixed", "testing/manifest:allowDeny")
res, err = pc.IsRunningImageAllowed(image)
assertRunningRejectedPolicyRequirement(t, res, err)
// prReject works
image = dirImageMock("fixtures/dir-img-mixed", "testing/manifest:reject")
res, err = pc.IsRunningImageAllowed(image)
assertRunningRejectedPolicyRequirement(t, res, err)
// prInsecureAcceptAnything works
image = dirImageMock("fixtures/dir-img-mixed", "testing/manifest:acceptAnything")
res, err = pc.IsRunningImageAllowed(image)
assertRunningAllowed(t, res, err)
// Empty list of requirements (invalid)
image = dirImageMock("fixtures/dir-img-valid", "testing/manifest:invalidEmptyRequirements")
res, err = pc.IsRunningImageAllowed(image)
assertRunningRejectedPolicyRequirement(t, res, err)
// Unexpected state (context already destroyed)
destroyedPC, err := NewPolicyContext(pc.Policy)
require.NoError(t, err)
err = destroyedPC.Destroy()
require.NoError(t, err)
image = dirImageMock("fixtures/dir-img-valid", "testing/manifest:latest")
res, err = destroyedPC.IsRunningImageAllowed(image)
assertRunningRejected(t, res, err)
// Not testing the pcInUse->pcReady transition, that would require custom PolicyRequirement
// implementations meddling with the state, or threads. This is for catching trivial programmer
// mistakes only, anyway.
// Invalid IntendedDockerReference value
image = dirImageMock("fixtures/dir-img-valid", "UPPERCASEISINVALID")
res, err = pc.IsRunningImageAllowed(image)
assertRunningRejected(t, res, err)
}
// Helpers for validating PolicyRequirement.isSignatureAuthorAccepted results:
// assertSARRejected verifies that isSignatureAuthorAccepted returns a consistent sarRejected result