diff --git a/pkg/attest/verify.go b/pkg/attest/verify.go index 9905890..dc72da7 100644 --- a/pkg/attest/verify.go +++ b/pkg/attest/verify.go @@ -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) diff --git a/pkg/attestation/referrers_test.go b/pkg/attestation/referrers_test.go index 54acdc8..8ec705f 100644 --- a/pkg/attestation/referrers_test.go +++ b/pkg/attestation/referrers_test.go @@ -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) + } } - } + }) } } diff --git a/pkg/config/types.go b/pkg/config/types.go index 9bb5f0b..87e9dc5 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -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 { diff --git a/pkg/oci/registry.go b/pkg/oci/registry.go index c79ea62..ab0f6a5 100644 --- a/pkg/oci/registry.go +++ b/pkg/oci/registry.go @@ -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 } diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index 3a41e92..184cfcf 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -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 != "" { diff --git a/pkg/policy/types.go b/pkg/policy/types.go index e1fbf56..d44665a 100644 --- a/pkg/policy/types.go +++ b/pkg/policy/types.go @@ -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 { diff --git a/test/testdata/local-policy/doi/policy.rego b/test/testdata/local-policy/doi/policy.rego new file mode 100644 index 0000000..aadb8cd --- /dev/null +++ b/test/testdata/local-policy/doi/policy.rego @@ -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", + }, +} diff --git a/test/testdata/local-policy/mapping.yaml b/test/testdata/local-policy/mapping.yaml new file mode 100644 index 0000000..557c8d5 --- /dev/null +++ b/test/testdata/local-policy/mapping.yaml @@ -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"