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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user