mirror of
https://github.com/containers/skopeo.git
synced 2025-07-31 06:31:06 +00:00
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:
parent
fd9c615d88
commit
21229685cf
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user