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"
@@ -21,21 +22,29 @@ import (
) )
var ( 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")
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl") LocalPolicy = filepath.Join("..", "..", "test", "testdata", "local-policy")
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail") LocalPolicyAttached = filepath.Join("..", "..", "test", "testdata", "local-policy-attached")
TestTempDir = "attest-sign-test" PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
TestTempDir = "attest-sign-test"
) )
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
skipSubject bool referrersServer *httptest.Server
useDigest bool skipSubject 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))),
@@ -44,76 +53,135 @@ func TestAttestationReferenceTypes(t *testing.T) {
server: httptest.NewServer(registry.New()), server: httptest.NewServer(registry.New()),
}, },
{ {
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))),
},
} { } {
s := tc.server t.Run(fmt.Sprint(tc), func(t *testing.T) {
defer s.Close() s := tc.server
u, err := url.Parse(s.URL) defer s.Close()
require.NoError(t, err)
opts := &attestation.SigningOptions{ if tc.referrersServer != nil {
Replace: true, defer tc.referrersServer.Close()
SkipSubject: tc.skipSubject,
}
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
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)
require.NoError(t, err)
err = mirror.PushIndexToRegistry(signedIndex, indexName)
for _, platform := range platforms {
// can eval policy in the normal way
ref := indexName
if tc.useDigest {
options := oci.WithOptions(ctx, nil)
subjectRef, err := name.ParseReference(indexName)
require.NoError(t, err)
desc, err := remote.Index(subjectRef, options...)
require.NoError(t, err)
idxDigest, err := desc.Digest()
require.NoError(t, err)
ref = fmt.Sprintf("%s/repo@%s", u.Host, idxDigest.String())
} }
u, err := url.Parse(s.URL)
require.NoError(t, err)
policyOpts := &policy.PolicyOptions{ opts := &attestation.SigningOptions{
LocalPolicyDir: PassPolicyDir, Replace: true,
SkipSubject: tc.skipSubject,
} }
src, err := oci.ParseImageSpec(ref, oci.WithPlatform(platform)) attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
require.NoError(t, err) require.NoError(t, err)
results, err := attest.Verify(ctx, src, policyOpts)
require.NoError(t, err)
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
if !tc.skipSubject { indexName := fmt.Sprintf("%s/repo:root", u.Host)
// can evaluate policy using referrers require.NoError(t, err)
if tc.useDigest {
p, err := oci.ParsePlatform(platform) 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) require.NoError(t, err)
options := oci.WithOptions(ctx, p) }
} else {
signedIndex, err := attest.Sign(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
err = mirror.PushIndexToRegistry(signedIndex, indexName)
require.NoError(t, err)
}
for _, platform := range platforms {
// can eval policy in the normal way
ref := indexName
if tc.useDigest {
options := oci.WithOptions(ctx, nil)
subjectRef, err := name.ParseReference(indexName) subjectRef, err := name.ParseReference(indexName)
require.NoError(t, err) require.NoError(t, err)
desc, err := remote.Image(subjectRef, options...) desc, err := remote.Index(subjectRef, options...)
require.NoError(t, err) require.NoError(t, err)
subjectDigest, err := desc.Digest() idxDigest, err := desc.Digest()
require.NoError(t, err) require.NoError(t, err)
ref = fmt.Sprintf("%s/repo@%s", u.Host, subjectDigest.String()) ref = fmt.Sprintf("%s/repo@%s", u.Host, idxDigest.String())
}
policyOpts := &policy.PolicyOptions{
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)
if !tc.skipSubject {
// can evaluate policy using referrers
if tc.useDigest {
p, err := oci.ParsePlatform(platform)
require.NoError(t, err)
options := oci.WithOptions(ctx, p)
subjectRef, err := name.ParseReference(indexName)
require.NoError(t, err)
desc, err := remote.Image(subjectRef, options...)
require.NoError(t, err)
subjectDigest, err := desc.Digest()
require.NoError(t, err)
ref = fmt.Sprintf("%s/repo@%s", u.Host, subjectDigest.String())
}
src, err := oci.ParseImageSpec(ref, oci.WithPlatform(platform))
require.NoError(t, err)
results, err = attest.Verify(ctx, src, policyOpts)
require.NoError(t, err)
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
}
} }
} })
} }
} }

View File

@@ -1,17 +1,17 @@
package config 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,8 +23,8 @@ 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"`
} }
type PolicyMappingFile struct { type PolicyMappingFile struct {

View File

@@ -47,24 +47,16 @@ 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) { options := WithOptions(ctx, r.Platform)
case name.Digest: desc, err := remote.Image(subjectRef, options...)
// TODO should check if this is an index or an image if err != nil {
r.digest = t.DigestStr() return "", fmt.Errorf("failed to get image manifest: %w", err)
case name.Tag:
options := WithOptions(ctx, r.Platform)
desc, err := remote.Image(t, options...)
if err != nil {
return "", fmt.Errorf("failed to get image manifest: %w", err)
}
subjectDigest, err := desc.Digest()
if err != nil {
return "", fmt.Errorf("failed to get image digest: %w", err)
}
r.digest = subjectDigest.String()
default:
return "", fmt.Errorf("unsupported reference type: %T", t)
} }
subjectDigest, err := desc.Digest()
if err != nil {
return "", fmt.Errorf("failed to get image digest: %w", err)
}
r.digest = subjectDigest.String()
} }
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

@@ -27,11 +27,12 @@ type Result struct {
} }
type PolicyOptions struct { type PolicyOptions struct {
TufClient tuf.TUFClient TufClient tuf.TUFClient
LocalTargetsDir string LocalTargetsDir string
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"