feat!: remove MockTUFClient (#135)

* feat! remove MockTUFClient

*Breaking*
- use LocalPolicyDir and nil TUFClient instead

Other:
- add stateful Verifier
This commit is contained in:
James Carnegie
2024-08-28 09:53:52 +01:00
committed by GitHub
parent aed959f858
commit 9d39c5ae3d
22 changed files with 105 additions and 153 deletions

View File

@@ -31,6 +31,7 @@ func ExampleVerify_remote() {
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
DisableTUF: false, // set to disable TUF and rely on local policy files
}
src, err := oci.ParseImageSpec(image, oci.WithPlatform(platform))

View File

@@ -8,7 +8,6 @@ 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"
@@ -28,7 +27,6 @@ var (
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
@@ -45,6 +43,7 @@ func TestSignVerifyOCILayout(t *testing.T) {
}
policyOpts := &policy.Options{
LocalPolicyDir: PassPolicyDir,
DisableTUF: true,
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {

View File

@@ -17,26 +17,41 @@ import (
intoto "github.com/in-toto/in-toto-golang/in_toto"
)
func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.Options) (result *VerificationResult, err error) {
// so that we can resolve mapping from the image name earlier
detailsResolver, err := policy.CreateImageDetailsResolver(src)
if err != nil {
return nil, fmt.Errorf("failed to create image details resolver: %w", err)
}
err = populateDefaultOptions(opts)
type Verifier interface {
Verify(ctx context.Context, src *oci.ImageSpec) (result *VerificationResult, err error)
}
type tufVerifier struct {
opts *policy.Options
tufClient tuf.Downloader
}
func NewVerifier(opts *policy.Options) (Verifier, error) {
err := populateDefaultOptions(opts)
if err != nil {
return nil, err
}
tufClient, ok := tuf.GetDownloader(ctx)
if !ok {
var tufClient tuf.Downloader
if !opts.DisableTUF {
tufClient, err = tuf.NewClient(opts.TUFClientOptions)
if err != nil {
return nil, fmt.Errorf("failed to create TUF client: %w", err)
}
}
return &tufVerifier{
opts: opts,
tufClient: tufClient,
}, nil
}
pctx, err := policy.ResolvePolicy(ctx, tufClient, detailsResolver, opts)
func (v *tufVerifier) Verify(ctx context.Context, src *oci.ImageSpec) (result *VerificationResult, err error) {
// so that we can resolve mapping from the image name earlier
detailsResolver, err := policy.CreateImageDetailsResolver(src)
if err != nil {
return nil, fmt.Errorf("failed to create image details resolver: %w", err)
}
pctx, err := policy.ResolvePolicy(ctx, v.tufClient, detailsResolver, v.opts)
if err != nil {
return nil, fmt.Errorf("failed to resolve policy: %w", err)
}
@@ -47,14 +62,14 @@ func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.Options) (resu
}, nil
}
// this is overriding the mapping with a referrers config. Useful for testing if nothing else
if opts.ReferrersRepo != "" {
if v.opts.ReferrersRepo != "" {
pctx.Mapping.Attestations = &config.AttestationConfig{
Repo: opts.ReferrersRepo,
Repo: v.opts.ReferrersRepo,
Style: config.AttestationStyleReferrers,
}
} else if opts.AttestationStyle == config.AttestationStyleAttached {
} else if v.opts.AttestationStyle == config.AttestationStyleAttached {
pctx.Mapping.Attestations = &config.AttestationConfig{
Repo: opts.ReferrersRepo,
Repo: v.opts.ReferrersRepo,
Style: config.AttestationStyleAttached,
}
}
@@ -70,15 +85,27 @@ func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.Options) (resu
return result, nil
}
func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.Options) (result *VerificationResult, err error) {
verifier, err := NewVerifier(opts)
if err != nil {
return nil, err
}
return verifier.Verify(ctx, src)
}
func populateDefaultOptions(opts *policy.Options) (err error) {
if opts.LocalPolicyDir == "" && opts.DisableTUF {
return fmt.Errorf("local policy dir must be set if not using TUF")
}
if opts.LocalTargetsDir == "" {
opts.LocalTargetsDir, err = defaultLocalTargetsDir()
if err != nil {
return err
}
}
if opts.TUFClientOptions == nil {
if opts.DisableTUF && opts.TUFClientOptions != nil {
return fmt.Errorf("TUF client options set but TUF disabled")
} else if opts.TUFClientOptions == nil && !opts.DisableTUF {
opts.TUFClientOptions = tuf.NewDockerDefaultClientOptions(opts.LocalTargetsDir)
}
@@ -88,7 +115,6 @@ func populateDefaultOptions(opts *policy.Options) (err error) {
if opts.ReferrersRepo != "" && opts.AttestationStyle != config.AttestationStyleReferrers {
return fmt.Errorf("referrers repo specified but attestation source not set to referrers")
}
return nil
}

View File

@@ -70,7 +70,6 @@ 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)
@@ -93,6 +92,7 @@ func TestVSA(t *testing.T) {
policyOpts := &policy.Options{
LocalPolicyDir: PassPolicyDir,
AttestationStyle: config.AttestationStyleAttached,
DisableTUF: true,
}
results, err := Verify(ctx, spec, policyOpts)
require.NoError(t, err)
@@ -123,7 +123,6 @@ 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)
@@ -146,6 +145,7 @@ func TestVerificationFailure(t *testing.T) {
policyOpts := &policy.Options{
LocalPolicyDir: FailPolicyDir,
AttestationStyle: config.AttestationStyleAttached,
DisableTUF: true,
}
results, err := Verify(ctx, spec, policyOpts)
require.NoError(t, err)
@@ -176,16 +176,14 @@ 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
name string
signTL bool
policyDir string
imageName string
expectedNonSuccess Outcome
}{
{name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir},
@@ -223,6 +221,7 @@ func TestSignVerify(t *testing.T) {
policyOpts := &policy.Options{
LocalPolicyDir: tc.policyDir,
DisableTUF: true,
}
results, err := Verify(ctx, spec, policyOpts)
require.NoError(t, err)
@@ -251,6 +250,8 @@ func TestDefaultOptions(t *testing.T) {
attestationStyle config.AttestationStyle
referrersRepo string
expectedError string
disableTuf bool
localPolicyDir string
}{
{name: "empty"},
{name: "tufClient provided", tufOpts: &tuf.ClientOptions{MetadataSource: "a", TargetsSource: "b"}},
@@ -258,6 +259,8 @@ func TestDefaultOptions(t *testing.T) {
{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"},
{name: "tuf disabled and no local-policy-dir", disableTuf: true, expectedError: "local policy dir must be set if not using TUF"},
{name: "tuf disabled but options set", disableTuf: true, tufOpts: &tuf.ClientOptions{MetadataSource: "a", TargetsSource: "b"}, localPolicyDir: "foo", expectedError: "TUF client options set but TUF disabled"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
@@ -269,6 +272,8 @@ func TestDefaultOptions(t *testing.T) {
LocalTargetsDir: tc.localTargetsDir,
AttestationStyle: tc.attestationStyle,
ReferrersRepo: tc.referrersRepo,
DisableTUF: tc.disableTuf,
LocalPolicyDir: tc.localPolicyDir,
}
err = populateDefaultOptions(opts)

View File

@@ -13,7 +13,6 @@ 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"
@@ -28,14 +27,12 @@ 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
@@ -142,6 +139,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
policyOpts := &policy.Options{
LocalPolicyDir: LocalPolicy,
DisableTUF: true,
}
if tc.referrersRepo != "" {
@@ -185,7 +183,6 @@ 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
@@ -273,6 +270,7 @@ func TestReferencesInDifferentRepo(t *testing.T) {
referencedImage := fmt.Sprintf("%s@%s", indexName, mf.Digest.String())
policyOpts := &policy.Options{
LocalPolicyDir: PassPolicyDir,
DisableTUF: true,
}
src, err := oci.ParseImageSpec(referencedImage)
require.NoError(t, err)

View File

@@ -167,15 +167,16 @@ func resolvePolicyByID(opts *Options, tufClient tuf.Downloader) (*Policy, error)
return resolveLocalPolicy(opts, policy, "", "")
}
}
// must check tuf
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, tufClient, policy, "", "")
if !opts.DisableTUF {
// must check tuf
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, tufClient, policy, "", "")
}
}
return nil, fmt.Errorf("policy with id %s not found", opts.PolicyID)
}
@@ -209,29 +210,31 @@ func ResolvePolicy(ctx context.Context, tufClient tuf.Downloader, detailsResolve
if match.matchType == matchTypePolicy {
return resolveLocalPolicy(opts, match.policy, imageName, match.matchedName)
}
// must check tuf
tufMappings, err := config.LoadTUFMappings(tufClient, opts.LocalTargetsDir)
if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err)
}
if !opts.DisableTUF {
// must check tuf
tufMappings, err := config.LoadTUFMappings(tufClient, opts.LocalTargetsDir)
if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err)
}
// it's a mirror of a tuf policy
if match.matchType == matchTypeMatchNoPolicy {
for _, mapping := range tufMappings.Policies {
if mapping.ID == match.rule.PolicyID {
return resolveTUFPolicy(opts, tufClient, mapping, imageName, match.matchedName)
// it's a mirror of a tuf policy
if match.matchType == matchTypeMatchNoPolicy {
for _, mapping := range tufMappings.Policies {
if mapping.ID == match.rule.PolicyID {
return resolveTUFPolicy(opts, tufClient, mapping, imageName, match.matchedName)
}
}
}
// try to resolve a tuf policy directly
match, err = findPolicyMatch(imageName, tufMappings)
if err != nil {
return nil, err
}
if match.matchType == matchTypePolicy {
return resolveTUFPolicy(opts, tufClient, match.policy, imageName, match.matchedName)
}
}
// try to resolve a tuf policy directly
match, err = findPolicyMatch(imageName, tufMappings)
if err != nil {
return nil, err
}
if match.matchType == matchTypePolicy {
return resolveTUFPolicy(opts, tufClient, match.policy, imageName, match.matchedName)
}
return nil, nil
}

View File

@@ -11,7 +11,6 @@ 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/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -51,15 +50,15 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
policyID string
resolveErrorStr string
}{
{repo: "testdata/mock-tuf-allow", expectSuccess: true, resolver: defaultResolver},
{repo: "testdata/mock-tuf-allow", expectSuccess: true, resolver: defaultResolver, policyID: "docker-official-images"},
{repo: "testdata/mock-tuf-allow", resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr},
{repo: "testdata/mock-tuf-deny", resolver: defaultResolver},
{repo: "testdata/mock-tuf-verify-sig", expectSuccess: true, resolver: defaultResolver},
{repo: "testdata/mock-tuf-wrong-key", resolver: defaultResolver},
{repo: "testdata/mock-tuf-allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver},
{repo: "testdata/mock-tuf-allow-canonical", resolver: defaultResolver},
{repo: "testdata/mock-tuf-no-rego", resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"},
{repo: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver},
{repo: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver, policyID: "docker-official-images"},
{repo: "testdata/policies/allow", resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr},
{repo: "testdata/policies/deny", resolver: defaultResolver},
{repo: "testdata/policies/verify-sig", expectSuccess: true, resolver: defaultResolver},
{repo: "testdata/policies/wrong-key", resolver: defaultResolver},
{repo: "testdata/policies/allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver},
{repo: "testdata/policies/allow-canonical", resolver: defaultResolver},
{repo: "testdata/policies/no-rego", resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"},
}
for _, tc := range testCases {
@@ -72,11 +71,12 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
input.Tag = "test"
}
tufClient := tuf.NewMockTufClient(tc.repo, test.CreateTempDir(t, "", "tuf-dest"))
if tc.policy == nil {
tc.policy = &policy.Options{
LocalTargetsDir: test.CreateTempDir(t, "", "tuf-targets"),
PolicyID: tc.policyID,
LocalPolicyDir: tc.repo,
DisableTUF: true,
}
}
imageName, err := tc.resolver.ImageName(ctx)
@@ -87,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, tufClient, resolver, tc.policy)
policy, err := policy.ResolvePolicy(ctx, nil, resolver, tc.policy)
if tc.resolveErrorStr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.resolveErrorStr)
@@ -108,7 +108,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
}
func TestLoadingMappings(t *testing.T) {
policyMappings, err := config.LoadLocalMappings(filepath.Join("testdata", "mock-tuf-allow"))
policyMappings, err := config.LoadLocalMappings(filepath.Join("testdata", "policies", "allow"))
require.NoError(t, err)
assert.Equal(t, len(policyMappings.Rules), 3)
for _, mirror := range policyMappings.Rules {

View File

@@ -28,6 +28,7 @@ type Result struct {
type Options struct {
TUFClientOptions *tuf.ClientOptions
DisableTUF bool
LocalTargetsDir string
LocalPolicyDir string
PolicyID string

View File

@@ -1,67 +1,5 @@
package tuf
import (
"io"
"os"
"path/filepath"
"github.com/docker/attest/internal/util"
)
type MockTufClient struct {
srcPath string
dstPath string
}
func NewMockTufClient(srcPath string, dstPath string) *MockTufClient {
if srcPath == "" {
panic("srcPath must be set")
}
if dstPath == "" {
panic("dstPath must be set")
}
return &MockTufClient{
srcPath: srcPath,
dstPath: dstPath,
}
}
func (dc *MockTufClient) DownloadTarget(target string, filePath string) (file *TargetFile, err error) {
targetPath := filepath.Join(dc.srcPath, target)
src, err := os.Open(targetPath)
if err != nil {
return nil, err
}
defer src.Close()
var dstFilePath string
if filePath == "" {
dstFilePath = filepath.Join(dc.dstPath, filepath.FromSlash(target))
} else {
dstFilePath = filePath
}
err = os.MkdirAll(filepath.Dir(dstFilePath), os.ModePerm)
if err != nil {
return nil, err
}
dst, err := os.Create(dstFilePath)
if err != nil {
return nil, err
}
defer dst.Close()
// reading from tee will read from src and write to dst at the same time
tee := io.TeeReader(src, dst)
b, err := io.ReadAll(tee)
if err != nil {
return nil, err
}
return &TargetFile{ActualFilePath: dstFilePath, TargetURI: targetPath, Data: b, Digest: util.SHA256Hex(b)}, nil
}
type MockVersionChecker struct {
err error
}

View File

@@ -1,7 +1,6 @@
package tuf
import (
"context"
"errors"
"fmt"
"io/fs"
@@ -21,24 +20,6 @@ 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 (