Merge pull request #201 from docker/feat--add-verifier-version-to-vsa

feat: add verifier version to vsa
This commit is contained in:
Joel Kamp
2024-10-17 08:09:17 -05:00
committed by GitHub
7 changed files with 131 additions and 15 deletions

View File

@@ -3,6 +3,7 @@ package attestation
import (
"fmt"
"github.com/docker/attest/version"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/package-url/packageurl-go"
)
@@ -22,9 +23,12 @@ type VSAPredicate struct {
}
type VSAVerifier struct {
ID string `json:"id"`
ID string `json:"id"`
Version VerifierVersion `json:"version"`
}
type VerifierVersion map[string]string
type VSAPolicy struct {
URI string `json:"uri,omitempty"`
Digest map[string]string `json:"digest"`
@@ -44,3 +48,16 @@ func ToVSAResourceURI(sub intoto.Subject) (string, error) {
purl.Qualifiers = packageurl.QualifiersFromMap(quals)
return purl.String(), nil
}
func GetVerifierVersion(fetcher version.Fetcher) (VerifierVersion, error) {
attestVersion, err := fetcher.Get()
if err != nil {
return nil, fmt.Errorf("failed to get attest version: %w", err)
}
if attestVersion == nil {
return nil, nil
}
return VerifierVersion{
version.ThisModulePath: attestVersion.String(),
}, nil
}

View File

@@ -33,13 +33,17 @@ func (e *InvalidVersionError) Error() string {
}
func NewDefaultVersionChecker() *DefaultVersionChecker {
return &DefaultVersionChecker{}
return &DefaultVersionChecker{
VersionFetcher: version.NewGoVersionFetcher(),
}
}
type DefaultVersionChecker struct{}
type DefaultVersionChecker struct {
VersionFetcher version.Fetcher
}
func (vc *DefaultVersionChecker) CheckVersion(client Downloader) error {
attestVersion, err := version.Get()
attestVersion, err := vc.VersionFetcher.Get()
if err != nil {
return fmt.Errorf("failed to get version: %w", err)
}

61
tuf/version_test.go Normal file
View File

@@ -0,0 +1,61 @@
package tuf
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/Masterminds/semver/v3"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/version"
"github.com/stretchr/testify/assert"
)
const (
invalidVersion = "0.0.1"
validVersion = "v1.0.0-0"
versionConstraint = ">=v1.0.0-0"
)
func TestDefaultVersionChecker(t *testing.T) {
testDir := test.CreateTempDir(t, "", "tuf_temp")
versionConstraintsPath := filepath.Join(testDir, "version-constraints")
err := os.WriteFile(versionConstraintsPath, []byte(versionConstraint), 0o600)
assert.NoError(t, err)
tufClient := NewMockTufClient(testDir)
expectedError := fmt.Sprintf("%s version %s does not satisfy constraints %s: %s is less than %s", version.ThisModulePath, invalidVersion, versionConstraint, invalidVersion, validVersion)
testCases := []struct {
name string
expectedError string
version string
}{
{name: "version is less than the minimum", expectedError: expectedError, version: "0.0.1"},
{name: "version is equal to the minimum", version: "1.0.0"},
{name: "version is greater than the minimum", version: "1.0.1"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
checker := NewDefaultVersionChecker()
checker.VersionFetcher = &MockVersionFetcher{version: tc.version}
err := checker.CheckVersion(tufClient)
if tc.expectedError != "" {
assert.Error(t, err)
assert.Equal(t, tc.expectedError, err.Error())
return
}
assert.NoError(t, err)
})
}
}
type MockVersionFetcher struct {
version string
}
func (m *MockVersionFetcher) Get() (*semver.Version, error) {
return semver.NewVersion(m.version)
}

View File

@@ -19,10 +19,11 @@ func Set(ctx context.Context, userAgent string) context.Context {
// Get retrieves the HTTP user agent from the context.
func Get(ctx context.Context) string {
fetcher := version.NewGoVersionFetcher()
if ua, ok := ctx.Value(userAgentKey).(string); ok {
return ua
}
version, err := version.Get()
version, err := fetcher.Get()
if err != nil || version == nil {
return defaultUserAgent
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/docker/attest/oci"
"github.com/docker/attest/policy"
"github.com/docker/attest/tuf"
"github.com/docker/attest/version"
intoto "github.com/in-toto/in-toto-golang/in_toto"
)
@@ -21,6 +22,7 @@ type ImageVerifier struct {
opts *policy.Options
tufClient tuf.Downloader
attestationVerifier attestation.Verifier
versionFetcher version.Fetcher
}
func NewImageVerifier(ctx context.Context, opts *policy.Options) (*ImageVerifier, error) {
@@ -46,6 +48,7 @@ func NewImageVerifier(ctx context.Context, opts *policy.Options) (*ImageVerifier
opts: opts,
tufClient: tufClient,
attestationVerifier: attestationVerifier,
versionFetcher: version.NewGoVersionFetcher(),
}, nil
}
@@ -93,7 +96,7 @@ func (verifier *ImageVerifier) Verify(ctx context.Context, src *oci.ImageSpec) (
return nil, fmt.Errorf("failed to create attestation resolver: %w", err)
}
evaluator := policy.NewRegoEvaluator(verifier.opts.Debug, verifier.attestationVerifier)
result, err = verifyAttestations(ctx, resolver, evaluator, resolvedPolicy, verifier.opts)
result, err = verifier.verifyAttestations(ctx, resolver, evaluator, resolvedPolicy)
if err != nil {
return nil, fmt.Errorf("failed to evaluate policy: %w", err)
}
@@ -141,7 +144,7 @@ func defaultLocalTargetsDir() (string, error) {
return filepath.Join(homeDir, ".docker", "tuf"), nil
}
func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.Result) (*VerificationResult, error) {
func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.Result, versionFetcher version.Fetcher) (*VerificationResult, error) {
dgst, err := oci.SplitDigest(input.Digest)
if err != nil {
return nil, fmt.Errorf("failed to split digest: %w", err)
@@ -168,6 +171,10 @@ func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.
}
vsaPolicy := attestation.VSAPolicy{URI: result.Summary.PolicyURI, DownloadLocation: p.URI, Digest: p.Digest}
attestVersion, err := attestation.GetVerifierVersion(versionFetcher)
if err != nil {
return nil, fmt.Errorf("failed to get verifier version: %w", err)
}
return &VerificationResult{
Policy: p,
@@ -182,7 +189,8 @@ func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.
},
Predicate: attestation.VSAPredicate{
Verifier: attestation.VSAVerifier{
ID: result.Summary.Verifier,
ID: result.Summary.Verifier,
Version: attestVersion,
},
TimeVerified: time.Now().UTC().Format(time.RFC3339),
ResourceURI: resourceURI,
@@ -195,7 +203,7 @@ func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.
}, nil
}
func verifyAttestations(ctx context.Context, resolver attestation.Resolver, evaluator policy.Evaluator, resolvedPolicy *policy.Policy, opts *policy.Options) (*VerificationResult, error) {
func (verifier *ImageVerifier) verifyAttestations(ctx context.Context, resolver attestation.Resolver, evaluator policy.Evaluator, resolvedPolicy *policy.Policy) (*VerificationResult, error) {
desc, err := resolver.ImageDescriptor(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get image descriptor: %w", err)
@@ -247,7 +255,7 @@ func verifyAttestations(ctx context.Context, resolver attestation.Resolver, eval
Domain: reference.Domain(ref),
NormalizedName: reference.Path(ref),
FamiliarName: reference.FamiliarName(ref),
Parameters: opts.Parameters,
Parameters: verifier.opts.Parameters,
}
// rego has null strings
if tag != "" {
@@ -257,7 +265,7 @@ func verifyAttestations(ctx context.Context, resolver attestation.Resolver, eval
if err != nil {
return nil, fmt.Errorf("policy evaluation failed: %w", err)
}
verificationResult, err := toVerificationResult(resolvedPolicy, input, result)
verificationResult, err := toVerificationResult(resolvedPolicy, input, result, verifier.versionFetcher)
if err != nil {
return nil, fmt.Errorf("failed to convert to policy result: %w", err)
}

View File

@@ -9,6 +9,7 @@ import (
"testing"
"time"
"github.com/Masterminds/semver/v3"
"github.com/distribution/reference"
"github.com/docker/attest/attestation"
"github.com/docker/attest/internal/test"
@@ -17,6 +18,7 @@ import (
"github.com/docker/attest/policy"
"github.com/docker/attest/tlog"
"github.com/docker/attest/tuf"
"github.com/docker/attest/version"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/secure-systems-lab/go-securesystemslib/dsse"
"github.com/stretchr/testify/assert"
@@ -32,9 +34,16 @@ var (
)
const (
LinuxAMD64 = "linux/amd64"
LinuxAMD64 = "linux/amd64"
TestVerifierVersion = "9.9.9"
)
type MockVersionFetcher struct{}
func (m *MockVersionFetcher) Get() (*semver.Version, error) {
return semver.NewVersion(TestVerifierVersion)
}
func TestVerifyAttestations(t *testing.T) {
ex, err := os.ReadFile(ExampleAttestation)
assert.NoError(t, err)
@@ -62,7 +71,9 @@ func TestVerifyAttestations(t *testing.T) {
return policy.AllowedResult(), tc.policyEvaluationError
},
}
_, err := verifyAttestations(ctx, resolver, &mockPE, &policy.Policy{ResolvedName: ""}, &policy.Options{})
verifier, err := NewImageVerifier(ctx, &policy.Options{})
require.NoError(t, err)
_, err = verifier.verifyAttestations(ctx, resolver, &mockPE, &policy.Policy{ResolvedName: ""})
if tc.expectedError != nil {
if assert.Error(t, err) {
assert.Equal(t, tc.expectedError.Error(), err.Error())
@@ -102,7 +113,10 @@ func TestVSA(t *testing.T) {
AttestationStyle: mapping.AttestationStyleAttached,
DisableTUF: true,
}
results, err := Verify(ctx, spec, policyOpts)
verifier, err := NewImageVerifier(ctx, policyOpts)
require.NoError(t, err)
verifier.versionFetcher = &MockVersionFetcher{}
results, err := verifier.Verify(ctx, spec)
require.NoError(t, err)
assert.Equal(t, OutcomeSuccess, results.Outcome)
assert.Empty(t, results.Violations)
@@ -135,6 +149,7 @@ func TestVSA(t *testing.T) {
assert.NotEmpty(t, digest)
assert.Contains(t, []string{"application/vnd.in-toto.provenance+dsse", "application/vnd.in-toto.spdx+dsse"}, input.MediaType)
}
assert.Equal(t, TestVerifierVersion, attestationPredicate.Verifier.Version[version.ThisModulePath])
}
func TestVerificationFailure(t *testing.T) {

View File

@@ -9,9 +9,19 @@ import (
const ThisModulePath = "github.com/docker/attest"
type Fetcher interface {
Get() (*semver.Version, error)
}
type GoModVersionFetcher struct{}
func NewGoVersionFetcher() *GoModVersionFetcher {
return &GoModVersionFetcher{}
}
// Get returns the version of the attest module.
// this can return nil if the version can't be determined (without an error).
func Get() (*semver.Version, error) {
func (*GoModVersionFetcher) Get() (*semver.Version, error) {
var attestMod *debug.Module
bi, ok := debug.ReadBuildInfo()
if !ok {