Add proper mirror support (#74)

* Add rewrite support and fix existing tests

* Add unit tests for policy matching

* Compile regexes up front and store policies in map

* Add test for verify flow with mirror

* Rename ImageName -> ResolvedName

And only set it when necessary

* Rename Rewrite -> Replacement

but keep it as rewrite in the yaml
This commit is contained in:
Jonny Stoten
2024-07-12 17:09:41 +01:00
committed by GitHub
parent 247448a765
commit a4c3bd07fe
32 changed files with 619 additions and 225 deletions

View File

@@ -22,12 +22,13 @@ import (
)
var (
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
TestTempDir = "attest-sign-test"
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
PassMirrorPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-mirror")
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
TestTempDir = "attest-sign-test"
)
func TestSignVerifyOCILayout(t *testing.T) {

View File

@@ -3,8 +3,10 @@ package attest
import (
"context"
"fmt"
"strings"
"time"
"github.com/distribution/reference"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
@@ -36,12 +38,12 @@ func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.PolicyOptions)
}
// this is overriding the mapping with a referrers config. Useful for testing if nothing else
if opts.ReferrersRepo != "" {
pctx.Mapping.Attestations = &config.ReferrersConfig{
pctx.Mapping.Attestations = &config.AttestationConfig{
Repo: opts.ReferrersRepo,
Style: config.AttestationStyleReferrers,
}
} else if opts.AttestationStyle == config.AttestationStyleAttached {
pctx.Mapping.Attestations = &config.ReferrersConfig{
pctx.Mapping.Attestations = &config.AttestationConfig{
Repo: opts.ReferrersRepo,
Style: config.AttestationStyleAttached,
}
@@ -122,6 +124,19 @@ func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, p
if err != nil {
return nil, err
}
if pctx.ResolvedName != "" {
// this means the name we have is not the one we want to use for policy evaluation
// so we need to replace it with the one we resolved during policy resolution.
// this can happen if the name is an alias for another image, e.g. if it is a mirror
ref, err := reference.ParseNormalizedNamed(name)
if err != nil {
return nil, fmt.Errorf("failed to parse image name: %w", err)
}
oldName := ref.Name()
name = strings.Replace(name, oldName, pctx.ResolvedName, 1)
}
purl, canonical, err := oci.RefToPURL(name, platform)
if err != nil {
return nil, fmt.Errorf("failed to convert ref to purl: %w", err)

View File

@@ -59,7 +59,7 @@ func TestVerifyAttestations(t *testing.T) {
}
ctx := policy.WithPolicyEvaluator(context.Background(), &mockPE)
_, err := VerifyAttestations(ctx, resolver, nil)
_, err := VerifyAttestations(ctx, resolver, &policy.Policy{ResolvedName: ""})
if tc.expectedError != nil {
if assert.Error(t, err) {
assert.Equal(t, tc.expectedError.Error(), err.Error())
@@ -189,22 +189,24 @@ func TestVerificationFailure(t *testing.T) {
assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI)
}
// test signing without a TL entry
func TestSignVerifyNoTL(t *testing.T) {
func TestSignVerify(t *testing.T) {
ctx, signer := test.Setup(t)
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
// setup an image with signed attestations
outputLayout := test.CreateTempDir(t, "", TestTempDir)
testCases := []struct {
name string
signTL bool
policyDir string
success bool
name string
signTL bool
policyDir string
imageName string
expectError bool
}{
{name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir, success: true},
{name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir, success: false},
{name: "no tl", signTL: false, policyDir: PassPolicyDir, success: false},
{name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir},
{name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir},
{name: "no tl", signTL: false, policyDir: PassPolicyDir},
{name: "mirror", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "mirror.org/library/test-image:test"},
{name: "mirror no match", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectError: true},
}
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
@@ -222,13 +224,18 @@ func TestSignVerifyNoTL(t *testing.T) {
signedIndex := attIdx.Index
signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests)
require.NoError(t, err)
imageName := tc.imageName
if imageName == "" {
imageName = attIdx.Name
}
// output signed attestations
idx := v1.ImageIndex(empty.Index)
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
Add: signedIndex,
Descriptor: v1.Descriptor{
Annotations: map[string]string{
oci.OciReferenceTarget: attIdx.Name,
oci.OciReferenceTarget: imageName,
},
},
})
@@ -241,8 +248,17 @@ func TestSignVerifyNoTL(t *testing.T) {
src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64))
require.NoError(t, err)
results, err := Verify(ctx, src, policyOpts)
if tc.expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, OutcomeSuccess, results.Outcome)
platform, err := oci.ParsePlatform(LinuxAMD64)
require.NoError(t, err)
expectedPURL, _, err := oci.RefToPURL(attIdx.Name, platform)
require.NoError(t, err)
assert.Equal(t, expectedPURL, results.Input.Purl)
})
}
}

View File

@@ -37,6 +37,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
platforms := []string{"linux/amd64", "linux/arm64"}
for _, tc := range []struct {
name string
server *httptest.Server
referrersServer *httptest.Server
skipSubject bool
@@ -44,46 +45,53 @@ func TestAttestationReferenceTypes(t *testing.T) {
referrersRepo string
attestationSource config.AttestationStyle
expectFailure bool
policyDir string
}{
{
name: "referrers support, defaults",
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
},
{
name: "no referrers support",
server: httptest.NewServer(registry.New()),
},
{
name: "attached attestations",
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
skipSubject: true,
attestationSource: config.AttestationStyleAttached,
},
{
name: "use digest",
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
useDigest: true,
},
{
name: "attached attestations, referrers repo (mismatched args)",
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
expectFailure: true, //mismatched args
attestationSource: config.AttestationStyleAttached,
referrersRepo: "referrers",
},
{
name: "referrers attestations, referrers repo (no policy)",
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
expectFailure: true, // no policy
attestationSource: config.AttestationStyleReferrers,
referrersRepo: "referrers",
},
{
name: "referrers attestations",
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
attestationSource: config.AttestationStyleReferrers,
},
{
name: "referrers attestations, no referrers support on server",
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(false))),
attestationSource: config.AttestationStyleReferrers,
referrersServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
},
} {
t.Run(fmt.Sprint(tc), func(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
s := tc.server
defer s.Close()
@@ -111,6 +119,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
require.NoError(t, err)
for _, img := range signedManifests {
err = mirror.PushImageToRegistry(img.Attestation.Image, fmt.Sprintf("%s:tag-does-not-matter", repo))
require.NoError(t, err)
@@ -142,9 +151,6 @@ func TestAttestationReferenceTypes(t *testing.T) {
policyOpts := &policy.PolicyOptions{
LocalPolicyDir: LocalPolicy,
}
if tc.policyDir != "" {
policyOpts.LocalPolicyDir = tc.policyDir
}
if tc.referrersRepo != "" {
policyOpts.ReferrersRepo = tc.referrersRepo
@@ -192,65 +198,70 @@ func TestReferencesInDifferentRepo(t *testing.T) {
ctx, signer := test.Setup(t)
repoName := "repo"
for _, tc := range []struct {
name string
server *httptest.Server
refServer *httptest.Server
}{
{
name: "referrers support",
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
refServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
},
{
name: "no referrers support",
server: httptest.NewServer(registry.New()),
refServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
},
} {
server := tc.server
defer server.Close()
serverUrl, err := url.Parse(server.URL)
require.NoError(t, err)
refServer := tc.refServer
defer refServer.Close()
refServerUrl, err := url.Parse(refServer.URL)
require.NoError(t, err)
opts := &attestation.SigningOptions{
Replace: true,
SkipTL: true,
}
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
require.NoError(t, err)
indexName := fmt.Sprintf("%s/%s:latest", serverUrl.Host, repoName)
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
require.NoError(t, err)
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
// push signed attestation image to the ref server
for _, img := range signedManifests {
// push references using subject-digest.att convention
err = mirror.PushImageToRegistry(img.Attestation.Image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerUrl.Host, repoName))
t.Run(tc.name, func(t *testing.T) {
server := tc.server
defer server.Close()
serverUrl, err := url.Parse(server.URL)
require.NoError(t, err)
}
mfs2, err := attIdx.Index.IndexManifest()
require.NoError(t, err)
for _, mf := range mfs2.Manifests {
//skip signed/unsigned attestations
if mf.Annotations[attestation.DockerReferenceType] == attestation.AttestationManifestType {
continue
refServer := tc.refServer
defer refServer.Close()
refServerUrl, err := url.Parse(refServer.URL)
require.NoError(t, err)
opts := &attestation.SigningOptions{
Replace: true,
SkipTL: true,
}
// can evaluate policy using referrers in a different repo
referencedImage := fmt.Sprintf("%s@%s", indexName, mf.Digest.String())
policyOpts := &policy.PolicyOptions{
LocalPolicyDir: PassPolicyDir,
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
require.NoError(t, err)
indexName := fmt.Sprintf("%s/%s:latest", serverUrl.Host, repoName)
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
require.NoError(t, err)
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
// push signed attestation image to the ref server
for _, img := range signedManifests {
// push references using subject-digest.att convention
err = mirror.PushImageToRegistry(img.Attestation.Image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerUrl.Host, repoName))
require.NoError(t, err)
}
src, err := oci.ParseImageSpec(referencedImage)
mfs2, err := attIdx.Index.IndexManifest()
require.NoError(t, err)
results, err := attest.Verify(ctx, src, policyOpts)
require.NoError(t, err)
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
}
for _, mf := range mfs2.Manifests {
//skip signed/unsigned attestations
if mf.Annotations[attestation.DockerReferenceType] == attestation.AttestationManifestType {
continue
}
// can evaluate policy using referrers in a different repo
referencedImage := fmt.Sprintf("%s@%s", indexName, mf.Digest.String())
policyOpts := &policy.PolicyOptions{
LocalPolicyDir: PassPolicyDir,
}
src, err := oci.ParseImageSpec(referencedImage)
require.NoError(t, err)
results, err := attest.Verify(ctx, src, policyOpts)
require.NoError(t, err)
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
}
})
}
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"github.com/docker/attest/pkg/tuf"
goyaml "gopkg.in/yaml.v3"
@@ -17,7 +18,7 @@ func LoadLocalMappings(configDir string) (*PolicyMappings, error) {
if configDir == "" {
return nil, nil
}
mappings := &PolicyMappings{}
mappings := &policyMappingsFile{}
path := filepath.Join(configDir, MappingFilename)
mappingFile, err := os.ReadFile(path)
if err != nil {
@@ -27,7 +28,7 @@ func LoadLocalMappings(configDir string) (*PolicyMappings, error) {
if err != nil {
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", path, err)
}
return mappings, nil
return expandMappingFile(mappings)
}
func LoadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMappings, error) {
@@ -39,11 +40,38 @@ func LoadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMa
if err != nil {
return nil, fmt.Errorf("failed to download policy mapping file %s: %w", filename, err)
}
mappings := &PolicyMappings{}
mappings := &policyMappingsFile{}
err = goyaml.Unmarshal(fileContents, mappings)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", filename, err)
}
return mappings, nil
return expandMappingFile(mappings)
}
func expandMappingFile(mappingFile *policyMappingsFile) (*PolicyMappings, error) {
policies := make(map[string]*PolicyMapping)
for _, policy := range mappingFile.Policies {
policies[policy.Id] = policy
}
var rules []*PolicyRule
for _, rule := range mappingFile.Rules {
r, err := regexp.Compile(rule.Pattern)
if err != nil {
return nil, err
}
rules = append(rules, &PolicyRule{
Pattern: r,
PolicyId: rule.PolicyId,
Replacement: rule.Replacement,
})
}
return &PolicyMappings{
Version: mappingFile.Version,
Kind: mappingFile.Kind,
Policies: policies,
Rules: rules,
}, nil
}

View File

@@ -1,10 +1,25 @@
package config
import "regexp"
type policyMappingsFile struct {
Version string `yaml:"version"`
Kind string `yaml:"kind"`
Policies []*PolicyMapping `yaml:"policies"`
Rules []*policyRuleFile `yaml:"rules"`
}
type policyRuleFile struct {
Pattern string `yaml:"pattern"`
PolicyId string `yaml:"policy-id"`
Replacement string `yaml:"rewrite"`
}
type PolicyMappings struct {
Version string `json:"version"`
Kind string `json:"kind"`
Policies []*PolicyMapping `json:"policies"`
Mirrors []*PolicyMirror `json:"mirrors"`
Version string
Kind string
Policies map[string]*PolicyMapping
Rules []*PolicyRule
}
type AttestationStyle string
@@ -15,34 +30,23 @@ const (
)
type PolicyMapping struct {
Id string `json:"id"`
Description string `json:"description"`
Origin *PolicyOrigin `json:"origin"`
Files []PolicyMappingFile `json:"files"`
Attestations *ReferrersConfig `json:"attestations"`
Id string `yaml:"id"`
Description string `yaml:"description"`
Files []PolicyMappingFile `yaml:"files"`
Attestations *AttestationConfig `yaml:"attestations"`
}
type ReferrersConfig struct {
Style AttestationStyle `json:"style"`
Repo string `json:"repo"`
type AttestationConfig struct {
Style AttestationStyle `yaml:"style"`
Repo string `yaml:"repo"`
}
type PolicyMappingFile struct {
Path string `json:"path"`
Path string `yaml:"path"`
}
type PolicyMirror struct {
PolicyId string `yaml:"policy-id"`
Mirror MirrorSpec `json:"mirror"`
}
type MirrorSpec struct {
Domains []string `json:"domains"`
Prefix string `json:"prefix"`
}
type PolicyOrigin struct {
Name string `json:"name"`
Prefix string `json:"prefix"`
Domain string `json:"domain"`
type PolicyRule struct {
Pattern *regexp.Regexp
PolicyId string
Replacement string
}

View File

@@ -6,15 +6,13 @@ import (
"os"
"path"
"path/filepath"
"slices"
"strings"
"github.com/distribution/reference"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
)
func resolveLocalPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*Policy, error) {
func resolveLocalPolicy(opts *PolicyOptions, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
if opts.LocalPolicyDir == "" {
return nil, fmt.Errorf("local policy dir not set")
}
@@ -35,10 +33,13 @@ func resolveLocalPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*Po
InputFiles: files,
Mapping: mapping,
}
if imageName != matchedName {
policy.ResolvedName = matchedName
}
return policy, nil
}
func resolveTufPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*Policy, error) {
func resolveTufPolicy(opts *PolicyOptions, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
files := make([]*PolicyFile, 0, len(mapping.Files))
for _, f := range mapping.Files {
filename := f.Path
@@ -55,32 +56,68 @@ func resolveTufPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*Poli
InputFiles: files,
Mapping: mapping,
}
if imageName != matchedName {
policy.ResolvedName = matchedName
}
return policy, nil
}
func findPolicyMatch(named reference.Named, mappings *config.PolicyMappings) (*config.PolicyMapping, *config.PolicyMirror) {
if mappings != nil {
for _, mapping := range mappings.Policies {
if mapping.Origin.Domain == reference.Domain(named) &&
strings.HasPrefix(reference.Path(named), mapping.Origin.Prefix) {
return mapping, nil
}
}
// now search mirrors
for _, mirror := range mappings.Mirrors {
if (slices.Contains(mirror.Mirror.Domains, reference.Domain(named)) ||
slices.Contains(mirror.Mirror.Domains, "*")) &&
strings.HasPrefix(reference.Path(named), mirror.Mirror.Prefix) {
for _, mapping := range mappings.Policies {
if mapping.Id == mirror.PolicyId {
return mapping, nil
}
type matchType string
const (
matchTypePolicy matchType = "policy"
matchTypeMatchNoPolicy matchType = "match_no_policy"
matchTypeNoMatch matchType = "no_match"
)
type policyMatch struct {
matchType matchType
policy *config.PolicyMapping
rule *config.PolicyRule
matchedName string
}
func findPolicyMatch(imageName string, mappings *config.PolicyMappings) (*policyMatch, error) {
if mappings == nil {
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
}
return findPolicyMatchImpl(imageName, mappings, make(map[*config.PolicyRule]bool))
}
func findPolicyMatchImpl(imageName string, mappings *config.PolicyMappings, matched map[*config.PolicyRule]bool) (*policyMatch, error) {
for _, rule := range mappings.Rules {
if rule.Pattern.MatchString(imageName) {
switch {
case rule.PolicyId == "" && rule.Replacement == "":
return nil, fmt.Errorf("rule %s has neither policy-id nor rewrite", rule.Pattern)
case rule.PolicyId != "" && rule.Replacement != "":
return nil, fmt.Errorf("rule %s has both policy-id and rewrite", rule.Pattern)
case rule.PolicyId != "":
policy := mappings.Policies[rule.PolicyId]
if policy != nil {
return &policyMatch{
matchType: matchTypePolicy,
policy: policy,
rule: rule,
matchedName: imageName,
}, nil
}
return nil, mirror
return &policyMatch{
matchType: matchTypeMatchNoPolicy,
rule: rule,
matchedName: imageName,
}, nil
case rule.Replacement != "":
if matched[rule] {
return nil, fmt.Errorf("rewrite loop detected")
}
matched[rule] = true
imageName = rule.Pattern.ReplaceAllString(imageName, rule.Replacement)
return findPolicyMatchImpl(imageName, mappings, matched)
}
}
}
return nil, nil
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
}
func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
@@ -90,10 +127,9 @@ func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
}
if localMappings != nil {
for _, mapping := range localMappings.Policies {
if mapping.Id == opts.PolicyId {
return resolveLocalPolicy(opts, mapping)
}
policy := localMappings.Policies[opts.PolicyId]
if policy != nil {
return resolveLocalPolicy(opts, policy, "", "")
}
}
@@ -102,10 +138,9 @@ func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err)
}
for _, mapping := range tufMappings.Policies {
if mapping.Id == opts.PolicyId {
return resolveTufPolicy(opts, mapping)
}
policy := tufMappings.Policies[opts.PolicyId]
if policy != nil {
return resolveTufPolicy(opts, policy, "", "")
}
return nil, fmt.Errorf("policy with id %s not found", opts.PolicyId)
}
@@ -124,7 +159,7 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver
if err != nil {
return nil, fmt.Errorf("failed to get image name: %w", err)
}
named, err := reference.ParseNormalizedNamed(imageName)
imageName, err = normalizeImageName(imageName)
if err != nil {
return nil, fmt.Errorf("failed to parse image name: %w", err)
}
@@ -132,9 +167,12 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver
if err != nil {
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
}
mapping, mirror := findPolicyMatch(named, localMappings)
if mapping != nil {
return resolveLocalPolicy(opts, mapping)
match, err := findPolicyMatch(imageName, localMappings)
if err != nil {
return nil, err
}
if match.matchType == matchTypePolicy {
return resolveLocalPolicy(opts, match.policy, imageName, match.matchedName)
}
// must check tuf
tufMappings, err := config.LoadTufMappings(opts.TufClient, opts.LocalTargetsDir)
@@ -143,20 +181,31 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver
}
// it's a mirror of a tuf policy
if mirror != nil {
if match.matchType == matchTypeMatchNoPolicy {
for _, mapping := range tufMappings.Policies {
if mapping.Id == mirror.PolicyId {
return resolveTufPolicy(opts, mapping)
if mapping.Id == match.rule.PolicyId {
return resolveTufPolicy(opts, mapping, imageName, match.matchedName)
}
}
}
// try to resolve a tuf policy directly
mapping, _ = findPolicyMatch(named, tufMappings)
if mapping == nil {
return nil, nil
match, err = findPolicyMatch(imageName, tufMappings)
if err != nil {
return nil, err
}
return resolveTufPolicy(opts, mapping)
if match.matchType == matchTypePolicy {
return resolveTufPolicy(opts, match.policy, imageName, match.matchedName)
}
return nil, nil
}
func normalizeImageName(imageName string) (string, error) {
named, err := reference.ParseNormalizedNamed(imageName)
if err != nil {
return "", fmt.Errorf("failed to parse image name: %w", err)
}
return named.Name(), nil
}
func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsResolver, error) {

View File

@@ -0,0 +1,121 @@
package policy
import (
"path/filepath"
"testing"
"github.com/docker/attest/pkg/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFindPolicyMatch(t *testing.T) {
testCases := []struct {
name string
imageName string
mappingDir string
expectError bool
expectedMatchType matchType
expectedPolicyID string
expectedImageName string
}{
{
name: "alpine",
mappingDir: "doi",
imageName: "docker.io/library/alpine",
expectedMatchType: matchTypePolicy,
expectedPolicyID: "docker-official-images",
expectedImageName: "docker.io/library/alpine",
},
{
name: "no match",
mappingDir: "doi",
imageName: "docker.io/something/else",
expectedMatchType: matchTypeNoMatch,
expectedImageName: "docker.io/something/else",
},
{
name: "match, no policy",
mappingDir: "local",
imageName: "docker.io/library/alpine",
expectedMatchType: matchTypeMatchNoPolicy,
expectedImageName: "docker.io/library/alpine",
},
{
name: "simple rewrite",
mappingDir: "simple-rewrite",
imageName: "mycoolmirror.org/library/alpine",
expectedMatchType: matchTypePolicy,
expectedPolicyID: "docker-official-images",
expectedImageName: "docker.io/library/alpine",
},
{
name: "rewrite no match",
mappingDir: "rewrite-to-no-match",
imageName: "mycoolmirror.org/library/alpine",
expectedMatchType: matchTypeNoMatch,
expectedImageName: "badredirect.org/alpine",
},
{
name: "rewrite to match, no policy",
mappingDir: "rewrite-to-local",
imageName: "mycoolmirror.org/library/alpine",
expectedMatchType: matchTypeMatchNoPolicy,
expectedImageName: "docker.io/library/alpine",
},
{
name: "multiple rewrites",
mappingDir: "rewrite-multiple",
imageName: "myevencoolermirror.org/library/alpine",
expectedMatchType: matchTypePolicy,
expectedPolicyID: "docker-official-images",
expectedImageName: "docker.io/library/alpine",
},
{
name: "invalid rewrites",
mappingDir: "rewrite-invalid",
imageName: "mycoolmirror.org/library/alpine",
expectError: true,
expectedMatchType: matchTypePolicy,
expectedPolicyID: "docker-official-images",
expectedImageName: "docker.io/library/alpine",
},
{
name: "rewrite loop",
mappingDir: "rewrite-loop",
imageName: "yin/alpine",
expectError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mappings, err := config.LoadLocalMappings(filepath.Join("testdata", "mappings", tc.mappingDir))
require.NoError(t, err)
match, err := findPolicyMatch(tc.imageName, mappings)
if tc.expectError {
require.Error(t, err)
// TODO: check error matches expected error message
return
}
require.NoError(t, err)
assert.Equal(t, tc.expectedMatchType, match.matchType)
if match.matchType == matchTypePolicy {
if assert.NotNil(t, match.policy) {
assert.Equal(t, tc.expectedPolicyID, match.policy.Id)
}
}
assert.Equal(t, tc.expectedImageName, match.matchedName)
})
}
}

View File

@@ -84,6 +84,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
src, err := oci.ParseImageSpec(imageName, oci.WithPlatform(platform.String()))
require.NoError(t, err)
resolver, err := policy.CreateImageDetailsResolver(src)
require.NoError(t, err)
policy, err := policy.ResolvePolicy(ctx, resolver, tc.policy)
if tc.errorStr != "" {
require.Error(t, err)
@@ -91,6 +92,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
return
}
require.NoErrorf(t, err, "failed to resolve policy")
require.NotNil(t, policy, "policy should not be nil")
result, err := re.Evaluate(ctx, tc.resolver, policy, input)
require.NoErrorf(t, err, "Evaluate failed")
@@ -107,8 +109,10 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
func TestLoadingMappings(t *testing.T) {
policyMappings, err := config.LoadLocalMappings(filepath.Join("testdata", "mock-tuf-allow"))
require.NoError(t, err)
assert.Equal(t, len(policyMappings.Mirrors), 1)
for _, mirror := range policyMappings.Mirrors {
assert.Equal(t, "docker-official-images", mirror.PolicyId)
assert.Equal(t, len(policyMappings.Rules), 3)
for _, mirror := range policyMappings.Rules {
if mirror.PolicyId != "" {
assert.Equal(t, "docker-official-images", mirror.PolicyId)
}
}
}

View File

@@ -0,0 +1,10 @@
version: v1
kind: policy-mapping
policies:
- id: docker-official-images
description: Docker Official Images
files:
- path: doi/policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images

View File

@@ -0,0 +1,10 @@
version: v1
kind: policy-mapping
policies:
- id: local-policy
description: Local Policy
files:
- path: local-policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images # note this policy does not exist in this file

View File

@@ -0,0 +1,13 @@
version: v1
kind: policy-mapping
policies:
- id: docker-official-images
description: Docker Official Images
files:
- path: doi/policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images
- pattern: "^mycoolmirror[.]org/library/(.*)$"
rewrite: "docker.io/library/$1"
policy-id: docker-official-images # invalid to specify both rewrite and policy-id

View File

@@ -0,0 +1,14 @@
version: v1
kind: policy-mapping
policies:
- id: docker-official-images
description: Docker Official Images
files:
- path: doi/policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images
- pattern: "^yin/(.*)$"
rewrite: "yang/$1"
- pattern: "^yang/(.*)$"
rewrite: "yin/$1"

View File

@@ -0,0 +1,14 @@
version: v1
kind: policy-mapping
policies:
- id: docker-official-images
description: Docker Official Images
files:
- path: doi/policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images
- pattern: "^mycoolmirror[.]org/library/(.*)$"
rewrite: "docker.io/library/$1"
- pattern: "^myevencoolermirror[.]org/library/(.*)$"
rewrite: "mycoolmirror.org/library/$1"

View File

@@ -0,0 +1,12 @@
version: v1
kind: policy-mapping
policies:
- id: local-policy
description: Local Policy
files:
- path: local-policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images # note this policy does not exist in this file
- pattern: "^mycoolmirror[.]org/library/(.*)$"
rewrite: "docker.io/library/$1"

View File

@@ -0,0 +1,12 @@
version: v1
kind: policy-mapping
policies:
- id: docker-official-images
description: Docker Official Images
files:
- path: doi/policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images
- pattern: "^mycoolmirror[.]org/library/(.*)$"
rewrite: "badredirect.org/$1" # no matching rule for this rewrite

View File

@@ -0,0 +1,12 @@
version: v1
kind: policy-mapping
policies:
- id: docker-official-images
description: Docker Official Images
files:
- path: doi/policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images
- pattern: "^mycoolmirror[.]org/library/(.*)$"
rewrite: "docker.io/library/$1"

View File

@@ -2,15 +2,16 @@
version: v1
kind: policy-mapping
policies:
- origin:
domain: docker.io
prefix: library/
id: docker-official-images
- id: docker-official-images
description: Docker Official Images
attestations:
repo: "localhost:5001/library-refs"
files:
- path: doi/policy.rego
mirrors:
- policy-id: docker-official-images
mirror:
domains: [localhost:5001, registry.local:5000]
prefix: ""
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images
- pattern: ^localhost:5001/(.*)$
rewrite: docker.io/library/$1
- pattern: ^registry[.]local:5000/(.*)$
rewrite: docker.io/library/$1

View File

@@ -2,17 +2,16 @@
version: v1
kind: policy-mapping
policies:
- origin:
domain: docker.io
prefix: library/
id: docker-official-images
- id: docker-official-images
description: Docker Official Images
attestations:
repo: "localhost:5001/library-refs"
files:
- path: doi/policy.rego
mirrors:
- policy-id: docker-official-images
mirror:
domains: [localhost:5001, registry.local:5000]
prefix: ""
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images
- pattern: ^localhost:5001/(.*)$
rewrite: docker.io/library/$1
- pattern: ^registry[.]local:5000/(.*)$
rewrite: docker.io/library/$1

View File

@@ -2,10 +2,10 @@
version: v1
kind: policy-mapping
policies:
- origin:
domain: docker.io
prefix: library/
id: docker-official-images
- id: docker-official-images
description: Docker Official Images
files:
- path: doi/policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images

View File

@@ -2,10 +2,10 @@
version: v1
kind: policy-mapping
policies:
- origin:
domain: docker.io
prefix: library/
id: docker-official-images
- id: docker-official-images
description: Docker Official Images
files:
- path: doi/policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images

View File

@@ -2,10 +2,10 @@
version: v1
kind: policy-mapping
policies:
- origin:
domain: docker.io
prefix: library/
id: docker-official-images
- id: docker-official-images
description: Docker Official Images
files:
- path: doi/policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images

View File

@@ -36,9 +36,10 @@ type PolicyOptions struct {
}
type Policy struct {
InputFiles []*PolicyFile
Query string
Mapping *config.PolicyMapping
InputFiles []*PolicyFile
Query string
Mapping *config.PolicyMapping
ResolvedName string
}
type PolicyInput struct {

View File

@@ -1,11 +1,10 @@
# map repos to policies
version: v1
kind: policy-mapping
policies:
- origin:
domain: docker.io
prefix: library/
id: test-images
- id: test-images
description: Local test images
files:
- path: doi/policy.rego
- path: policy.rego
rules:
- pattern: ".*"
policy-id: test-images

View File

@@ -0,0 +1,12 @@
version: v1
kind: policy-mapping
policies:
- id: test-images
description: Local test images
files:
- path: policy.rego
rules:
- pattern: "^docker[.]io/library/test-image$"
policy-id: test-images
- pattern: "^mirror[.]org/library/(.*)$"
rewrite: docker.io/library/$1

View File

@@ -0,0 +1,54 @@
package attest
import rego.v1
keys := [{
"id": "6b241993defaba26558c64f94a94303ce860e7ad9163d801495c91cf57197c75",
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZmicqYSY38DprGr42jU0V3ND0ROj\nzSRH1+yjsxhh0bi52Hh/DuOhrSq2KJ5a09lW3ybnDjljowbkof0Y1i9Oow==\n-----END PUBLIC KEY-----",
"from": "2023-12-15T14:00:00Z",
"to": null,
# this key is still active
"status": "active",
"signing-format": "dssev1",
}]
provs(pred) := p if {
res := attest.fetch(pred)
not res.error
p := res.value
}
atts := union({
provs("https://slsa.dev/provenance/v0.2"),
provs("https://spdx.dev/Document"),
})
opts := {"keys": keys}
statements contains s if {
some att in atts
res := attest.verify(att, opts)
not res.error
s := res.value
}
subjects contains subject if {
some statement in statements
some subject in statement.subject
}
success if {
print("input:",input)
true
}
result := {
"success": success,
"violations": set(),
"summary": {
"subjects": subjects,
"slsa_levels": ["SLSA_BUILD_LEVEL_3"],
"verifier": "docker-official-images",
"policy_uri": "https://docker.com/official/policy/v0.1",
},
}

View File

@@ -1,11 +1,10 @@
# map repos to policies
version: v1
kind: policy-mapping
policies:
- origin:
domain: docker.io
prefix: library/
id: test-images
- id: test-images
description: Local test images
files:
- path: doi/policy.rego
- path: policy.rego
rules:
- pattern: ".*"
policy-id: test-images

View File

@@ -1,16 +1,10 @@
# map repos to policies
version: v1
kind: policy-mapping
policies:
- origin:
domain: docker.io
prefix: library/
id: test-images
- id: test-images
description: Local test images
files:
- path: doi/policy.rego
mirrors:
- policy-id: test-images
mirror:
domains: ["*"]
prefix: ""
- path: policy.rego
rules:
- pattern: ".*"
policy-id: test-images

View File

@@ -1,29 +1,18 @@
# map repos to policies
version: v1
kind: policy-mapping
policies:
- origin:
domain: docker.io
prefix: library/
id: test-images
description: Local test images
- id: docker-official-images
description: Docker Official Images
files:
- path: "doi/policy.rego"
mirrors:
- policy-id: test-images
mirror:
domains: ["*"]
prefix: "repo"
- policy-id: test-images
mirror:
domains: ["*"]
prefix: "library/"
- policy-id: test-images
mirror:
domains: ["*"]
prefix: "test-image"
- policy-id: test-images
mirror:
domains: ["*"]
prefix: "image-signer-verifier-test"
- path: doi/policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images
- pattern: "repo$"
policy-id: docker-official-images
- pattern: "test-image$"
policy-id: docker-official-images
- pattern: "image-signer-verifier-test$"
policy-id: docker-official-images
- pattern: "library/(.*)$"
rewrite: docker.io/library/$1