2024-10-17 13:40:17 -05:00
/ *
2024-10-18 09:25:31 -05:00
Copyright Docker attest authors
2024-10-17 13:40:17 -05:00
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
2024-10-18 09:25:31 -05:00
2024-04-15 15:20:56 -05:00
package tuf
import (
"context"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
2024-09-09 14:22:17 +01:00
"github.com/docker/attest/internal/test"
2024-04-15 15:20:56 -05:00
"github.com/docker/attest/internal/util"
2024-09-02 16:17:50 +01:00
"github.com/docker/attest/oci"
2024-09-09 18:08:01 +01:00
"github.com/docker/attest/useragent"
2024-04-15 15:20:56 -05:00
"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/static"
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/stretchr/testify/assert"
2024-08-29 17:43:45 +01:00
"github.com/stretchr/testify/require"
2024-04-15 15:20:56 -05:00
"github.com/theupdateframework/go-tuf/v2/metadata"
"github.com/theupdateframework/go-tuf/v2/metadata/config"
"github.com/theupdateframework/go-tuf/v2/metadata/updater"
)
const (
tufTargetMediaType = "application/vnd.tuf.target"
2024-08-01 15:35:15 +01:00
testRole = "test-role"
tufMetadataRepo = "tuf-metadata"
2024-08-13 10:03:33 -05:00
targetsPath = "/tuf-targets"
metadataPath = "/tuf-metadata"
targetsRepo = "test" + targetsPath
2024-04-15 15:20:56 -05:00
)
func TestRegistryFetcher ( t * testing . T ) {
2024-09-09 14:22:17 +01:00
ctx := context . Background ( )
regServer := test . NewLocalRegistry ( ctx )
defer regServer . Close ( )
regAddr , err := url . Parse ( regServer . URL )
require . NoError ( t , err )
LoadRegistryTestData ( ctx , t , regAddr , OCITUFTestDataPath )
2024-04-15 15:20:56 -05:00
2024-08-13 10:03:33 -05:00
metadataRepo := regAddr . Host + metadataPath
targetsRepo := regAddr . Host + targetsPath
2024-04-15 15:20:56 -05:00
targetFile := "test.txt"
2024-08-01 15:35:15 +01:00
delegatedRole := testRole
2024-04-22 12:22:15 -05:00
dir := CreateTempDir ( t , "" , "tuf_temp" )
delegatedDir := CreateTempDir ( t , dir , delegatedRole )
2024-04-15 15:20:56 -05:00
delegatedTargetFile := fmt . Sprintf ( "%s/%s" , delegatedRole , targetFile )
2024-08-29 18:27:13 +01:00
// note - url is ignored here - needed to make http url parsing happy even when using oci
cfg , err := config . New ( "" , DockerTUFRootDev . Data )
2024-08-29 17:43:45 +01:00
require . NoError ( t , err )
2024-04-15 15:20:56 -05:00
cfg . LocalMetadataDir = dir
cfg . LocalTargetsDir = dir
cfg . RemoteTargetsURL = targetsRepo
2024-08-29 18:27:13 +01:00
cfg . RemoteMetadataURL = metadataRepo
2024-09-09 14:22:17 +01:00
cfg . Fetcher , err = NewRegistryFetcher ( context . Background ( ) , cfg )
2024-08-30 17:03:29 +01:00
require . NoError ( t , err )
2024-04-15 15:20:56 -05:00
// create a new Updater instance
up , err := updater . New ( cfg )
2024-08-29 17:43:45 +01:00
require . NoError ( t , err )
2024-04-15 15:20:56 -05:00
// refresh the metadata
err = up . Refresh ( )
2024-08-29 17:43:45 +01:00
require . NoError ( t , err )
2024-04-15 15:20:56 -05:00
// download top-level target
targetInfo , err := up . GetTargetInfo ( targetFile )
2024-08-29 17:43:45 +01:00
require . NoError ( t , err )
2024-04-15 15:20:56 -05:00
_ , _ , err = up . DownloadTarget ( targetInfo , filepath . Join ( dir , targetInfo . Path ) , "" )
2024-08-29 17:43:45 +01:00
require . NoError ( t , err )
2024-04-15 15:20:56 -05:00
// download delegated target
targetInfo , err = up . GetTargetInfo ( delegatedTargetFile )
2024-08-29 17:43:45 +01:00
require . NoError ( t , err )
2024-04-15 15:20:56 -05:00
_ , _ , err = up . DownloadTarget ( targetInfo , filepath . Join ( delegatedDir , targetFile ) , "" )
2024-08-29 17:43:45 +01:00
require . NoError ( t , err )
2024-04-15 15:20:56 -05:00
}
func TestRoleFromConsistentName ( t * testing . T ) {
testCases := [ ] struct {
name string
expected string
} {
{ "root.json" , metadata . ROOT } ,
{ "1.root.json" , metadata . ROOT } ,
{ "targets.json" , metadata . TARGETS } ,
{ "63.targets.json" , metadata . TARGETS } ,
{ "timestamp" , metadata . TIMESTAMP } ,
}
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
assert . Equal ( t , tc . expected , roleFromConsistentName ( tc . name ) )
} )
}
}
func TestIsDelegatedRole ( t * testing . T ) {
testCases := [ ] struct {
name string
expected bool
} {
{ metadata . ROOT , false } ,
{ metadata . TARGETS , false } ,
{ metadata . TIMESTAMP , false } ,
{ "doi" , true } ,
{ "test" , true } ,
}
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
assert . Equal ( t , tc . expected , isDelegatedRole ( tc . name ) )
} )
}
}
func TestFindFileInManifest ( t * testing . T ) {
// make test image manifest
file := "test.json"
data := [ ] byte ( "test" )
2024-08-13 10:03:33 -05:00
hash := v1 . Hash { Hex : util . SHA256Hex ( data ) }
2024-04-15 15:20:56 -05:00
img := empty . Image
img = mutate . MediaType ( img , types . OCIManifestSchema1 )
img = mutate . ConfigMediaType ( img , types . OCIConfigJSON )
// add test layer
name := strings . Join ( [ ] string { hash . Hex , file } , "." )
2024-08-01 15:35:15 +01:00
ann := map [ string ] string { TUFFileNameAnnotation : name }
2024-04-15 15:20:56 -05:00
layer := mutate . Addendum { Layer : static . NewLayer ( data , tufTargetMediaType ) , Annotations : ann }
img , err := mutate . Append ( img , layer )
assert . NoError ( t , err )
2024-08-01 15:35:15 +01:00
imageManifest , err := img . RawManifest ( )
2024-04-15 15:20:56 -05:00
assert . NoError ( t , err )
// make test index manifest
idx := v1 . ImageIndex ( empty . Index )
assert . NoError ( t , err )
// append image to index with annotation
idx = mutate . AppendManifests ( idx , mutate . IndexAddendum {
Add : img ,
Descriptor : v1 . Descriptor {
Annotations : map [ string ] string {
2024-08-01 15:35:15 +01:00
TUFFileNameAnnotation : name ,
2024-04-15 15:20:56 -05:00
} ,
} ,
} )
2024-08-01 15:35:15 +01:00
indexManifest , err := idx . RawManifest ( )
2024-04-15 15:20:56 -05:00
assert . NoError ( t , err )
// cache image layer
d := & RegistryFetcher {
cache : NewImageCache ( ) ,
targetsRepo : targetsRepo ,
}
imgHash , err := img . Digest ( )
assert . NoError ( t , err )
2024-08-01 15:35:15 +01:00
d . cache . Put ( fmt . Sprintf ( "%s@%s" , targetsRepo , imgHash . String ( ) ) , imageManifest )
2024-04-15 15:20:56 -05:00
testCases := [ ] struct {
name string
manifest [ ] byte
file string
expected string
} {
2024-08-01 15:35:15 +01:00
{ "consistent filename image" , imageManifest , fmt . Sprintf ( "%s.%s" , hash . Hex , file ) , hash . Hex } ,
{ "filename image" , imageManifest , file , "" } ,
{ "consistent filename index" , indexManifest , fmt . Sprintf ( "%s.%s" , hash . Hex , file ) , hash . Hex } ,
2024-04-15 15:20:56 -05:00
}
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
digest , err := d . findFileInManifest ( tc . manifest , tc . file )
if tc . expected == "" {
assert . Error ( t , err )
return
}
assert . NoError ( t , err )
assert . Equal ( t , tc . expected , digest . Hex )
} )
}
}
func TestParseImgRef ( t * testing . T ) {
2024-08-13 10:03:33 -05:00
metadataRepo := "test" + metadataPath
2024-08-01 15:35:15 +01:00
metadataTag := LatestTag
delegatedRole := testRole
2024-08-30 17:03:29 +01:00
validRef := fmt . Sprintf ( "%s/2.root.json" , metadataRepo )
expectedRef := fmt . Sprintf ( "docker.io/%s:%s" , metadataRepo , metadataTag )
2024-04-15 15:20:56 -05:00
testCases := [ ] struct {
2024-08-30 17:03:29 +01:00
name string
ref string
expectedRef string
expectedFile string
metadataRepo string
metadataTag string
expectedRefError string
expectedConstructorError string
targetsRepo string
2024-04-15 15:20:56 -05:00
} {
2024-08-30 17:03:29 +01:00
{ 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 : "library path" , ref : fmt . Sprintf ( "test%s/2.root.json" , metadataPath ) , metadataRepo : "test" + metadataPath , expectedRef : "docker.io/test/tuf-metadata:latest" , expectedFile : "2.root.json" } ,
{ name : "short targets 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 ( "docker.io/%s:%s" , metadataRepo , delegatedRole ) , expectedFile : "5.test-role.json" } ,
{ name : "top-level target" , ref : fmt . Sprintf ( "%s/policy.yaml" , targetsRepo ) , expectedRef : fmt . Sprintf ( "docker.io/%s:policy.yaml" , targetsRepo ) , expectedFile : "policy.yaml" } ,
{ name : "delegated target" , ref : fmt . Sprintf ( "%s/%s/policy.yaml" , targetsRepo , delegatedRole ) , expectedRef : fmt . Sprintf ( "docker.io/%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" } ,
2024-04-15 15:20:56 -05:00
}
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
2024-08-30 17:03:29 +01:00
repo := metadataRepo
if tc . metadataRepo != "" {
repo = tc . metadataRepo
}
targets := targetsRepo
if tc . targetsRepo != "" {
targets = tc . targetsRepo
}
cfg := & config . UpdaterConfig {
RemoteMetadataURL : repo ,
RemoteTargetsURL : targets ,
}
2024-09-09 14:22:17 +01:00
d , err := NewRegistryFetcher ( context . Background ( ) , cfg )
2024-08-30 17:03:29 +01:00
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" )
}
2024-04-15 15:20:56 -05:00
}
} )
}
}
func TestGetDataFromLayer ( t * testing . T ) {
data := [ ] byte ( "test" )
layer := static . NewLayer ( data , tufTargetMediaType )
testCases := [ ] struct {
name string
layer v1 . Layer
max int64
expected [ ] byte
} {
{ "valid length" , layer , int64 ( len ( data ) ) , data } ,
{ "invalid length" , layer , int64 ( len ( data ) - 1 ) , nil } ,
}
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
data , err := getDataFromLayer ( tc . layer , tc . max )
if tc . expected == nil {
assert . Error ( t , err )
return
}
assert . NoError ( t , err )
assert . Equal ( t , tc . expected , data )
} )
}
}
func TestPullFileLayer ( t * testing . T ) {
2024-09-09 14:22:17 +01:00
ctx := context . Background ( )
regServer := test . NewLocalRegistry ( ctx )
defer regServer . Close ( )
url , err := url . Parse ( regServer . URL )
require . NoError ( t , err )
2024-04-15 15:20:56 -05:00
// make test layer
2024-08-01 15:35:15 +01:00
repo := tufMetadataRepo
2024-04-15 15:20:56 -05:00
data := [ ] byte ( "test" )
testLayer := static . NewLayer ( data , tufTargetMediaType )
hash , err := testLayer . Digest ( )
assert . NoError ( t , err )
layerRef := fmt . Sprintf ( "%s/%s@%s" , url . Host , repo , hash . String ( ) )
2024-09-09 14:22:17 +01:00
// cache test manifest
d , err := NewRegistryFetcher ( context . Background ( ) , & config . UpdaterConfig {
RemoteMetadataURL : tufMetadataRepo ,
RemoteTargetsURL : tufMetadataRepo ,
} )
require . NoError ( t , err )
2024-04-15 15:20:56 -05:00
d . cache . Put ( layerRef , data )
// push uncached image layer to local registry
uncachedData := [ ] byte ( "uncached" )
uncachedTestLayer := static . NewLayer ( uncachedData , tufTargetMediaType )
uncachedHash , err := uncachedTestLayer . Digest ( )
assert . NoError ( t , err )
uncachedLayerRef := fmt . Sprintf ( "%s/%s@%s" , url . Host , repo , uncachedHash . String ( ) )
img := empty . Image
img , err = mutate . Append ( img , mutate . Addendum { Layer : uncachedTestLayer } )
assert . NoError ( t , err )
2024-09-09 14:22:17 +01:00
err = crane . Push ( img , fmt . Sprintf ( "%s/%s" , url . Host , fmt . Sprintf ( "%s:latest" , repo ) ) , crane . WithUserAgent ( useragent . Get ( ctx ) ) )
2024-04-15 15:20:56 -05:00
assert . NoError ( t , err )
testCases := [ ] struct {
name string
ref string
maxLength int
expected [ ] byte
} {
{ "cached layer" , layerRef , len ( data ) , data } ,
{ "uncached layer" , uncachedLayerRef , len ( uncachedData ) , uncachedData } ,
}
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
layer , err := d . pullFileLayer ( tc . ref , int64 ( tc . maxLength ) )
if tc . expected == nil {
assert . Error ( t , err )
return
}
assert . NoError ( t , err )
assert . Greater ( t , len ( layer ) , 0 )
} )
}
}
func TestGetManifest ( t * testing . T ) {
2024-09-09 14:22:17 +01:00
ctx := context . Background ( )
regServer := test . NewLocalRegistry ( ctx )
defer regServer . Close ( )
url , err := url . Parse ( regServer . URL )
require . NoError ( t , err )
2024-04-15 15:20:56 -05:00
// make test manifest
2024-08-01 15:35:15 +01:00
repo := tufMetadataRepo
2024-04-15 15:20:56 -05:00
img := empty . Image
img = mutate . MediaType ( img , types . OCIManifestSchema1 )
img = mutate . ConfigMediaType ( img , types . OCIConfigJSON )
imgRef := fmt . Sprintf ( "%s/%s:latest" , url . Host , repo )
// cache test manifest
2024-09-09 14:22:17 +01:00
d , err := NewRegistryFetcher ( context . Background ( ) , & config . UpdaterConfig {
RemoteMetadataURL : tufMetadataRepo ,
RemoteTargetsURL : tufMetadataRepo ,
} )
require . NoError ( t , err )
2024-04-15 15:20:56 -05:00
mf , err := img . RawManifest ( )
assert . NoError ( t , err )
d . cache . Put ( imgRef , mf )
// push test image to local registry
unchachedImgRef := fmt . Sprintf ( "%s/%s:unchached" , url . Host , repo )
2024-09-09 14:22:17 +01:00
err = crane . Push ( img , unchachedImgRef , crane . WithUserAgent ( useragent . Get ( ctx ) ) )
2024-04-15 15:20:56 -05:00
assert . NoError ( t , err )
testCases := [ ] struct {
name string
ref string
expected [ ] byte
} {
{ "cached image manifest" , imgRef , mf } ,
{ "uncached image manifest" , unchachedImgRef , mf } ,
}
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
manifest , err := d . getManifest ( tc . ref )
assert . NoError ( t , err )
assert . Equal ( t , tc . expected , manifest )
} )
}
}
2024-08-01 15:35:15 +01:00
// LoadRegistryTestData pushes TUF metadata and targets to an OCI registry.
2024-09-09 14:22:17 +01:00
func LoadRegistryTestData ( ctx context . Context , t * testing . T , registry * url . URL , path string ) {
2024-04-15 15:20:56 -05:00
// push tuf metadata and targets to local registry
2024-08-01 15:35:15 +01:00
MetadataRepo := tufMetadataRepo
TargetsRepo := "tuf-targets"
DelegatedRole := testRole
2024-04-15 15:20:56 -05:00
// push top-level metadata -> metadata:latest
2024-09-09 14:22:17 +01:00
err := LoadMetadata ( ctx , filepath . Join ( path , "metadata" ) , registry . Host , MetadataRepo , LatestTag )
2024-04-15 15:20:56 -05:00
if err != nil {
t . Fatal ( err )
}
// push delegated metadata -> metadata:<DELEGATED_ROLE>
2024-09-09 14:22:17 +01:00
err = LoadMetadata ( ctx , filepath . Join ( path , "metadata" , DelegatedRole ) , registry . Host , MetadataRepo , DelegatedRole )
2024-04-15 15:20:56 -05:00
if err != nil {
t . Fatal ( err )
}
// push targets -> targets:<HASH>.<FILE>.ext (image) or targets:<DELEGATED ROLE> <index)
targetDirs , err := os . ReadDir ( filepath . Join ( path , "targets" ) )
if err != nil {
t . Fatal ( err )
}
for _ , dir := range targetDirs {
if ! dir . IsDir ( ) {
continue
}
tIdx , err := layout . ImageIndexFromPath ( filepath . Join ( path , "targets" , dir . Name ( ) ) )
if err != nil {
t . Fatal ( err )
}
2024-08-01 15:35:15 +01:00
ref , err := name . ParseReference ( fmt . Sprintf ( "%s/%s:%s" , registry . Host , TargetsRepo , dir . Name ( ) ) )
2024-04-15 15:20:56 -05:00
if err != nil {
t . Fatal ( err )
}
mf , err := tIdx . IndexManifest ( )
if err != nil {
t . Fatal ( err )
}
2024-09-10 17:40:20 +01:00
switch {
case len ( mf . Manifests ) == 1 :
2024-04-15 15:20:56 -05:00
// top-level target
img , err := tIdx . Image ( mf . Manifests [ 0 ] . Digest )
if err != nil {
t . Fatal ( err )
}
2024-09-09 14:22:17 +01:00
err = remote . Write ( ref , img , oci . WithOptions ( ctx , nil ) ... )
2024-04-15 15:20:56 -05:00
if err != nil {
t . Fatal ( err )
}
2024-09-10 17:40:20 +01:00
case len ( mf . Manifests ) > 1 :
2024-04-15 15:20:56 -05:00
// delegated target
2024-09-09 14:22:17 +01:00
err = remote . WriteIndex ( ref , tIdx , oci . WithOptions ( ctx , nil ) ... )
2024-04-15 15:20:56 -05:00
if err != nil {
t . Fatal ( err )
}
2024-08-01 15:35:15 +01:00
default :
2024-04-15 15:20:56 -05:00
t . Fatal ( "no manifests found" )
}
}
}
2024-08-01 15:35:15 +01:00
// LoadMetadata loads TUF metadata from a local path and pushes to a registry.
2024-09-09 14:22:17 +01:00
func LoadMetadata ( ctx context . Context , path , host , repo , tag string ) error {
2024-04-15 15:20:56 -05:00
mIdx , err := layout . ImageIndexFromPath ( path )
if err != nil {
return err
}
ref , err := name . ParseReference ( fmt . Sprintf ( "%s/%s:%s" , host , repo , tag ) )
if err != nil {
return err
}
mf , err := mIdx . IndexManifest ( )
if err != nil {
return err
}
img , err := mIdx . Image ( mf . Manifests [ 0 ] . Digest )
if err != nil {
return err
}
2024-09-09 14:22:17 +01:00
return remote . Write ( ref , img , oci . WithOptions ( ctx , nil ) ... )
2024-04-15 15:20:56 -05:00
}