mirror of https://github.com/containers/image.git
361 lines
15 KiB
Go
361 lines
15 KiB
Go
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 won’t 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)
|
||
}
|
||
}
|