fix: use a client pointing at Docker's TUF by default (#104)

`policy.Options` now contains the arguments to `tuf.Client`'s constructor rather than an actual Client. If these arguments are not provided, defaults pointing at Docker's TUF repo will be used. An actual TUF client can be passed in on the context (which is useful for testing). If this is not provided `attest.Verify` will create a TUF client using the options on `policy.Options`.

---------

Co-authored-by: Joel Kamp <joel.kamp@docker.com>
This commit is contained in:
Jonny Stoten
2024-08-23 09:33:30 +01:00
committed by GitHub
parent 802725caf0
commit aed959f858
18 changed files with 227 additions and 84 deletions

View File

@@ -50,7 +50,7 @@ jobs:
token: ${{ secrets.TC_CLOUD_TOKEN }}
- name: go test including e2e
if: matrix.os == 'ubuntu-latest' && github.actor != 'dependabot[bot]'
run: go test -tags=e2e -v ./... -coverprofile=coverage.out -covermode=atomic
run: go test -tags=e2e -v ./... -coverpkg=./... -coverprofile=coverage.out -covermode=atomic
- name: go test excluding e2e
if: matrix.os == 'macos-latest' || github.actor == 'dependabot[bot]'
run: go test -v ./...

View File

@@ -6,24 +6,12 @@ import (
"os"
"path/filepath"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/pkg/attest"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
"github.com/docker/attest/pkg/tuf"
)
func createTufClient(outputPath string) (*tuf.Client, error) {
// using oci tuf metadata and targets
metadataURI := "registry-1.docker.io/docker/tuf-metadata:latest"
targetsURI := "registry-1.docker.io/docker/tuf-targets"
// example using http tuf metadata and targets
// metadataURI := "https://docker.github.io/tuf-staging/metadata"
// targetsURI := "https://docker.github.io/tuf-staging/targets"
return tuf.NewClient(embed.RootStaging.Data, outputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker())
}
func ExampleVerify_remote() {
// create a tuf client
home, err := os.UserHomeDir()
@@ -31,10 +19,7 @@ func ExampleVerify_remote() {
panic(err)
}
tufOutputPath := filepath.Join(home, ".docker", "tuf")
tufClient, err := createTufClient(tufOutputPath)
if err != nil {
panic(err)
}
tufClientOpts := tuf.NewDockerDefaultClientOptions(tufOutputPath)
// create a resolver for remote attestations
image := "registry-1.docker.io/library/notary:server"
@@ -42,10 +27,10 @@ func ExampleVerify_remote() {
// configure policy options
opts := &policy.Options{
TUFClient: tufClient,
LocalTargetsDir: filepath.Join(home, ".docker", "policy"), // location to store policy files downloaded from TUF
LocalPolicyDir: "", // overrides TUF policy for local policy files if set
PolicyID: "", // set to ignore policy mapping and select a policy by id
TUFClientOptions: tufClientOpts,
LocalTargetsDir: filepath.Join(home, ".docker", "policy"), // location to store policy files downloaded from TUF
LocalPolicyDir: "", // overrides TUF policy for local policy files if set
PolicyID: "", // set to ignore policy mapping and select a policy by id
}
src, err := oci.ParseImageSpec(image, oci.WithPlatform(platform))

View File

@@ -8,6 +8,7 @@ import (
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
"github.com/docker/attest/pkg/tuf"
intoto "github.com/in-toto/in-toto-golang/in_toto"
v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
"github.com/stretchr/testify/assert"
@@ -21,11 +22,13 @@ var (
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
InputsPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-inputs")
EmptyPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-policies")
TestTempDir = "attest-sign-test"
)
func TestSignVerifyOCILayout(t *testing.T) {
ctx, signer := test.Setup(t)
ctx = tuf.WithDownloader(ctx, tuf.NewMockTufClient(EmptyPolicyDir, test.CreateTempDir(t, "", "tuf-dest")))
testCases := []struct {
name string

View File

@@ -3,6 +3,8 @@ package attest
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
@@ -11,6 +13,7 @@ import (
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
"github.com/docker/attest/pkg/tuf"
intoto "github.com/in-toto/in-toto-golang/in_toto"
)
@@ -20,13 +23,20 @@ func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.Options) (resu
if err != nil {
return nil, fmt.Errorf("failed to create image details resolver: %w", err)
}
if opts.AttestationStyle == "" {
opts.AttestationStyle = config.AttestationStyleReferrers
err = populateDefaultOptions(opts)
if err != nil {
return nil, err
}
if opts.ReferrersRepo != "" && opts.AttestationStyle != config.AttestationStyleReferrers {
return nil, fmt.Errorf("referrers repo specified but attestation source not set to referrers")
tufClient, ok := tuf.GetDownloader(ctx)
if !ok {
tufClient, err = tuf.NewClient(opts.TUFClientOptions)
if err != nil {
return nil, fmt.Errorf("failed to create TUF client: %w", err)
}
}
pctx, err := policy.ResolvePolicy(ctx, detailsResolver, opts)
pctx, err := policy.ResolvePolicy(ctx, tufClient, detailsResolver, opts)
if err != nil {
return nil, fmt.Errorf("failed to resolve policy: %w", err)
}
@@ -60,6 +70,36 @@ func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.Options) (resu
return result, nil
}
func populateDefaultOptions(opts *policy.Options) (err error) {
if opts.LocalTargetsDir == "" {
opts.LocalTargetsDir, err = defaultLocalTargetsDir()
if err != nil {
return err
}
}
if opts.TUFClientOptions == nil {
opts.TUFClientOptions = tuf.NewDockerDefaultClientOptions(opts.LocalTargetsDir)
}
if opts.AttestationStyle == "" {
opts.AttestationStyle = config.AttestationStyleReferrers
}
if opts.ReferrersRepo != "" && opts.AttestationStyle != config.AttestationStyleReferrers {
return fmt.Errorf("referrers repo specified but attestation source not set to referrers")
}
return nil
}
func defaultLocalTargetsDir() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get user home directory: %w", err)
}
return filepath.Join(homeDir, ".docker", "tuf"), nil
}
func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.Result) (*VerificationResult, error) {
dgst, err := oci.SplitDigest(input.Digest)
if err != nil {

View File

@@ -14,6 +14,7 @@ import (
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
"github.com/docker/attest/pkg/tuf"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -69,6 +70,7 @@ func TestVerifyAttestations(t *testing.T) {
func TestVSA(t *testing.T) {
ctx, signer := test.Setup(t)
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
ctx = tuf.WithDownloader(ctx, tuf.NewMockTufClient(EmptyPolicyDir, test.CreateTempDir(t, "", "tuf-dest")))
// setup an image with signed attestations
outputLayout := test.CreateTempDir(t, "", TestTempDir)
@@ -121,6 +123,7 @@ func TestVSA(t *testing.T) {
func TestVerificationFailure(t *testing.T) {
ctx, signer := test.Setup(t)
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
ctx = tuf.WithDownloader(ctx, tuf.NewMockTufClient(EmptyPolicyDir, test.CreateTempDir(t, "", "tuf-dest")))
// setup an image with signed attestations
outputLayout := test.CreateTempDir(t, "", TestTempDir)
@@ -173,21 +176,23 @@ func TestVerificationFailure(t *testing.T) {
func TestSignVerify(t *testing.T) {
ctx, signer := test.Setup(t)
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
ctx = tuf.WithDownloader(ctx, tuf.NewMockTufClient(EmptyPolicyDir, test.CreateTempDir(t, "", "tuf-dest")))
// setup an image with signed attestations
outputLayout := test.CreateTempDir(t, "", TestTempDir)
testCases := []struct {
name string
signTL bool
policyDir string
imageName string
expectError bool
name string
signTL bool
policyDir string
imageName string
expectedNonSuccess Outcome
}{
{name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir},
{name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir},
{name: "no tl", signTL: false, policyDir: PassPolicyDir},
{name: "mirror", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "mirror.org/library/test-image:test"},
{name: "mirror no match", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectError: true},
{name: "mirror no match", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectedNonSuccess: OutcomeNoPolicy},
{name: "verify inputs", signTL: false, policyDir: InputsPolicyDir},
}
@@ -220,11 +225,11 @@ func TestSignVerify(t *testing.T) {
LocalPolicyDir: tc.policyDir,
}
results, err := Verify(ctx, spec, policyOpts)
if tc.expectError {
require.Error(t, err)
require.NoError(t, err)
if tc.expectedNonSuccess != "" {
assert.Equal(t, tc.expectedNonSuccess, results.Outcome)
return
}
require.NoError(t, err)
assert.Equal(t, OutcomeSuccess, results.Outcome)
platform, err := oci.ParsePlatform(LinuxAMD64)
require.NoError(t, err)
@@ -237,3 +242,67 @@ func TestSignVerify(t *testing.T) {
})
}
}
func TestDefaultOptions(t *testing.T) {
testCases := []struct {
name string
tufOpts *tuf.ClientOptions
localTargetsDir string
attestationStyle config.AttestationStyle
referrersRepo string
expectedError string
}{
{name: "empty"},
{name: "tufClient provided", tufOpts: &tuf.ClientOptions{MetadataSource: "a", TargetsSource: "b"}},
{name: "localTargetsDir provided", localTargetsDir: test.CreateTempDir(t, "", TestTempDir)},
{name: "attestationStyle provided", attestationStyle: config.AttestationStyleAttached},
{name: "referrersRepo provided", referrersRepo: "referrers"},
{name: "referrersRepo provided with attached", referrersRepo: "referrers", attestationStyle: config.AttestationStyleAttached, expectedError: "referrers repo specified but attestation source not set to referrers"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
defaultTargets, err := defaultLocalTargetsDir()
require.NoError(t, err)
opts := &policy.Options{
TUFClientOptions: tc.tufOpts,
LocalTargetsDir: tc.localTargetsDir,
AttestationStyle: tc.attestationStyle,
ReferrersRepo: tc.referrersRepo,
}
err = populateDefaultOptions(opts)
if tc.expectedError != "" {
require.Error(t, err)
assert.Equal(t, tc.expectedError, err.Error())
return
}
require.NoError(t, err)
if tc.localTargetsDir != "" {
assert.Equal(t, tc.localTargetsDir, opts.LocalTargetsDir)
} else {
assert.Equal(t, defaultTargets, opts.LocalTargetsDir)
}
if tc.attestationStyle != "" {
assert.Equal(t, tc.attestationStyle, opts.AttestationStyle)
} else {
assert.Equal(t, config.AttestationStyleReferrers, opts.AttestationStyle)
}
if tc.tufOpts != nil {
assert.Equal(t, tc.tufOpts, opts.TUFClientOptions)
} else {
assert.NotNil(t, opts.TUFClientOptions)
}
if tc.referrersRepo != "" {
assert.Equal(t, tc.referrersRepo, opts.ReferrersRepo)
} else {
assert.Empty(t, opts.ReferrersRepo)
}
})
}
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
"github.com/docker/attest/pkg/tuf"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/registry"
"github.com/google/go-containerregistry/pkg/v1/remote"
@@ -27,12 +28,14 @@ var (
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")
EmptyTUFDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-policies")
TestTempDir = "attest-sign-test"
)
func TestAttestationReferenceTypes(t *testing.T) {
ctx, signer := test.Setup(t)
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
ctx = tuf.WithDownloader(ctx, tuf.NewMockTufClient(EmptyTUFDir, test.CreateTempDir(t, "", "tuf-dest")))
platforms := []string{"linux/amd64", "linux/arm64"}
for _, tc := range []struct {
name string
@@ -182,6 +185,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
func TestReferencesInDifferentRepo(t *testing.T) {
ctx, signer := test.Setup(t)
ctx = tuf.WithDownloader(ctx, tuf.NewMockTufClient(EmptyTUFDir, test.CreateTempDir(t, "", "tuf-dest")))
repoName := "repo"
for _, tc := range []struct {
name string

View File

@@ -6,7 +6,6 @@ import (
"path/filepath"
"strings"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/pkg/mirror"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/tuf"
@@ -30,7 +29,7 @@ func ExampleNewTUFMirror() {
// configure TUF mirror
metadataURI := "https://docker.github.io/tuf-staging/metadata"
targetsURI := "https://docker.github.io/tuf-staging/targets"
m, err := mirror.NewTUFMirror(embed.RootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker())
m, err := mirror.NewTUFMirror(tuf.DockerTUFRootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker())
if err != nil {
panic(err)
}

View File

@@ -9,7 +9,6 @@ import (
"strings"
"testing"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/tuf"
"github.com/stretchr/testify/assert"
@@ -26,7 +25,7 @@ func TestGetTufMetadataMirror(t *testing.T) {
defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
assert.NoError(t, err)
tufMetadata, err := m.getMetadataMirror(server.URL + metadataPath)
@@ -44,7 +43,7 @@ func TestGetMetadataManifest(t *testing.T) {
defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
assert.NoError(t, err)
img, err := m.GetMetadataManifest(server.URL + metadataPath)
@@ -83,7 +82,7 @@ func TestGetDelegatedMetadataMirrors(t *testing.T) {
defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
assert.NoError(t, err)
delegations, err := m.GetDelegatedMetadataMirrors()

View File

@@ -3,15 +3,14 @@ package mirror
import (
"fmt"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/pkg/tuf"
)
func NewTUFMirror(root []byte, tufPath, metadataURL, targetsURL string, versionChecker tuf.VersionChecker) (*TUFMirror, error) {
if root == nil {
root = embed.RootDefault.Data
root = tuf.DockerTUFRootDefault.Data
}
tufClient, err := tuf.NewClient(root, tufPath, metadataURL, targetsURL, versionChecker)
tufClient, err := tuf.NewClient(&tuf.ClientOptions{InitialRoot: root, Path: tufPath, MetadataSource: metadataURL, TargetsSource: targetsURL, VersionChecker: versionChecker})
if err != nil {
return nil, fmt.Errorf("failed to create TUF client: %w", err)
}

View File

@@ -8,7 +8,6 @@ import (
"strings"
"testing"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/tuf"
"github.com/stretchr/testify/assert"
@@ -27,7 +26,7 @@ func TestGetTufTargetsMirror(t *testing.T) {
defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
assert.NoError(t, err)
targets, err := m.GetTUFTargetMirrors()
@@ -61,7 +60,7 @@ func TestTargetDelegationMetadata(t *testing.T) {
defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp")
tm, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
tm, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
assert.NoError(t, err)
targets, err := tm.TUFClient.LoadDelegatedTargets("test-role", "targets")
@@ -74,7 +73,7 @@ func TestGetDelegatedTargetMirrors(t *testing.T) {
defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
assert.NoError(t, err)
mirrors, err := m.GetDelegatedTargetMirrors()

View File

@@ -12,6 +12,7 @@ import (
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/tuf"
)
func resolveLocalPolicy(opts *Options, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
@@ -57,13 +58,13 @@ func resolveLocalPolicy(opts *Options, mapping *config.PolicyMapping, imageName
return policy, nil
}
func resolveTUFPolicy(opts *Options, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
func resolveTUFPolicy(opts *Options, tufClient tuf.Downloader, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
var URI string
var digest map[string]string
files := make([]*File, 0, len(mapping.Files))
for _, f := range mapping.Files {
filename := f.Path
file, err := opts.TUFClient.DownloadTarget(filename, filepath.Join(opts.LocalTargetsDir, filename))
file, err := tufClient.DownloadTarget(filename, filepath.Join(opts.LocalTargetsDir, filename))
if err != nil {
return nil, fmt.Errorf("failed to download policy file %s: %w", filename, err)
}
@@ -154,7 +155,7 @@ func findPolicyMatchImpl(imageName string, mappings *config.PolicyMappings, matc
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
}
func resolvePolicyByID(opts *Options) (*Policy, error) {
func resolvePolicyByID(opts *Options, tufClient tuf.Downloader) (*Policy, error) {
if opts.PolicyID != "" {
localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir)
if err != nil {
@@ -168,21 +169,21 @@ func resolvePolicyByID(opts *Options) (*Policy, error) {
}
// must check tuf
tufMappings, err := config.LoadTUFMappings(opts.TUFClient, opts.LocalTargetsDir)
tufMappings, err := config.LoadTUFMappings(tufClient, opts.LocalTargetsDir)
if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err)
}
policy := tufMappings.Policies[opts.PolicyID]
if policy != nil {
return resolveTUFPolicy(opts, policy, "", "")
return resolveTUFPolicy(opts, tufClient, policy, "", "")
}
return nil, fmt.Errorf("policy with id %s not found", opts.PolicyID)
}
return nil, nil
}
func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver, opts *Options) (*Policy, error) {
p, err := resolvePolicyByID(opts)
func ResolvePolicy(ctx context.Context, tufClient tuf.Downloader, detailsResolver oci.ImageDetailsResolver, opts *Options) (*Policy, error) {
p, err := resolvePolicyByID(opts, tufClient)
if err != nil {
return nil, fmt.Errorf("failed to resolve policy by id: %w", err)
}
@@ -209,7 +210,7 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver
return resolveLocalPolicy(opts, match.policy, imageName, match.matchedName)
}
// must check tuf
tufMappings, err := config.LoadTUFMappings(opts.TUFClient, opts.LocalTargetsDir)
tufMappings, err := config.LoadTUFMappings(tufClient, opts.LocalTargetsDir)
if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err)
}
@@ -218,7 +219,7 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver
if match.matchType == matchTypeMatchNoPolicy {
for _, mapping := range tufMappings.Policies {
if mapping.ID == match.rule.PolicyID {
return resolveTUFPolicy(opts, mapping, imageName, match.matchedName)
return resolveTUFPolicy(opts, tufClient, mapping, imageName, match.matchedName)
}
}
}
@@ -229,7 +230,7 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver
return nil, err
}
if match.matchType == matchTypePolicy {
return resolveTUFPolicy(opts, match.policy, imageName, match.matchedName)
return resolveTUFPolicy(opts, tufClient, match.policy, imageName, match.matchedName)
}
return nil, nil
}

View File

@@ -75,7 +75,6 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
tufClient := tuf.NewMockTufClient(tc.repo, test.CreateTempDir(t, "", "tuf-dest"))
if tc.policy == nil {
tc.policy = &policy.Options{
TUFClient: tufClient,
LocalTargetsDir: test.CreateTempDir(t, "", "tuf-targets"),
PolicyID: tc.policyID,
}
@@ -88,7 +87,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
require.NoError(t, err)
resolver, err := policy.CreateImageDetailsResolver(src)
require.NoError(t, err)
policy, err := policy.ResolvePolicy(ctx, resolver, tc.policy)
policy, err := policy.ResolvePolicy(ctx, tufClient, resolver, tc.policy)
if tc.resolveErrorStr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.resolveErrorStr)

View File

@@ -27,7 +27,7 @@ type Result struct {
}
type Options struct {
TUFClient tuf.Downloader
TUFClientOptions *tuf.ClientOptions
LocalTargetsDir string
LocalPolicyDir string
PolicyID string

View File

@@ -4,7 +4,6 @@ import (
"os"
"path/filepath"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/pkg/tuf"
"github.com/theupdateframework/go-tuf/v2/metadata"
)
@@ -21,7 +20,7 @@ func ExampleNewClient_registry() {
metadataURI := "registry-1.docker.io/docker/tuf-metadata:latest"
targetsURI := "registry-1.docker.io/docker/tuf-targets"
registryClient, err := tuf.NewClient(embed.RootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker())
registryClient, err := tuf.NewClient(&tuf.ClientOptions{tuf.DockerTUFRootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker()})
if err != nil {
panic(err)
}

View File

@@ -9,7 +9,6 @@ import (
"strings"
"testing"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/internal/util"
"github.com/docker/attest/pkg/oci"
"github.com/google/go-containerregistry/pkg/crane"
@@ -56,7 +55,7 @@ func TestRegistryFetcher(t *testing.T) {
delegatedDir := CreateTempDir(t, dir, delegatedRole)
delegatedTargetFile := fmt.Sprintf("%s/%s", delegatedRole, targetFile)
cfg, err := config.New(metadataRepo, embed.RootDev.Data)
cfg, err := config.New(metadataRepo, DockerTUFRootDev.Data)
assert.NoError(t, err)
cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataImgTag, targetsRepo)

View File

@@ -1,6 +1,7 @@
package tuf
import (
"context"
"errors"
"fmt"
"io/fs"
@@ -20,6 +21,24 @@ import (
"github.com/theupdateframework/go-tuf/v2/metadata/updater"
)
type tufCtxKeyType struct{}
var DownloaderCtxKey tufCtxKeyType
// WithDownloader sets Downloader in context.
func WithDownloader(ctx context.Context, downloader Downloader) context.Context {
return context.WithValue(ctx, DownloaderCtxKey, downloader)
}
// GetDownloader returns the Downloader from context and `true` if it exists, otherwise `nil` and `false`.
func GetDownloader(ctx context.Context) (Downloader, bool) {
t, ok := ctx.Value(DownloaderCtxKey).(Downloader)
if !ok {
return nil, false
}
return t, true
}
type Source string
const (
@@ -35,6 +54,11 @@ var (
DockerTUFRootDefault = embed.RootDefault
)
const (
defaultMetadataSource = "docker/tuf-metadata:latest"
defaultTargetsSource = "docker/tuf-targets"
)
type Downloader interface {
DownloadTarget(target, filePath string) (file *TargetFile, err error)
}
@@ -51,19 +75,37 @@ type TargetFile struct {
Data []byte
}
type ClientOptions struct {
InitialRoot []byte
Path string
MetadataSource string
TargetsSource string
VersionChecker VersionChecker
}
func NewDockerDefaultClientOptions(tufPath string) *ClientOptions {
return &ClientOptions{
InitialRoot: DockerTUFRootDefault.Data,
Path: tufPath,
MetadataSource: defaultMetadataSource,
TargetsSource: defaultTargetsSource,
VersionChecker: NewDefaultVersionChecker(),
}
}
// NewClient creates a new TUF client.
func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string, versionChecker VersionChecker) (*Client, error) {
func NewClient(opts *ClientOptions) (*Client, error) {
var tufSource Source
if strings.HasPrefix(metadataSource, "https://") || strings.HasPrefix(metadataSource, "http://") {
if strings.HasPrefix(opts.MetadataSource, "https://") || strings.HasPrefix(opts.MetadataSource, "http://") {
tufSource = HTTPSource
} else {
tufSource = OCISource
}
tufRootDigest := util.SHA256Hex(initialRoot)
tufRootDigest := util.SHA256Hex(opts.InitialRoot)
// create a directory for each initial root.json
metadataPath := filepath.Join(tufPath, tufRootDigest)
metadataPath := filepath.Join(opts.Path, tufRootDigest)
err := os.MkdirAll(metadataPath, os.ModePerm)
if err != nil {
return nil, fmt.Errorf("failed to create directory '%s': %w", metadataPath, err)
@@ -76,29 +118,29 @@ func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string
return nil, fmt.Errorf("failed to read root.json: %w", err)
}
// write the root.json file to the metadata directory
err = os.WriteFile(rootFile, initialRoot, 0o666) // #nosec G306
err = os.WriteFile(rootFile, opts.InitialRoot, 0o666) // #nosec G306
if err != nil {
return nil, fmt.Errorf("Failed to write root.json %w", err)
}
rootBytes = initialRoot
rootBytes = opts.InitialRoot
}
// create updater configuration
cfg, err := config.New(metadataSource, rootBytes) // default config
cfg, err := config.New(opts.MetadataSource, rootBytes) // default config
if err != nil {
return nil, fmt.Errorf("failed to create TUF updater configuration: %w", err)
}
cfg.LocalMetadataDir = metadataPath
cfg.LocalTargetsDir = filepath.Join(metadataPath, "download")
cfg.RemoteTargetsURL = targetsSource
cfg.RemoteTargetsURL = opts.TargetsSource
if tufSource == OCISource {
metadataRepo, metadataTag, found := strings.Cut(metadataSource, ":")
metadataRepo, metadataTag, found := strings.Cut(opts.MetadataSource, ":")
if !found {
fmt.Printf("metadata tag not found in URL, using latest\n")
metadataTag = LatestTag
}
cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataTag, targetsSource)
cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataTag, opts.TargetsSource)
}
// create a new Updater instance
@@ -118,7 +160,7 @@ func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string
cfg: cfg,
}
err = versionChecker.CheckVersion(client)
err = opts.VersionChecker.CheckVersion(client)
if err != nil {
return nil, err
}

View File

@@ -9,8 +9,8 @@ import (
"path/filepath"
"testing"
"github.com/docker/attest/internal/embed"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/theupdateframework/go-tuf/v2/metadata"
)
@@ -65,18 +65,18 @@ func TestRootInit(t *testing.T) {
}
for _, tc := range testCases {
_, err := NewClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker)
_, err := NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
assert.NoErrorf(t, err, "Failed to create TUF client: %v", err)
// recreation should work with same root
_, err = NewClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker)
_, err = NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
assert.NoErrorf(t, err, "Failed to recreate TUF client: %v", err)
_, err = NewClient([]byte("broken"), tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker)
_, err = NewClient(&ClientOptions{[]byte("broken"), tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
assert.Errorf(t, err, "Expected error recreating TUF client with broken root: %v", err)
_, err = NewClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysBadVersionChecker)
assert.Errorf(t, err, "Expected error creating TUF client with bad attest version: %v", err)
_, err = NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysBadVersionChecker})
assert.Errorf(t, err, "Expected error recreating TUF client with bad version checker")
}
}
@@ -108,11 +108,13 @@ func TestDownloadTarget(t *testing.T) {
}{
{"http", server.URL + "/metadata", server.URL + "/targets"},
{"oci", regAddr.Host + "/tuf-metadata:latest", regAddr.Host + "/tuf-targets"},
{"http, download before init", server.URL + "/metadata", server.URL + "/targets"},
}
for _, tc := range testCases {
tufClient, err := NewClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker)
assert.NoErrorf(t, err, "Failed to create TUF client: %v", err)
tufClient, err := NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
require.NoErrorf(t, err, "Failed to create TUF client: %v", err)
require.NotNil(t, tufClient.updater, "Failed to create updater")
// get trusted tuf metadata
trustedMetadata := tufClient.updater.GetTrustedMetadataSet()

View File

@@ -0,0 +1,4 @@
version: v1
kind: policy-mapping
policies:
rules: