diff --git a/attestation/vsa.go b/attestation/vsa.go index 8ebd398..d334d03 100644 --- a/attestation/vsa.go +++ b/attestation/vsa.go @@ -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 +} diff --git a/tuf/version.go b/tuf/version.go index 233501d..9c48ebd 100644 --- a/tuf/version.go +++ b/tuf/version.go @@ -39,7 +39,8 @@ func NewDefaultVersionChecker() *DefaultVersionChecker { type DefaultVersionChecker struct{} func (vc *DefaultVersionChecker) CheckVersion(client Downloader) error { - attestVersion, err := version.Get() + fetcher := version.NewGoVersionFetcher() + attestVersion, err := fetcher.Get() if err != nil { return fmt.Errorf("failed to get version: %w", err) } diff --git a/useragent/useragent.go b/useragent/useragent.go index ea51eb4..f551f4a 100644 --- a/useragent/useragent.go +++ b/useragent/useragent.go @@ -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 } diff --git a/verify.go b/verify.go index 4611fd1..ac2c0d6 100644 --- a/verify.go +++ b/verify.go @@ -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) } diff --git a/verify_test.go b/verify_test.go index 09546f8..ff51fd5 100644 --- a/verify_test.go +++ b/verify_test.go @@ -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) { diff --git a/version/version.go b/version/version.go index 14fda73..166dcaa 100644 --- a/version/version.go +++ b/version/version.go @@ -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 {