image/signature/policy_reference_match_test.go

361 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package signature
import (
"context"
"fmt"
"testing"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/internal/testing/mocks"
"github.com/containers/image/v5/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
fullRHELRef = "registry.access.redhat.com/rhel7/rhel:7.2.3"
untaggedRHELRef = "registry.access.redhat.com/rhel7/rhel"
digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
digestSuffixOther = "@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
)
func TestParseImageAndDockerReference(t *testing.T) {
const (
ok1 = "busybox"
ok2 = fullRHELRef
bad1 = "UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES"
bad2 = ""
)
// Success
ref, err := reference.ParseNormalizedNamed(ok1)
require.NoError(t, err)
r1, r2, err := parseImageAndDockerReference(refImageMock{ref}, ok2)
require.NoError(t, err)
assert.Equal(t, ok1, reference.FamiliarString(r1))
assert.Equal(t, ok2, reference.FamiliarString(r2))
// Unidentified images are rejected.
_, _, err = parseImageAndDockerReference(refImageMock{nil}, ok2)
require.Error(t, err)
assert.IsType(t, PolicyRequirementError(""), err)
// Failures
for _, refs := range [][]string{
{bad1, ok2},
{ok1, bad2},
{bad1, bad2},
} {
ref, err := reference.ParseNormalizedNamed(refs[0])
if err == nil {
_, _, err := parseImageAndDockerReference(refImageMock{ref}, refs[1])
assert.Error(t, err)
}
}
}
// refImageMock is a mock of types.UnparsedImage which returns itself in Reference().DockerReference.
type refImageMock struct{ reference.Named }
func (ref refImageMock) Reference() types.ImageReference {
return refImageReferenceMock(ref)
}
func (ref refImageMock) Close() error {
panic("unexpected call to a mock function")
}
func (ref refImageMock) Manifest(ctx context.Context) ([]byte, string, error) {
panic("unexpected call to a mock function")
}
func (ref refImageMock) Signatures(context.Context) ([][]byte, error) {
panic("unexpected call to a mock function")
}
func (ref refImageMock) LayerInfosForCopy(ctx context.Context) ([]types.BlobInfo, error) {
panic("unexpected call to a mock function")
}
// refImageReferenceMock is a mock of types.ImageReference which returns itself in DockerReference.
type refImageReferenceMock struct{ reference.Named }
func (ref refImageReferenceMock) Transport() types.ImageTransport {
// We use this in error messages, so sady we must return something. But right now we do so only when DockerReference is nil, so restrict to that.
if ref.Named == nil {
return mocks.NameImageTransport("== Transport mock")
}
panic("unexpected call to a mock function")
}
func (ref refImageReferenceMock) StringWithinTransport() string {
// We use this in error messages, so sadly we must return something. But right now we do so only when DockerReference is nil, so restrict to that.
if ref.Named == nil {
return "== StringWithinTransport for an image with no Docker support"
}
panic("unexpected call to a mock function")
}
func (ref refImageReferenceMock) DockerReference() reference.Named {
return ref.Named
}
func (ref refImageReferenceMock) PolicyConfigurationIdentity() string {
panic("unexpected call to a mock function")
}
func (ref refImageReferenceMock) PolicyConfigurationNamespaces() []string {
panic("unexpected call to a mock function")
}
func (ref refImageReferenceMock) NewImage(ctx context.Context, sys *types.SystemContext) (types.ImageCloser, error) {
panic("unexpected call to a mock function")
}
func (ref refImageReferenceMock) NewImageSource(ctx context.Context, sys *types.SystemContext) (types.ImageSource, error) {
panic("unexpected call to a mock function")
}
func (ref refImageReferenceMock) NewImageDestination(ctx context.Context, sys *types.SystemContext) (types.ImageDestination, error) {
panic("unexpected call to a mock function")
}
func (ref refImageReferenceMock) DeleteImage(ctx context.Context, sys *types.SystemContext) error {
panic("unexpected call to a mock function")
}
type prmSymmetricTableTest struct {
refA, refB string
result bool
}
// Test cases for exact reference match. The behavior is supposed to be symmetric.
var prmExactMatchTestTable = []prmSymmetricTableTest{
// Success, simple matches
{"busybox:latest", "busybox:latest", true},
{fullRHELRef, fullRHELRef, true},
{"busybox" + digestSuffix, "busybox" + digestSuffix, true}, // NOTE: This is not documented; signing digests is not recommended at this time.
// Non-canonical reference format is canonicalized
{"library/busybox:latest", "busybox:latest", true},
{"docker.io/library/busybox:latest", "busybox:latest", true},
{"library/busybox" + digestSuffix, "busybox" + digestSuffix, true},
// Mismatch
{"busybox:latest", "busybox:notlatest", false},
{"busybox:latest", "notbusybox:latest", false},
{"busybox:latest", "hostname/library/busybox:notlatest", false},
{"hostname/library/busybox:latest", "busybox:notlatest", false},
{"busybox:latest", fullRHELRef, false},
{"busybox" + digestSuffix, "notbusybox" + digestSuffix, false},
{"busybox:latest", "busybox" + digestSuffix, false},
{"busybox" + digestSuffix, "busybox" + digestSuffixOther, false},
// NameOnly references
{"busybox", "busybox:latest", false},
{"busybox", "busybox" + digestSuffix, false},
{"busybox", "busybox", false},
// References with both tags and digests: We match them exactly (requiring BOTH to match)
// NOTE: Again, this is not documented behavior; the recommendation is to sign tags, not digests, and then tag-and-digest references wont match the signed identity.
{"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffix, true},
{"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, false},
{"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffix, false},
{"busybox:latest" + digestSuffix, "busybox" + digestSuffix, false},
{"busybox:latest" + digestSuffix, "busybox:latest", false},
// Invalid format
{"UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES", "busybox:latest", false},
{"", "UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES", false},
// Even if they are exactly equal, invalid values are rejected.
{"INVALID", "INVALID", false},
}
// Test cases for repository-only reference match. The behavior is supposed to be symmetric.
var prmRepositoryMatchTestTable = []prmSymmetricTableTest{
// Success, simple matches
{"busybox:latest", "busybox:latest", true},
{fullRHELRef, fullRHELRef, true},
{"busybox" + digestSuffix, "busybox" + digestSuffix, true}, // NOTE: This is not documented; signing digests is not recommended at this time.
// Non-canonical reference format is canonicalized
{"library/busybox:latest", "busybox:latest", true},
{"docker.io/library/busybox:latest", "busybox:latest", true},
{"library/busybox" + digestSuffix, "busybox" + digestSuffix, true},
// The same as above, but with mismatching tags
{"busybox:latest", "busybox:notlatest", true},
{fullRHELRef + "tagsuffix", fullRHELRef, true},
{"library/busybox:latest", "busybox:notlatest", true},
{"busybox:latest", "library/busybox:notlatest", true},
{"docker.io/library/busybox:notlatest", "busybox:latest", true},
{"busybox:notlatest", "docker.io/library/busybox:latest", true},
{"busybox:latest", "busybox" + digestSuffix, true},
{"busybox" + digestSuffix, "busybox" + digestSuffixOther, true}, // Even this is accepted here. (This could more reasonably happen with two different digest algorithms.)
// The same as above, but with defaulted tags (should not actually happen)
{"busybox", "busybox:notlatest", true},
{fullRHELRef, untaggedRHELRef, true},
{"busybox", "busybox" + digestSuffix, true},
{"library/busybox", "busybox", true},
{"docker.io/library/busybox", "busybox", true},
// Mismatch
{"busybox:latest", "notbusybox:latest", false},
{"hostname/library/busybox:latest", "busybox:notlatest", false},
{"busybox:latest", fullRHELRef, false},
{"busybox" + digestSuffix, "notbusybox" + digestSuffix, false},
// References with both tags and digests: We ignore both anyway.
{"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffix, true},
{"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, true},
{"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffix, true},
{"busybox:latest" + digestSuffix, "busybox" + digestSuffix, true},
{"busybox:latest" + digestSuffix, "busybox:latest", true},
// Invalid format
{"UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES", "busybox:latest", false},
{"", "UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES", false},
// Even if they are exactly equal, invalid values are rejected.
{"INVALID", "INVALID", false},
}
func testImageAndSig(t *testing.T, prm PolicyReferenceMatch, imageRef, sigRef string, result bool) {
// This assumes that all ways to obtain a reference.Named perform equivalent validation,
// and therefore values refused by reference.ParseNormalizedNamed can not happen in practice.
parsedImageRef, err := reference.ParseNormalizedNamed(imageRef)
if err != nil {
return
}
res := prm.matchesDockerReference(refImageMock{parsedImageRef}, sigRef)
assert.Equal(t, result, res, fmt.Sprintf("%s vs. %s", imageRef, sigRef))
}
func TestPRMMatchExactMatchesDockerReference(t *testing.T) {
prm := NewPRMMatchExact()
for _, test := range prmExactMatchTestTable {
testImageAndSig(t, prm, test.refA, test.refB, test.result)
testImageAndSig(t, prm, test.refB, test.refA, test.result)
}
// Even if they are signed with an empty string as a reference, unidentified images are rejected.
res := prm.matchesDockerReference(refImageMock{nil}, "")
assert.False(t, res, `unidentified vs. ""`)
}
func TestPMMMatchRepoDigestOrExactMatchesDockerReference(t *testing.T) {
prm := NewPRMMatchRepoDigestOrExact()
// prmMatchRepoDigestOrExact is a middle ground between prmMatchExact and prmMatchRepository:
// It accepts anything prmMatchExact accepts,…
for _, test := range prmExactMatchTestTable {
if test.result == true {
testImageAndSig(t, prm, test.refA, test.refB, test.result)
testImageAndSig(t, prm, test.refB, test.refA, test.result)
}
}
// … and it rejects everything prmMatchRepository rejects.
for _, test := range prmRepositoryMatchTestTable {
if test.result == false {
testImageAndSig(t, prm, test.refA, test.refB, test.result)
testImageAndSig(t, prm, test.refB, test.refA, test.result)
}
}
// The other cases, possibly asymmetrical:
for _, test := range []struct {
imageRef, sigRef string
result bool
}{
// Tag mismatch
{"busybox:latest", "busybox:notlatest", false},
{fullRHELRef + "tagsuffix", fullRHELRef, false},
{"library/busybox:latest", "busybox:notlatest", false},
{"busybox:latest", "library/busybox:notlatest", false},
{"docker.io/library/busybox:notlatest", "busybox:latest", false},
{"busybox:notlatest", "docker.io/library/busybox:latest", false},
// NameOnly references
{"busybox", "busybox:latest", false},
{"busybox:latest", "busybox", false},
{"busybox", "busybox" + digestSuffix, false},
{"busybox" + digestSuffix, "busybox", false},
{fullRHELRef, untaggedRHELRef, false},
{"busybox", "busybox", false},
// Tag references only accept signatures with matching tags.
{"busybox:latest", "busybox" + digestSuffix, false},
// Digest references accept any signature with matching repository.
{"busybox" + digestSuffix, "busybox:latest", true},
{"busybox" + digestSuffix, "busybox" + digestSuffixOther, true}, // Even this is accepted here. (This could more reasonably happen with two different digest algorithms.)
// References with both tags and digests: We match them exactly (requiring BOTH to match).
{"busybox:latest" + digestSuffix, "busybox:latest", false},
{"busybox:latest" + digestSuffix, "busybox:notlatest", false},
{"busybox:latest", "busybox:latest" + digestSuffix, false},
{"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, false},
{"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffixOther, false},
} {
testImageAndSig(t, prm, test.imageRef, test.sigRef, test.result)
}
}
func TestPRMMatchRepositoryMatchesDockerReference(t *testing.T) {
prm := NewPRMMatchRepository()
for _, test := range prmRepositoryMatchTestTable {
testImageAndSig(t, prm, test.refA, test.refB, test.result)
testImageAndSig(t, prm, test.refB, test.refA, test.result)
}
// Even if they are signed with an empty string as a reference, unidentified images are rejected.
res := prm.matchesDockerReference(refImageMock{nil}, "")
assert.False(t, res, `unidentified vs. ""`)
}
func TestParseDockerReferences(t *testing.T) {
const (
ok1 = "busybox"
ok2 = fullRHELRef
bad1 = "UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES"
bad2 = ""
)
// Success
r1, r2, err := parseDockerReferences(ok1, ok2)
require.NoError(t, err)
assert.Equal(t, ok1, reference.FamiliarString(r1))
assert.Equal(t, ok2, reference.FamiliarString(r2))
// Failures
for _, refs := range [][]string{
{bad1, ok2},
{ok1, bad2},
{bad1, bad2},
} {
_, _, err := parseDockerReferences(refs[0], refs[1])
assert.Error(t, err)
}
}
// forbiddenImageMock is a mock of types.UnparsedImage which ensures Reference is not called
type forbiddenImageMock struct{}
func (ref forbiddenImageMock) Reference() types.ImageReference {
panic("unexpected call to a mock function")
}
func (ref forbiddenImageMock) Close() error {
panic("unexpected call to a mock function")
}
func (ref forbiddenImageMock) Manifest(ctx context.Context) ([]byte, string, error) {
panic("unexpected call to a mock function")
}
func (ref forbiddenImageMock) Signatures(context.Context) ([][]byte, error) {
panic("unexpected call to a mock function")
}
func (ref forbiddenImageMock) LayerInfosForCopy(ctx context.Context) ([]types.BlobInfo, error) {
panic("unexpected call to a mock function")
}
func testExactPRMAndSig(t *testing.T, prmFactory func(string) PolicyReferenceMatch, imageRef, sigRef string, result bool) {
prm := prmFactory(imageRef)
res := prm.matchesDockerReference(forbiddenImageMock{}, sigRef)
assert.Equal(t, result, res, fmt.Sprintf("%s vs. %s", imageRef, sigRef))
}
func prmExactReferenceFactory(ref string) PolicyReferenceMatch {
// Do not use NewPRMExactReference, we want to also test the case with an invalid DockerReference,
// even though NewPRMExactReference should never let it happen.
return &prmExactReference{DockerReference: ref}
}
func TestPRMExactReferenceMatchesDockerReference(t *testing.T) {
for _, test := range prmExactMatchTestTable {
testExactPRMAndSig(t, prmExactReferenceFactory, test.refA, test.refB, test.result)
testExactPRMAndSig(t, prmExactReferenceFactory, test.refB, test.refA, test.result)
}
}
func prmExactRepositoryFactory(ref string) PolicyReferenceMatch {
// Do not use NewPRMExactRepository, we want to also test the case with an invalid DockerReference,
// even though NewPRMExactRepository should never let it happen.
return &prmExactRepository{DockerRepository: ref}
}
func TestPRMExactRepositoryMatchesDockerReference(t *testing.T) {
for _, test := range prmRepositoryMatchTestTable {
testExactPRMAndSig(t, prmExactRepositoryFactory, test.refA, test.refB, test.result)
testExactPRMAndSig(t, prmExactRepositoryFactory, test.refB, test.refA, test.result)
}
}