Various fixes (#63)

* Fix digest resolution and attestation style

* Add a bunch more tests

* Rename fields for consistency

* Remove copy-pasta

* Value -> pointer
This commit is contained in:
James Carnegie
2024-06-21 22:12:42 +01:00
committed by GitHub
parent 6bd57e02b6
commit d11b78fbb8
8 changed files with 245 additions and 96 deletions

View File

@@ -18,7 +18,12 @@ func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.PolicyOptions)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create image details resolver: %w", err) return nil, fmt.Errorf("failed to create image details resolver: %w", err)
} }
if opts.AttestationStyle == "" {
opts.AttestationStyle = config.AttestationStyleReferrers
}
if opts.ReferrersRepo != "" && opts.AttestationStyle != config.AttestationStyleReferrers {
return nil, fmt.Errorf("referrers repo specified but attestation source not set to referrers")
}
pctx, err := policy.ResolvePolicy(ctx, detailsResolver, opts) pctx, err := policy.ResolvePolicy(ctx, detailsResolver, opts)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to resolve policy: %w", err) return nil, fmt.Errorf("failed to resolve policy: %w", err)
@@ -33,7 +38,12 @@ func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.PolicyOptions)
if opts.ReferrersRepo != "" { if opts.ReferrersRepo != "" {
pctx.Mapping.Attestations = &config.ReferrersConfig{ pctx.Mapping.Attestations = &config.ReferrersConfig{
Repo: opts.ReferrersRepo, Repo: opts.ReferrersRepo,
Style: config.AttestationSourceReferrers, Style: config.AttestationStyleReferrers,
}
} else if opts.AttestationStyle == config.AttestationStyleAttached {
pctx.Mapping.Attestations = &config.ReferrersConfig{
Repo: opts.ReferrersRepo,
Style: config.AttestationStyleAttached,
} }
} }
// because we have a mapping now, we can select a resolver based on its contents (ie. referrers or attached) // because we have a mapping now, we can select a resolver based on its contents (ie. referrers or attached)

View File

@@ -10,6 +10,7 @@ import (
"github.com/docker/attest/internal/test" "github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/attest" "github.com/docker/attest/pkg/attest"
"github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/mirror" "github.com/docker/attest/pkg/mirror"
"github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy" "github.com/docker/attest/pkg/policy"
@@ -24,6 +25,8 @@ var (
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image") UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image") NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass") PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
LocalPolicy = filepath.Join("..", "..", "test", "testdata", "local-policy")
LocalPolicyAttached = filepath.Join("..", "..", "test", "testdata", "local-policy-attached")
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl") PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail") FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
TestTempDir = "attest-sign-test" TestTempDir = "attest-sign-test"
@@ -31,11 +34,17 @@ var (
func TestAttestationReferenceTypes(t *testing.T) { func TestAttestationReferenceTypes(t *testing.T) {
ctx, signer := test.Setup(t) ctx, signer := test.Setup(t)
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
platforms := []string{"linux/amd64", "linux/arm64"} platforms := []string{"linux/amd64", "linux/arm64"}
for _, tc := range []struct { for _, tc := range []struct {
server *httptest.Server server *httptest.Server
referrersServer *httptest.Server
skipSubject bool skipSubject bool
useDigest bool useDigest bool
referrersRepo string
attestationSource config.AttestationStyle
expectFailure bool
policyDir string
}{ }{
{ {
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))), server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
@@ -46,14 +55,41 @@ func TestAttestationReferenceTypes(t *testing.T) {
{ {
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))), server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
skipSubject: true, skipSubject: true,
attestationSource: config.AttestationStyleAttached,
}, },
{ {
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))), server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
useDigest: true, useDigest: true,
}, },
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
expectFailure: true, //mismatched args
attestationSource: config.AttestationStyleAttached,
referrersRepo: "referrers",
},
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
expectFailure: true, // no policy
attestationSource: config.AttestationStyleReferrers,
referrersRepo: "referrers",
},
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
attestationSource: config.AttestationStyleReferrers,
},
{
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) {
s := tc.server s := tc.server
defer s.Close() defer s.Close()
if tc.referrersServer != nil {
defer tc.referrersServer.Close()
}
u, err := url.Parse(s.URL) u, err := url.Parse(s.URL)
require.NoError(t, err) require.NoError(t, err)
@@ -63,12 +99,28 @@ func TestAttestationReferenceTypes(t *testing.T) {
} }
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage) attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
require.NoError(t, err) require.NoError(t, err)
signedIndex, err := attest.Sign(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
indexName := fmt.Sprintf("%s/repo:root", u.Host) indexName := fmt.Sprintf("%s/repo:root", u.Host)
require.NoError(t, err) require.NoError(t, err)
if tc.referrersServer != nil {
ru, err := url.Parse(s.URL)
require.NoError(t, err)
repo := fmt.Sprintf("%s/referrers", ru.Host)
tc.referrersRepo = repo
images, err := attest.SignedAttestationImages(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
for _, img := range images {
err = mirror.PushImageToRegistry(img.Image, fmt.Sprintf("%s:tag-does-not-matter", repo))
require.NoError(t, err)
}
} else {
signedIndex, err := attest.Sign(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
err = mirror.PushIndexToRegistry(signedIndex, indexName) err = mirror.PushIndexToRegistry(signedIndex, indexName)
require.NoError(t, err)
}
for _, platform := range platforms { for _, platform := range platforms {
// can eval policy in the normal way // can eval policy in the normal way
@@ -85,11 +137,26 @@ func TestAttestationReferenceTypes(t *testing.T) {
} }
policyOpts := &policy.PolicyOptions{ policyOpts := &policy.PolicyOptions{
LocalPolicyDir: PassPolicyDir, LocalPolicyDir: LocalPolicy,
}
if tc.policyDir != "" {
policyOpts.LocalPolicyDir = tc.policyDir
}
if tc.referrersRepo != "" {
policyOpts.ReferrersRepo = tc.referrersRepo
}
if tc.attestationSource != "" {
policyOpts.AttestationStyle = tc.attestationSource
} }
src, err := oci.ParseImageSpec(ref, oci.WithPlatform(platform)) src, err := oci.ParseImageSpec(ref, oci.WithPlatform(platform))
require.NoError(t, err) require.NoError(t, err)
results, err := attest.Verify(ctx, src, policyOpts) results, err := attest.Verify(ctx, src, policyOpts)
if tc.expectFailure {
require.Error(t, err)
continue
}
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, attest.OutcomeSuccess, results.Outcome) assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
@@ -114,6 +181,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
assert.Equal(t, attest.OutcomeSuccess, results.Outcome) assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
} }
} }
})
} }
} }

View File

@@ -3,15 +3,15 @@ package config
type PolicyMappings struct { type PolicyMappings struct {
Version string `json:"version"` Version string `json:"version"`
Kind string `json:"kind"` Kind string `json:"kind"`
Policies []PolicyMapping `json:"policies"` Policies []*PolicyMapping `json:"policies"`
Mirrors []PolicyMirror `json:"mirrors"` Mirrors []*PolicyMirror `json:"mirrors"`
} }
type AttestationSource string type AttestationStyle string
const ( const (
AttestationSourceAttached AttestationSource = "attached" AttestationStyleAttached AttestationStyle = "attached"
AttestationSourceReferrers AttestationSource = "referrers" AttestationStyleReferrers AttestationStyle = "referrers"
) )
type PolicyMapping struct { type PolicyMapping struct {
@@ -23,7 +23,7 @@ type PolicyMapping struct {
} }
type ReferrersConfig struct { type ReferrersConfig struct {
Style AttestationSource `json:"style"` Style AttestationStyle `json:"style"`
Repo string `json:"repo"` Repo string `json:"repo"`
} }

View File

@@ -47,13 +47,8 @@ func (r *RegistryImageDetailsResolver) ImageDigest(ctx context.Context) (string,
if err != nil { if err != nil {
return "", fmt.Errorf("failed to parse reference: %w", err) return "", fmt.Errorf("failed to parse reference: %w", err)
} }
switch t := subjectRef.(type) {
case name.Digest:
// TODO should check if this is an index or an image
r.digest = t.DigestStr()
case name.Tag:
options := WithOptions(ctx, r.Platform) options := WithOptions(ctx, r.Platform)
desc, err := remote.Image(t, options...) desc, err := remote.Image(subjectRef, options...)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get image manifest: %w", err) return "", fmt.Errorf("failed to get image manifest: %w", err)
} }
@@ -62,9 +57,6 @@ func (r *RegistryImageDetailsResolver) ImageDigest(ctx context.Context) (string,
return "", fmt.Errorf("failed to get image digest: %w", err) return "", fmt.Errorf("failed to get image digest: %w", err)
} }
r.digest = subjectDigest.String() r.digest = subjectDigest.String()
default:
return "", fmt.Errorf("unsupported reference type: %T", t)
}
} }
return r.digest, nil return r.digest, nil
} }

View File

@@ -63,7 +63,7 @@ func findPolicyMatch(named reference.Named, mappings *config.PolicyMappings) (*c
for _, mapping := range mappings.Policies { for _, mapping := range mappings.Policies {
if mapping.Origin.Domain == reference.Domain(named) && if mapping.Origin.Domain == reference.Domain(named) &&
strings.HasPrefix(reference.Path(named), mapping.Origin.Prefix) { strings.HasPrefix(reference.Path(named), mapping.Origin.Prefix) {
return &mapping, nil return mapping, nil
} }
} }
// now search mirrors // now search mirrors
@@ -73,10 +73,10 @@ func findPolicyMatch(named reference.Named, mappings *config.PolicyMappings) (*c
strings.HasPrefix(reference.Path(named), mirror.Mirror.Prefix) { strings.HasPrefix(reference.Path(named), mirror.Mirror.Prefix) {
for _, mapping := range mappings.Policies { for _, mapping := range mappings.Policies {
if mapping.Id == mirror.PolicyId { if mapping.Id == mirror.PolicyId {
return &mapping, nil return mapping, nil
} }
} }
return nil, &mirror return nil, mirror
} }
} }
} }
@@ -92,7 +92,7 @@ func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
if localMappings != nil { if localMappings != nil {
for _, mapping := range localMappings.Policies { for _, mapping := range localMappings.Policies {
if mapping.Id == opts.PolicyId { if mapping.Id == opts.PolicyId {
return resolveLocalPolicy(opts, &mapping) return resolveLocalPolicy(opts, mapping)
} }
} }
} }
@@ -104,7 +104,7 @@ func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
} }
for _, mapping := range tufMappings.Policies { for _, mapping := range tufMappings.Policies {
if mapping.Id == opts.PolicyId { if mapping.Id == opts.PolicyId {
return resolveTufPolicy(opts, &mapping) return resolveTufPolicy(opts, mapping)
} }
} }
return nil, fmt.Errorf("policy with id %s not found", opts.PolicyId) return nil, fmt.Errorf("policy with id %s not found", opts.PolicyId)
@@ -146,7 +146,7 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver
if mirror != nil { if mirror != nil {
for _, mapping := range tufMappings.Policies { for _, mapping := range tufMappings.Policies {
if mapping.Id == mirror.PolicyId { if mapping.Id == mirror.PolicyId {
return resolveTufPolicy(opts, &mapping) return resolveTufPolicy(opts, mapping)
} }
} }
} }
@@ -172,7 +172,7 @@ func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsRes
func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *config.PolicyMapping) (oci.AttestationResolver, error) { func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *config.PolicyMapping) (oci.AttestationResolver, error) {
switch resolver := resolver.(type) { switch resolver := resolver.(type) {
case *oci.RegistryImageDetailsResolver: case *oci.RegistryImageDetailsResolver:
if mapping.Attestations != nil && mapping.Attestations.Style == config.AttestationSourceAttached { if mapping.Attestations != nil && mapping.Attestations.Style == config.AttestationStyleAttached {
return oci.NewRegistryAttestationResolver(resolver) return oci.NewRegistryAttestationResolver(resolver)
} else { } else {
if mapping.Attestations != nil && mapping.Attestations.Repo != "" { if mapping.Attestations != nil && mapping.Attestations.Repo != "" {

View File

@@ -32,6 +32,7 @@ type PolicyOptions struct {
LocalPolicyDir string LocalPolicyDir string
PolicyId string PolicyId string
ReferrersRepo string ReferrersRepo string
AttestationStyle config.AttestationStyle
} }
type Policy struct { type Policy struct {

View File

@@ -0,0 +1,49 @@
package attest
import rego.v1
keys := [{
"id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4",
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgH23D1i2+ZIOtVjmfB7iFvX8AhVN\n9CPJ4ie9axw+WRHozGnRy99U2dRge3zueBBg2MweF0zrToXGig2v3YOrdw==\n-----END PUBLIC KEY-----",
"from": "2023-12-15T14:00:00Z",
"to": null,
"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
}
result := {
"success": count(atts) > 0,
"violations": set(),
"attestations": statements,
"summary": {
"subjects": subjects,
"slsa_level": "SLSA_BUILD_LEVEL_3",
"verifier": "docker-official-images",
"policy_uri": "https://docker.com/official/policy/v0.1",
},
}

29
test/testdata/local-policy/mapping.yaml vendored Normal file
View File

@@ -0,0 +1,29 @@
# map repos to policies
version: v1
kind: policy-mapping
policies:
- origin:
domain: docker.io
prefix: library/
id: test-images
description: Local test 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"