diff --git a/signature/policy_eval.go b/signature/policy_eval.go index a6a863a3..c62328ed 100644 --- a/signature/policy_eval.go +++ b/signature/policy_eval.go @@ -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 +} diff --git a/signature/policy_eval_test.go b/signature/policy_eval_test.go index 4e1e0ca2..254b285d 100644 --- a/signature/policy_eval_test.go +++ b/signature/policy_eval_test.go @@ -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