Add support for separate attestation storage repo (#62)

* Add support for separate attestation storage repo
* Move mapping file types and parsing to config package
* Change signature of Verify to take image/platform
* Separate Attestation Resolvers to their own files (registry, layout and referrers)
* Add support configuring referrers resolution style in mapping.yaml
* Add registry test
This commit is contained in:
James Carnegie
2024-06-21 11:29:16 +01:00
committed by GitHub
parent 86878482c3
commit 6bd57e02b6
20 changed files with 877 additions and 619 deletions

View File

@@ -10,95 +10,11 @@ import (
"strings"
"github.com/distribution/reference"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/tuf"
intoto "github.com/in-toto/in-toto-golang/in_toto"
goyaml "gopkg.in/yaml.v3"
)
const (
PolicyMappingFileName = "mapping.yaml"
)
type Summary struct {
Subjects []intoto.Subject `json:"subjects"`
SLSALevels []string `json:"slsa_levels"`
Verifier string `json:"verifier"`
PolicyURI string `json:"policy_uri"`
}
type Violation struct {
Type string `json:"type"`
Description string `json:"description"`
Attestation *intoto.Statement `json:"attestation"`
Details map[string]any `json:"details"`
}
type Result struct {
Success bool `json:"success"`
Violations []Violation `json:"violations"`
Summary Summary `json:"summary"`
}
type PolicyMappings struct {
Version string `json:"version"`
Kind string `json:"kind"`
Policies []PolicyMapping `json:"policies"`
Mirrors []PolicyMirror `json:"mirrors"`
}
type PolicyMapping struct {
Id string `json:"id"`
Description string `json:"description"`
Origin PolicyOrigin `json:"origin"`
Files []PolicyMappingFile `json:"files"`
}
type PolicyMappingFile struct {
Path string `json:"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 PolicyOptions struct {
TufClient tuf.TUFClient
LocalTargetsDir string
LocalPolicyDir string
PolicyId string
}
type Policy struct {
InputFiles []*PolicyFile
Query string
}
type PolicyInput struct {
Digest string `json:"digest"`
Purl string `json:"purl"`
IsCanonical bool `json:"isCanonical"`
}
type PolicyFile struct {
Path string
Content []byte
}
func resolveLocalPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, error) {
func resolveLocalPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*Policy, error) {
if opts.LocalPolicyDir == "" {
return nil, fmt.Errorf("local policy dir not set")
}
@@ -117,28 +33,12 @@ func resolveLocalPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, e
}
policy := &Policy{
InputFiles: files,
Mapping: mapping,
}
return policy, nil
}
func LoadLocalMappings(opts *PolicyOptions) (*PolicyMappings, error) {
if opts.LocalPolicyDir == "" {
return nil, nil
}
mappings := &PolicyMappings{}
path := path.Join(opts.LocalPolicyDir, PolicyMappingFileName)
mappingFile, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read policy mapping file %s: %w", path, err)
}
err = goyaml.Unmarshal(mappingFile, mappings)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", path, err)
}
return mappings, nil
}
func resolveTufPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, error) {
func resolveTufPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*Policy, error) {
files := make([]*PolicyFile, 0, len(mapping.Files))
for _, f := range mapping.Files {
filename := f.Path
@@ -153,29 +53,12 @@ func resolveTufPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, err
}
policy := &Policy{
InputFiles: files,
Mapping: mapping,
}
return policy, nil
}
func loadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMappings, error) {
if tufClient == nil {
return nil, fmt.Errorf("tuf client not set")
}
filename := PolicyMappingFileName
_, fileContents, err := tufClient.DownloadTarget(filename, filepath.Join(localTargetsDir, filename))
if err != nil {
return nil, fmt.Errorf("failed to download policy file %s: %w", filename, err)
}
mappings := &PolicyMappings{}
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
}
func findPolicyMatch(named reference.Named, mappings *PolicyMappings) (*PolicyMapping, *PolicyMirror) {
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) &&
@@ -202,7 +85,7 @@ func findPolicyMatch(named reference.Named, mappings *PolicyMappings) (*PolicyMa
func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
if opts.PolicyId != "" {
localMappings, err := LoadLocalMappings(opts)
localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir)
if err != nil {
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
}
@@ -215,9 +98,9 @@ func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
}
// must check tuf
tufMappings, err := loadTufMappings(opts.TufClient, opts.LocalTargetsDir)
tufMappings, err := config.LoadTufMappings(opts.TufClient, opts.LocalTargetsDir)
if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings: %w", err)
return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err)
}
for _, mapping := range tufMappings.Policies {
if mapping.Id == opts.PolicyId {
@@ -229,7 +112,7 @@ func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
return nil, nil
}
func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *PolicyOptions) (*Policy, error) {
func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver, opts *PolicyOptions) (*Policy, error) {
p, err := resolvePolicyById(opts)
if err != nil {
return nil, fmt.Errorf("failed to resolve policy by id: %w", err)
@@ -237,8 +120,7 @@ func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *
if p != nil {
return p, nil
}
imageName, err := resolver.ImageName(ctx)
imageName, err := detailsResolver.ImageName(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get image name: %w", err)
}
@@ -246,7 +128,7 @@ func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *
if err != nil {
return nil, fmt.Errorf("failed to parse image name: %w", err)
}
localMappings, err := LoadLocalMappings(opts)
localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir)
if err != nil {
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
}
@@ -255,9 +137,9 @@ func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *
return resolveLocalPolicy(opts, mapping)
}
// must check tuf
tufMappings, err := loadTufMappings(opts.TufClient, opts.LocalTargetsDir)
tufMappings, err := config.LoadTufMappings(opts.TufClient, opts.LocalTargetsDir)
if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings: %w", err)
return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err)
}
// it's a mirror of a tuf policy
@@ -276,3 +158,32 @@ func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *
}
return resolveTufPolicy(opts, mapping)
}
func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsResolver, error) {
switch imageSource.Type {
case oci.OCI:
return oci.NewOCILayoutAttestationResolver(imageSource)
case oci.Docker:
return oci.NewRegistryImageDetailsResolver(imageSource)
}
return nil, fmt.Errorf("unsupported image source type: %s", imageSource.Type)
}
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 {
return oci.NewRegistryAttestationResolver(resolver)
} else {
if mapping.Attestations != nil && mapping.Attestations.Repo != "" {
return oci.NewReferrersAttestationResolver(resolver, oci.WithReferrersRepo(mapping.Attestations.Repo))
} else {
return oci.NewReferrersAttestationResolver(resolver)
}
}
case *oci.OCILayoutResolver:
return resolver, nil
default:
return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver)
}
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
"github.com/docker/attest/pkg/tuf"
@@ -76,8 +77,14 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
PolicyId: tc.policyId,
}
}
policy, err := policy.ResolvePolicy(ctx, tc.resolver, tc.policy)
imageName, err := tc.resolver.ImageName(ctx)
require.NoError(t, err)
platform, err := tc.resolver.ImagePlatform(ctx)
require.NoError(t, err)
src, err := oci.ParseImageSpec(imageName, oci.WithPlatform(platform.String()))
require.NoError(t, err)
resolver, err := policy.CreateImageDetailsResolver(src)
policy, err := policy.ResolvePolicy(ctx, resolver, tc.policy)
if tc.errorStr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errorStr)
@@ -98,10 +105,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
}
func TestLoadingMappings(t *testing.T) {
opts := &policy.PolicyOptions{
LocalPolicyDir: filepath.Join("testdata", "mock-tuf-allow"),
}
policyMappings, err := policy.LoadLocalMappings(opts)
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 {

View File

@@ -7,6 +7,8 @@ policies:
prefix: library/
id: docker-official-images
description: Docker Official Images
attestations:
repo: "localhost:5001/library-refs"
files:
- path: doi/policy.rego
mirrors:

52
pkg/policy/types.go Normal file
View File

@@ -0,0 +1,52 @@
package policy
import (
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/tuf"
intoto "github.com/in-toto/in-toto-golang/in_toto"
)
type Summary struct {
Subjects []intoto.Subject `json:"subjects"`
SLSALevels []string `json:"slsa_levels"`
Verifier string `json:"verifier"`
PolicyURI string `json:"policy_uri"`
}
type Violation struct {
Type string `json:"type"`
Description string `json:"description"`
Attestation *intoto.Statement `json:"attestation"`
Details map[string]any `json:"details"`
}
type Result struct {
Success bool `json:"success"`
Violations []Violation `json:"violations"`
Summary Summary `json:"summary"`
}
type PolicyOptions struct {
TufClient tuf.TUFClient
LocalTargetsDir string
LocalPolicyDir string
PolicyId string
ReferrersRepo string
}
type Policy struct {
InputFiles []*PolicyFile
Query string
Mapping *config.PolicyMapping
}
type PolicyInput struct {
Digest string `json:"digest"`
Purl string `json:"purl"`
IsCanonical bool `json:"isCanonical"`
}
type PolicyFile struct {
Path string
Content []byte
}