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:
@@ -18,7 +18,12 @@ func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.PolicyOptions)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
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 != "" {
|
||||
pctx.Mapping.Attestations = &config.ReferrersConfig{
|
||||
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)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/pkg/attest"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/config"
|
||||
"github.com/docker/attest/pkg/mirror"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/policy"
|
||||
@@ -21,21 +22,29 @@ 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")
|
||||
LocalPolicy = filepath.Join("..", "..", "test", "testdata", "local-policy")
|
||||
LocalPolicyAttached = filepath.Join("..", "..", "test", "testdata", "local-policy-attached")
|
||||
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) {
|
||||
ctx, signer := test.Setup(t)
|
||||
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
|
||||
platforms := []string{"linux/amd64", "linux/arm64"}
|
||||
for _, tc := range []struct {
|
||||
server *httptest.Server
|
||||
skipSubject bool
|
||||
useDigest bool
|
||||
server *httptest.Server
|
||||
referrersServer *httptest.Server
|
||||
skipSubject bool
|
||||
useDigest bool
|
||||
referrersRepo string
|
||||
attestationSource config.AttestationStyle
|
||||
expectFailure bool
|
||||
policyDir string
|
||||
}{
|
||||
{
|
||||
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(registry.WithReferrersSupport(true))),
|
||||
skipSubject: true,
|
||||
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||
skipSubject: true,
|
||||
attestationSource: config.AttestationStyleAttached,
|
||||
},
|
||||
{
|
||||
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(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
|
||||
defer s.Close()
|
||||
u, err := url.Parse(s.URL)
|
||||
require.NoError(t, err)
|
||||
t.Run(fmt.Sprint(tc), func(t *testing.T) {
|
||||
s := tc.server
|
||||
defer s.Close()
|
||||
|
||||
opts := &attestation.SigningOptions{
|
||||
Replace: true,
|
||||
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())
|
||||
if tc.referrersServer != nil {
|
||||
defer tc.referrersServer.Close()
|
||||
}
|
||||
u, err := url.Parse(s.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
policyOpts := &policy.PolicyOptions{
|
||||
LocalPolicyDir: PassPolicyDir,
|
||||
opts := &attestation.SigningOptions{
|
||||
Replace: true,
|
||||
SkipSubject: tc.skipSubject,
|
||||
}
|
||||
src, err := oci.ParseImageSpec(ref, oci.WithPlatform(platform))
|
||||
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
|
||||
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 {
|
||||
// can evaluate policy using referrers
|
||||
if tc.useDigest {
|
||||
p, err := oci.ParsePlatform(platform)
|
||||
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||
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)
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
desc, err := remote.Image(subjectRef, options...)
|
||||
desc, err := remote.Index(subjectRef, options...)
|
||||
require.NoError(t, err)
|
||||
subjectDigest, err := desc.Digest()
|
||||
idxDigest, err := desc.Digest()
|
||||
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))
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package config
|
||||
|
||||
type PolicyMappings struct {
|
||||
Version string `json:"version"`
|
||||
Kind string `json:"kind"`
|
||||
Policies []PolicyMapping `json:"policies"`
|
||||
Mirrors []PolicyMirror `json:"mirrors"`
|
||||
Version string `json:"version"`
|
||||
Kind string `json:"kind"`
|
||||
Policies []*PolicyMapping `json:"policies"`
|
||||
Mirrors []*PolicyMirror `json:"mirrors"`
|
||||
}
|
||||
|
||||
type AttestationSource string
|
||||
type AttestationStyle string
|
||||
|
||||
const (
|
||||
AttestationSourceAttached AttestationSource = "attached"
|
||||
AttestationSourceReferrers AttestationSource = "referrers"
|
||||
AttestationStyleAttached AttestationStyle = "attached"
|
||||
AttestationStyleReferrers AttestationStyle = "referrers"
|
||||
)
|
||||
|
||||
type PolicyMapping struct {
|
||||
@@ -23,8 +23,8 @@ type PolicyMapping struct {
|
||||
}
|
||||
|
||||
type ReferrersConfig struct {
|
||||
Style AttestationSource `json:"style"`
|
||||
Repo string `json:"repo"`
|
||||
Style AttestationStyle `json:"style"`
|
||||
Repo string `json:"repo"`
|
||||
}
|
||||
|
||||
type PolicyMappingFile struct {
|
||||
|
||||
@@ -47,24 +47,16 @@ func (r *RegistryImageDetailsResolver) ImageDigest(ctx context.Context) (string,
|
||||
if err != nil {
|
||||
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)
|
||||
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)
|
||||
options := WithOptions(ctx, r.Platform)
|
||||
desc, err := remote.Image(subjectRef, 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()
|
||||
}
|
||||
return r.digest, nil
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ func findPolicyMatch(named reference.Named, mappings *config.PolicyMappings) (*c
|
||||
for _, mapping := range mappings.Policies {
|
||||
if mapping.Origin.Domain == reference.Domain(named) &&
|
||||
strings.HasPrefix(reference.Path(named), mapping.Origin.Prefix) {
|
||||
return &mapping, nil
|
||||
return mapping, nil
|
||||
}
|
||||
}
|
||||
// now search mirrors
|
||||
@@ -73,10 +73,10 @@ func findPolicyMatch(named reference.Named, mappings *config.PolicyMappings) (*c
|
||||
strings.HasPrefix(reference.Path(named), mirror.Mirror.Prefix) {
|
||||
for _, mapping := range mappings.Policies {
|
||||
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 {
|
||||
for _, mapping := range localMappings.Policies {
|
||||
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 {
|
||||
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)
|
||||
@@ -146,7 +146,7 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver
|
||||
if mirror != nil {
|
||||
for _, mapping := range tufMappings.Policies {
|
||||
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) {
|
||||
switch resolver := resolver.(type) {
|
||||
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)
|
||||
} else {
|
||||
if mapping.Attestations != nil && mapping.Attestations.Repo != "" {
|
||||
|
||||
@@ -27,11 +27,12 @@ type Result struct {
|
||||
}
|
||||
|
||||
type PolicyOptions struct {
|
||||
TufClient tuf.TUFClient
|
||||
LocalTargetsDir string
|
||||
LocalPolicyDir string
|
||||
PolicyId string
|
||||
ReferrersRepo string
|
||||
TufClient tuf.TUFClient
|
||||
LocalTargetsDir string
|
||||
LocalPolicyDir string
|
||||
PolicyId string
|
||||
ReferrersRepo string
|
||||
AttestationStyle config.AttestationStyle
|
||||
}
|
||||
|
||||
type Policy struct {
|
||||
|
||||
49
test/testdata/local-policy/doi/policy.rego
vendored
Normal file
49
test/testdata/local-policy/doi/policy.rego
vendored
Normal 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
29
test/testdata/local-policy/mapping.yaml
vendored
Normal 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"
|
||||
Reference in New Issue
Block a user