Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: use canonical names inside TUF fetcher #144

Merged
merged 4 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions pkg/tuf/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"time"

"github.com/distribution/reference"
"github.com/docker/attest/pkg/oci"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/crane"
Expand Down Expand Up @@ -67,13 +68,29 @@ type Layers struct {
MediaType string `json:"mediaType"`
}

func NewRegistryFetcher(metadataRepo, metadataTag, targetsRepo string) *RegistryFetcher {
func NewRegistryFetcher(metadataRepo, targetsRepo string) (*RegistryFetcher, error) {
ref, err := reference.ParseNormalizedNamed(metadataRepo)
if err != nil {
return nil, fmt.Errorf("failed to parse metadata repo: %w", err)
}
// add latest tag
metadataTag := LatestTag
if tag, ok := ref.(reference.Tagged); ok {
metadataTag = tag.Tag()
}
metadataRepo = ref.Name()

ref, err = reference.ParseNormalizedNamed(targetsRepo)
kipz marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, fmt.Errorf("failed to parse targets repo: %w", err)
}
targetsRepo = ref.Name()
return &RegistryFetcher{
metadataRepo: metadataRepo,
metadataTag: metadataTag,
targetsRepo: targetsRepo,
cache: NewImageCache(),
}
}, nil
}

// DownloadFile downloads a file from an OCI registry, errors out if it failed,
Expand Down Expand Up @@ -188,7 +205,7 @@ func getDataFromLayer(fileLayer v1.Layer, maxLength int64) ([]byte, error) {
// parseImgRef maintains the Fetcher interface by parsing a URL path to an image reference and file name.
func (d *RegistryFetcher) parseImgRef(urlPath string) (imgRef, fileName string, err error) {
// Check if repo is target or metadata
if strings.Contains(urlPath, d.targetsRepo) {
if strings.HasPrefix(urlPath, d.targetsRepo) {
// determine if the target path contains subdirectories and set image name accordingly
// <repo>/<filename> -> image = <repo>:<filename>, layer = <filename>
// <repo>/<subdir>/<filename> -> index = <repo>:<subdir> , image = <filename> -> layer = <filename>
Expand All @@ -198,7 +215,7 @@ func (d *RegistryFetcher) parseImgRef(urlPath string) (imgRef, fileName string,
return fmt.Sprintf("%s:%s", d.targetsRepo, subdir), fmt.Sprintf("%s/%s", subdir, name), nil
}
return fmt.Sprintf("%s:%s", d.targetsRepo, target), target, nil
} else if strings.Contains(urlPath, d.metadataRepo) {
} else if strings.HasPrefix(urlPath, d.metadataRepo) {
// build the metadata image name
// determine if role is a delegated role and set the tag accordingly
fileName = path.Base(urlPath)
Expand Down
66 changes: 46 additions & 20 deletions pkg/tuf/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const (
tufMetadataRepo = "tuf-metadata"
targetsPath = "/tuf-targets"
metadataPath = "/tuf-metadata"
targetsRepo = "test" + targetsPath
targetsRepo = "docker.io/test" + targetsPath
)

func TestRegistryFetcher(t *testing.T) {
Expand All @@ -48,7 +48,6 @@ func TestRegistryFetcher(t *testing.T) {
LoadRegistryTestData(t, regAddr, OCITUFTestDataPath)

metadataRepo := regAddr.Host + metadataPath
metadataImgTag := LatestTag
targetsRepo := regAddr.Host + targetsPath
targetFile := "test.txt"
delegatedRole := testRole
Expand All @@ -60,7 +59,8 @@ func TestRegistryFetcher(t *testing.T) {
cfg, err := config.New("", DockerTUFRootDev.Data)
require.NoError(t, err)

cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataImgTag, targetsRepo)
cfg.Fetcher, err = NewRegistryFetcher(metadataRepo, targetsRepo)
require.NoError(t, err)
cfg.LocalMetadataDir = dir
cfg.LocalTargetsDir = dir
cfg.RemoteTargetsURL = targetsRepo
Expand Down Expand Up @@ -187,31 +187,57 @@ func TestFindFileInManifest(t *testing.T) {
}

func TestParseImgRef(t *testing.T) {
metadataRepo := "test" + metadataPath
metadataRepo := "docker.io/test" + metadataPath
metadataTag := LatestTag
delegatedRole := testRole
validRef := fmt.Sprintf("%s/2.root.json", metadataRepo)
expectedRef := fmt.Sprintf("%s:%s", metadataRepo, metadataTag)
testCases := []struct {
name string
ref string
expectedRef string
expectedFile string
name string
ref string
expectedRef string
expectedFile string
metadataRepo string
metadataTag string
expectedRefError string
expectedConstructorError string
targetsRepo string
}{
{"top-level metadata", fmt.Sprintf("%s/2.root.json", metadataRepo), fmt.Sprintf("%s:%s", metadataRepo, metadataTag), "2.root.json"},
{"delegated metadata", fmt.Sprintf("%s/%s/5.test-role.json", metadataRepo, delegatedRole), fmt.Sprintf("%s:%s", metadataRepo, delegatedRole), "5.test-role.json"},
{"top-level target", fmt.Sprintf("%s/policy.yaml", targetsRepo), fmt.Sprintf("%s:policy.yaml", targetsRepo), "policy.yaml"},
{"delegated target", fmt.Sprintf("%s/%s/policy.yaml", targetsRepo, delegatedRole), fmt.Sprintf("%s:%s", targetsRepo, delegatedRole), fmt.Sprintf("%s/policy.yaml", delegatedRole)},
{name: "top-level metadata", ref: validRef, expectedRef: expectedRef, expectedFile: "2.root.json"},
{name: "short metdata repo", ref: validRef, metadataRepo: "test" + metadataPath, expectedRef: expectedRef, expectedFile: "2.root.json"},
{name: "short targetsRepo repo", ref: validRef, targetsRepo: "test" + targetsPath, expectedRef: expectedRef, expectedFile: "2.root.json"},
{name: "delegated metadata", ref: fmt.Sprintf("%s/%s/5.test-role.json", metadataRepo, delegatedRole), expectedRef: fmt.Sprintf("%s:%s", metadataRepo, delegatedRole), expectedFile: "5.test-role.json"},
{name: "top-level target", ref: fmt.Sprintf("%s/policy.yaml", targetsRepo), expectedRef: fmt.Sprintf("%s:policy.yaml", targetsRepo), expectedFile: "policy.yaml"},
{name: "delegated target", ref: fmt.Sprintf("%s/%s/policy.yaml", targetsRepo, delegatedRole), expectedRef: fmt.Sprintf("%s:%s", targetsRepo, delegatedRole), expectedFile: fmt.Sprintf("%s/policy.yaml", delegatedRole)},
{name: "docker/targets", ref: fmt.Sprintf("%s/2.root.json", "docker.io/docker/targets"), expectedRef: "docker.io/docker/targets:latest", expectedFile: "2.root.json", metadataRepo: "docker.io/docker/targets"},
{name: "malformed ref", ref: fmt.Sprintf("%s/2.root.json", "@broken"), expectedRefError: "urlPath: @broken/2.root.json must be in metadata or targets repo"},
{name: "malformed metadataRepo", ref: validRef, metadataRepo: "@broken", expectedConstructorError: "failed to parse metadata repo: invalid reference format"},
{name: "malformed targetsRepo", ref: validRef, targetsRepo: "@broken", expectedConstructorError: "failed to parse targets repo: invalid reference format"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
d := &RegistryFetcher{
metadataRepo: metadataRepo,
metadataTag: LatestTag,
targetsRepo: targetsRepo,
repo := metadataRepo
if tc.metadataRepo != "" {
repo = tc.metadataRepo
}
targets := targetsRepo
if tc.targetsRepo != "" {
targets = tc.targetsRepo
}
d, err := NewRegistryFetcher(repo, targets)
if tc.expectedConstructorError != "" {
assert.ErrorContains(t, err, tc.expectedConstructorError)
} else {
require.NoError(t, err)
imgRef, file, err := d.parseImgRef(tc.ref)
if tc.expectedRefError != "" {
assert.ErrorContains(t, err, tc.expectedRefError)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expectedRef, imgRef, "ref mismatch")
assert.Equal(t, tc.expectedFile, file, "file mismatch")
}
}
imgRef, file, err := d.parseImgRef(tc.ref)
assert.NoError(t, err)
assert.Equal(t, tc.expectedRef, imgRef)
assert.Equal(t, tc.expectedFile, file)
})
}
}
Expand Down
12 changes: 2 additions & 10 deletions pkg/tuf/tuf.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"strings"
"time"

"github.com/distribution/reference"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/internal/util"
"github.com/theupdateframework/go-tuf/v2/metadata"
Expand Down Expand Up @@ -120,17 +119,10 @@ func NewClient(opts *ClientOptions) (*Client, error) {
cfg.RemoteTargetsURL = opts.TargetsSource

if tufSource == OCISource {
ref, err := reference.ParseNormalizedNamed(opts.MetadataSource)
cfg.Fetcher, err = NewRegistryFetcher(opts.MetadataSource, opts.TargetsSource)
if err != nil {
return nil, fmt.Errorf("failed to parse metadata source: %w", err)
return nil, fmt.Errorf("failed to create registry fetcher: %w", err)
}
// add latest tag
metadataTag := LatestTag
if tag, ok := ref.(reference.Tagged); ok {
metadataTag = tag.Tag()
}
metadataRepo := ref.Name()
cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataTag, opts.TargetsSource)
}

// create a new Updater instance
Expand Down
44 changes: 23 additions & 21 deletions pkg/tuf/tuf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,27 +112,29 @@ func TestDownloadTarget(t *testing.T) {
}

for _, tc := range testCases {
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()
assert.NotNil(t, trustedMetadata, "Failed to get trusted metadata")

// download top-level target files
targets := trustedMetadata.Targets[metadata.TARGETS].Signed.Targets
for _, target := range targets {
// download target files
_, err := tufClient.DownloadTarget(target.Path, filepath.Join(tufPath, "download"))
assert.NoErrorf(t, err, "Failed to download target: %v", err)
}

// download delegated target
targetInfo, err := tufClient.updater.GetTargetInfo(delegatedTargetFile)
require.NoError(t, err)
_, err = tufClient.DownloadTarget(targetInfo.Path, filepath.Join(tufPath, targetInfo.Path))
assert.NoError(t, err)
t.Run(tc.name, func(t *testing.T) {
kipz marked this conversation as resolved.
Show resolved Hide resolved
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()
assert.NotNil(t, trustedMetadata, "Failed to get trusted metadata")

// download top-level target files
targets := trustedMetadata.Targets[metadata.TARGETS].Signed.Targets
for _, target := range targets {
// download target files
_, err := tufClient.DownloadTarget(target.Path, filepath.Join(tufPath, "download"))
assert.NoErrorf(t, err, "Failed to download target: %v", err)
}

// download delegated target
targetInfo, err := tufClient.updater.GetTargetInfo(delegatedTargetFile)
require.NoError(t, err)
_, err = tufClient.DownloadTarget(targetInfo.Path, filepath.Join(tufPath, targetInfo.Path))
assert.NoError(t, err)
})
}
}

Expand Down
Loading