From aed959f8581b413f61b2429e6dd16455e9830a6a Mon Sep 17 00:00:00 2001 From: Jonny Stoten Date: Fri, 23 Aug 2024 09:33:30 +0100 Subject: [PATCH] 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 --- .github/workflows/test.yml | 2 +- pkg/attest/example_verify_test.go | 25 ++---- pkg/attest/sign_test.go | 3 + pkg/attest/verify.go | 50 +++++++++-- pkg/attest/verify_test.go | 87 +++++++++++++++++-- pkg/attestation/referrers_test.go | 4 + pkg/mirror/example_mirror_test.go | 3 +- pkg/mirror/metadata_test.go | 7 +- pkg/mirror/mirror.go | 5 +- pkg/mirror/targets_test.go | 7 +- pkg/policy/policy.go | 21 ++--- pkg/policy/policy_test.go | 3 +- pkg/policy/types.go | 2 +- pkg/tuf/example_registry_test.go | 3 +- pkg/tuf/registry_test.go | 3 +- pkg/tuf/tuf.go | 64 +++++++++++--- pkg/tuf/tuf_test.go | 18 ++-- .../local-policy-no-policies/mapping.yaml | 4 + 18 files changed, 227 insertions(+), 84 deletions(-) create mode 100644 test/testdata/local-policy-no-policies/mapping.yaml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 88bdbf8..4679128 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 ./... diff --git a/pkg/attest/example_verify_test.go b/pkg/attest/example_verify_test.go index 25e30fd..c22a913 100644 --- a/pkg/attest/example_verify_test.go +++ b/pkg/attest/example_verify_test.go @@ -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)) diff --git a/pkg/attest/sign_test.go b/pkg/attest/sign_test.go index 27b821a..f3b7663 100644 --- a/pkg/attest/sign_test.go +++ b/pkg/attest/sign_test.go @@ -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 diff --git a/pkg/attest/verify.go b/pkg/attest/verify.go index 16571b2..f991dab 100644 --- a/pkg/attest/verify.go +++ b/pkg/attest/verify.go @@ -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 { diff --git a/pkg/attest/verify_test.go b/pkg/attest/verify_test.go index 70e919f..ba45dd8 100644 --- a/pkg/attest/verify_test.go +++ b/pkg/attest/verify_test.go @@ -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) + } + }) + } +} diff --git a/pkg/attestation/referrers_test.go b/pkg/attestation/referrers_test.go index 9d7d272..3b2b5d6 100644 --- a/pkg/attestation/referrers_test.go +++ b/pkg/attestation/referrers_test.go @@ -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 diff --git a/pkg/mirror/example_mirror_test.go b/pkg/mirror/example_mirror_test.go index 37003a2..c4c4b15 100644 --- a/pkg/mirror/example_mirror_test.go +++ b/pkg/mirror/example_mirror_test.go @@ -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) } diff --git a/pkg/mirror/metadata_test.go b/pkg/mirror/metadata_test.go index ee5417e..861d783 100644 --- a/pkg/mirror/metadata_test.go +++ b/pkg/mirror/metadata_test.go @@ -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() diff --git a/pkg/mirror/mirror.go b/pkg/mirror/mirror.go index bf376bf..55c8631 100644 --- a/pkg/mirror/mirror.go +++ b/pkg/mirror/mirror.go @@ -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) } diff --git a/pkg/mirror/targets_test.go b/pkg/mirror/targets_test.go index c2b3ec5..b229e42 100644 --- a/pkg/mirror/targets_test.go +++ b/pkg/mirror/targets_test.go @@ -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() diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index 6a82d15..afeb6b6 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -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 } diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index ac50122..994c00f 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -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) diff --git a/pkg/policy/types.go b/pkg/policy/types.go index e62cf0c..0840124 100644 --- a/pkg/policy/types.go +++ b/pkg/policy/types.go @@ -27,7 +27,7 @@ type Result struct { } type Options struct { - TUFClient tuf.Downloader + TUFClientOptions *tuf.ClientOptions LocalTargetsDir string LocalPolicyDir string PolicyID string diff --git a/pkg/tuf/example_registry_test.go b/pkg/tuf/example_registry_test.go index b6e4449..e7efe00 100644 --- a/pkg/tuf/example_registry_test.go +++ b/pkg/tuf/example_registry_test.go @@ -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) } diff --git a/pkg/tuf/registry_test.go b/pkg/tuf/registry_test.go index 45ead27..d648470 100644 --- a/pkg/tuf/registry_test.go +++ b/pkg/tuf/registry_test.go @@ -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) diff --git a/pkg/tuf/tuf.go b/pkg/tuf/tuf.go index cfcc913..f358ff3 100644 --- a/pkg/tuf/tuf.go +++ b/pkg/tuf/tuf.go @@ -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 } diff --git a/pkg/tuf/tuf_test.go b/pkg/tuf/tuf_test.go index 934f4dc..6eaf2a1 100644 --- a/pkg/tuf/tuf_test.go +++ b/pkg/tuf/tuf_test.go @@ -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() diff --git a/test/testdata/local-policy-no-policies/mapping.yaml b/test/testdata/local-policy-no-policies/mapping.yaml new file mode 100644 index 0000000..39405b7 --- /dev/null +++ b/test/testdata/local-policy-no-policies/mapping.yaml @@ -0,0 +1,4 @@ +version: v1 +kind: policy-mapping +policies: +rules: