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: