Merge pull request #124 from docker/feat-generate-vsa-policy-uri
feat: add `digest` and `downloadLocation` to VSA policy
This commit is contained in:
@@ -345,7 +345,11 @@ The VSA can be signed and published to the registry using the signing functions
|
|||||||
"timeVerified": "2024-04-19T08:00:00.01Z",
|
"timeVerified": "2024-04-19T08:00:00.01Z",
|
||||||
"resourceUri": "pkg:docker/example.org/example-image@1.0?platform=linux%2Famd64&digest=sha256%3A49f717386e5462e945232569a97a05831cb83bef8c3369be3bb7ea1793686960",
|
"resourceUri": "pkg:docker/example.org/example-image@1.0?platform=linux%2Famd64&digest=sha256%3A49f717386e5462e945232569a97a05831cb83bef8c3369be3bb7ea1793686960",
|
||||||
"policy": {
|
"policy": {
|
||||||
"uri": "https://example.org/internal-policy/v1"
|
"uri": "https://example.org/internal-policy/v1",
|
||||||
|
"downloadLocation": "https://docker.github.io/tuf-staging/targets/docker/d71d6b8f49fcba1295b16f5394dd5863a14e4277eb663d66d8c48e392509afe0.policy.rego",
|
||||||
|
"digest": {
|
||||||
|
"sha256": "d71d6b8f49fcba1295b16f5394dd5863a14e4277eb663d66d8c48e392509afe0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"verificationResult": "PASSED",
|
"verificationResult": "PASSED",
|
||||||
"verifiedLevels": ["SLSA_BUILD_LEVEL_3"]
|
"verifiedLevels": ["SLSA_BUILD_LEVEL_3"]
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vsaPolicy := attestation.VSAPolicy{URI: result.Summary.PolicyURI, DownloadLocation: p.URI, Digest: p.Digest}
|
||||||
|
|
||||||
return &VerificationResult{
|
return &VerificationResult{
|
||||||
Policy: p,
|
Policy: p,
|
||||||
Outcome: outcome,
|
Outcome: outcome,
|
||||||
@@ -103,7 +105,7 @@ func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.
|
|||||||
},
|
},
|
||||||
TimeVerified: time.Now().UTC().Format(time.RFC3339),
|
TimeVerified: time.Now().UTC().Format(time.RFC3339),
|
||||||
ResourceURI: resourceURI,
|
ResourceURI: resourceURI,
|
||||||
Policy: attestation.VSAPolicy{URI: result.Summary.PolicyURI},
|
Policy: vsaPolicy,
|
||||||
VerificationResult: outcomeStr,
|
VerificationResult: outcomeStr,
|
||||||
VerifiedLevels: result.Summary.SLSALevels,
|
VerifiedLevels: result.Summary.SLSALevels,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -112,7 +112,9 @@ func TestVSA(t *testing.T) {
|
|||||||
assert.Equal(t, "PASSED", attestationPredicate.VerificationResult)
|
assert.Equal(t, "PASSED", attestationPredicate.VerificationResult)
|
||||||
assert.Equal(t, "docker-official-images", attestationPredicate.Verifier.ID)
|
assert.Equal(t, "docker-official-images", attestationPredicate.Verifier.ID)
|
||||||
assert.Equal(t, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels)
|
assert.Equal(t, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels)
|
||||||
|
assert.Equal(t, PassPolicyDir+"/policy.rego", attestationPredicate.Policy.DownloadLocation)
|
||||||
assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI)
|
assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI)
|
||||||
|
assert.Equal(t, map[string]string{"sha256": "d71d6b8f49fcba1295b16f5394dd5863a14e4277eb663d66d8c48e392509afe0"}, attestationPredicate.Policy.Digest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVerificationFailure(t *testing.T) {
|
func TestVerificationFailure(t *testing.T) {
|
||||||
@@ -162,7 +164,9 @@ func TestVerificationFailure(t *testing.T) {
|
|||||||
assert.Equal(t, "FAILED", attestationPredicate.VerificationResult)
|
assert.Equal(t, "FAILED", attestationPredicate.VerificationResult)
|
||||||
assert.Equal(t, "docker-official-images", attestationPredicate.Verifier.ID)
|
assert.Equal(t, "docker-official-images", attestationPredicate.Verifier.ID)
|
||||||
assert.Equal(t, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels)
|
assert.Equal(t, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels)
|
||||||
|
assert.Equal(t, FailPolicyDir+"/policy.rego", attestationPredicate.Policy.DownloadLocation)
|
||||||
assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI)
|
assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI)
|
||||||
|
assert.Equal(t, map[string]string{"sha256": "ad045e1bd7cd602d90196acf68f2c57d7b51565d59e6e30e30d94ae86aa16201"}, attestationPredicate.Policy.Digest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSignVerify(t *testing.T) {
|
func TestSignVerify(t *testing.T) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type VSAPredicate struct {
|
|||||||
TimeVerified string `json:"timeVerified"`
|
TimeVerified string `json:"timeVerified"`
|
||||||
ResourceURI string `json:"resourceUri"`
|
ResourceURI string `json:"resourceUri"`
|
||||||
Policy VSAPolicy `json:"policy"`
|
Policy VSAPolicy `json:"policy"`
|
||||||
InputAttestations []VSAInputAttestation `json:"inputAttestations"`
|
InputAttestations []VSAInputAttestation `json:"inputAttestations,omitempty"`
|
||||||
VerificationResult string `json:"verificationResult"`
|
VerificationResult string `json:"verificationResult"`
|
||||||
VerifiedLevels []string `json:"verifiedLevels"`
|
VerifiedLevels []string `json:"verifiedLevels"`
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,9 @@ type VSAVerifier struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type VSAPolicy struct {
|
type VSAPolicy struct {
|
||||||
URI string `json:"uri"`
|
URI string `json:"uri,omitempty"`
|
||||||
|
Digest map[string]string `json:"digest"`
|
||||||
|
DownloadLocation string `json:"downloadLocation,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VSAInputAttestation struct {
|
type VSAInputAttestation struct {
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ func LoadTUFMappings(tufClient tuf.Downloader, localTargetsDir string) (*PolicyM
|
|||||||
return nil, fmt.Errorf("tuf client not set")
|
return nil, fmt.Errorf("tuf client not set")
|
||||||
}
|
}
|
||||||
filename := MappingFilename
|
filename := MappingFilename
|
||||||
_, fileContents, err := tufClient.DownloadTarget(filename, filepath.Join(localTargetsDir, filename))
|
file, err := tufClient.DownloadTarget(filename, filepath.Join(localTargetsDir, filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to download policy mapping file %s: %w", filename, err)
|
return nil, fmt.Errorf("failed to download policy mapping file %s: %w", filename, err)
|
||||||
}
|
}
|
||||||
mappings := &policyMappingsFile{}
|
mappings := &policyMappingsFile{}
|
||||||
|
|
||||||
err = yaml.Unmarshal(fileContents, mappings)
|
err = yaml.Unmarshal(file.Data, mappings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", filename, err)
|
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", filename, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func (m *TUFMirror) GetTUFTargetMirrors() ([]*Image, error) {
|
|||||||
targets := md.Targets[metadata.TARGETS].Signed.Targets
|
targets := md.Targets[metadata.TARGETS].Signed.Targets
|
||||||
for _, t := range targets {
|
for _, t := range targets {
|
||||||
// download target file
|
// download target file
|
||||||
_, data, err := m.TUFClient.DownloadTarget(t.Path, filepath.Join(m.tufPath, "download"))
|
file, err := m.TUFClient.DownloadTarget(t.Path, filepath.Join(m.tufPath, "download"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to download target %s: %w", t.Path, err)
|
return nil, fmt.Errorf("failed to download target %s: %w", t.Path, err)
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ func (m *TUFMirror) GetTUFTargetMirrors() ([]*Image, error) {
|
|||||||
}
|
}
|
||||||
name := hash.String() + "." + t.Path
|
name := hash.String() + "." + t.Path
|
||||||
ann := map[string]string{tufFileAnnotation: name}
|
ann := map[string]string{tufFileAnnotation: name}
|
||||||
layer := mutate.Addendum{Layer: static.NewLayer(data, tufTargetMediaType), Annotations: ann}
|
layer := mutate.Addendum{Layer: static.NewLayer(file.Data, tufTargetMediaType), Annotations: ann}
|
||||||
img, err = mutate.Append(img, layer)
|
img, err = mutate.Append(img, layer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to append role layer to image: %w", err)
|
return nil, fmt.Errorf("failed to append role layer to image: %w", err)
|
||||||
@@ -69,7 +69,7 @@ func (m *TUFMirror) GetDelegatedTargetMirrors() ([]*Index, error) {
|
|||||||
// for each target file, create an image with the target file as a layer
|
// for each target file, create an image with the target file as a layer
|
||||||
for _, target := range roleMeta.Signed.Targets {
|
for _, target := range roleMeta.Signed.Targets {
|
||||||
// download target file
|
// download target file
|
||||||
_, data, err := m.TUFClient.DownloadTarget(target.Path, filepath.Join(m.tufPath, "download"))
|
file, err := m.TUFClient.DownloadTarget(target.Path, filepath.Join(m.tufPath, "download"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to download target %s: %w", target.Path, err)
|
return nil, fmt.Errorf("failed to download target %s: %w", target.Path, err)
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,7 @@ func (m *TUFMirror) GetDelegatedTargetMirrors() ([]*Index, error) {
|
|||||||
}
|
}
|
||||||
name := hash.String() + "." + filename
|
name := hash.String() + "." + filename
|
||||||
ann := map[string]string{tufFileAnnotation: name}
|
ann := map[string]string{tufFileAnnotation: name}
|
||||||
layer := mutate.Addendum{Layer: static.NewLayer(data, tufTargetMediaType), Annotations: ann}
|
layer := mutate.Addendum{Layer: static.NewLayer(file.Data, tufTargetMediaType), Annotations: ann}
|
||||||
img, err = mutate.Append(img, layer)
|
img, err = mutate.Append(img, layer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to append role layer to image: %w", err)
|
return nil, fmt.Errorf("failed to append role layer to image: %w", err)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
|
"github.com/docker/attest/internal/util"
|
||||||
"github.com/docker/attest/pkg/attestation"
|
"github.com/docker/attest/pkg/attestation"
|
||||||
"github.com/docker/attest/pkg/config"
|
"github.com/docker/attest/pkg/config"
|
||||||
"github.com/docker/attest/pkg/oci"
|
"github.com/docker/attest/pkg/oci"
|
||||||
@@ -17,6 +18,8 @@ func resolveLocalPolicy(opts *Options, mapping *config.PolicyMapping, imageName
|
|||||||
if opts.LocalPolicyDir == "" {
|
if opts.LocalPolicyDir == "" {
|
||||||
return nil, fmt.Errorf("local policy dir not set")
|
return nil, fmt.Errorf("local policy dir not set")
|
||||||
}
|
}
|
||||||
|
var URI string
|
||||||
|
var digest map[string]string
|
||||||
files := make([]*File, 0, len(mapping.Files))
|
files := make([]*File, 0, len(mapping.Files))
|
||||||
for _, f := range mapping.Files {
|
for _, f := range mapping.Files {
|
||||||
filename := f.Path
|
filename := f.Path
|
||||||
@@ -29,10 +32,24 @@ func resolveLocalPolicy(opts *Options, mapping *config.PolicyMapping, imageName
|
|||||||
Path: filename,
|
Path: filename,
|
||||||
Content: fileContents,
|
Content: fileContents,
|
||||||
})
|
})
|
||||||
|
// if the file is a policy file, store the URI and digest
|
||||||
|
if filepath.Ext(filename) == ".rego" {
|
||||||
|
// TODO: support multiple rego files, need some way to identify the main policy file
|
||||||
|
if URI != "" {
|
||||||
|
return nil, fmt.Errorf("multiple policy files found in policy mapping")
|
||||||
|
}
|
||||||
|
URI = filePath
|
||||||
|
digest = map[string]string{"sha256": util.SHA256Hex(fileContents)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if URI == "" {
|
||||||
|
return nil, fmt.Errorf("no policy file found in policy mapping")
|
||||||
}
|
}
|
||||||
policy := &Policy{
|
policy := &Policy{
|
||||||
InputFiles: files,
|
InputFiles: files,
|
||||||
Mapping: mapping,
|
Mapping: mapping,
|
||||||
|
URI: URI,
|
||||||
|
Digest: digest,
|
||||||
}
|
}
|
||||||
if imageName != matchedName {
|
if imageName != matchedName {
|
||||||
policy.ResolvedName = matchedName
|
policy.ResolvedName = matchedName
|
||||||
@@ -41,21 +58,37 @@ func resolveLocalPolicy(opts *Options, mapping *config.PolicyMapping, imageName
|
|||||||
}
|
}
|
||||||
|
|
||||||
func resolveTUFPolicy(opts *Options, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
|
func resolveTUFPolicy(opts *Options, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
|
||||||
|
var URI string
|
||||||
|
var digest map[string]string
|
||||||
files := make([]*File, 0, len(mapping.Files))
|
files := make([]*File, 0, len(mapping.Files))
|
||||||
for _, f := range mapping.Files {
|
for _, f := range mapping.Files {
|
||||||
filename := f.Path
|
filename := f.Path
|
||||||
_, fileContents, err := opts.TUFClient.DownloadTarget(filename, filepath.Join(opts.LocalTargetsDir, filename))
|
file, err := opts.TUFClient.DownloadTarget(filename, filepath.Join(opts.LocalTargetsDir, filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to download policy file %s: %w", filename, err)
|
return nil, fmt.Errorf("failed to download policy file %s: %w", filename, err)
|
||||||
}
|
}
|
||||||
files = append(files, &File{
|
files = append(files, &File{
|
||||||
Path: filename,
|
Path: filename,
|
||||||
Content: fileContents,
|
Content: file.Data,
|
||||||
})
|
})
|
||||||
|
// if the file is a policy file, store the URI and digest
|
||||||
|
if filepath.Ext(filename) == ".rego" {
|
||||||
|
// TODO: support multiple rego files, need some way to identify the main policy file
|
||||||
|
if URI != "" {
|
||||||
|
return nil, fmt.Errorf("multiple policy files found in policy mapping")
|
||||||
|
}
|
||||||
|
URI = file.TargetURI
|
||||||
|
digest = map[string]string{"sha256": file.Digest}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if URI == "" {
|
||||||
|
return nil, fmt.Errorf("no policy file found in policy mapping")
|
||||||
}
|
}
|
||||||
policy := &Policy{
|
policy := &Policy{
|
||||||
InputFiles: files,
|
InputFiles: files,
|
||||||
Mapping: mapping,
|
Mapping: mapping,
|
||||||
|
URI: URI,
|
||||||
|
Digest: digest,
|
||||||
}
|
}
|
||||||
if imageName != matchedName {
|
if imageName != matchedName {
|
||||||
policy.ResolvedName = matchedName
|
policy.ResolvedName = matchedName
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func loadAttestation(t *testing.T, path string) *attestation.Envelope {
|
|||||||
|
|
||||||
func TestRegoEvaluator_Evaluate(t *testing.T) {
|
func TestRegoEvaluator_Evaluate(t *testing.T) {
|
||||||
ctx, _ := test.Setup(t)
|
ctx, _ := test.Setup(t)
|
||||||
errorStr := "failed to resolve policy by id: policy with id non-existent-policy-id not found"
|
resolveErrorStr := "failed to resolve policy by id: policy with id non-existent-policy-id not found"
|
||||||
TestDataPath := filepath.Join("..", "..", "test", "testdata")
|
TestDataPath := filepath.Join("..", "..", "test", "testdata")
|
||||||
ExampleAttestation := filepath.Join(TestDataPath, "example_attestation.json")
|
ExampleAttestation := filepath.Join(TestDataPath, "example_attestation.json")
|
||||||
|
|
||||||
@@ -43,22 +43,23 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
repo string
|
repo string
|
||||||
expectSuccess bool
|
expectSuccess bool
|
||||||
isCanonical bool
|
isCanonical bool
|
||||||
resolver attestation.Resolver
|
resolver attestation.Resolver
|
||||||
policy *policy.Options
|
policy *policy.Options
|
||||||
policyID string
|
policyID string
|
||||||
errorStr string
|
resolveErrorStr string
|
||||||
}{
|
}{
|
||||||
{repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver},
|
{repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver},
|
||||||
{repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver, policyID: "docker-official-images"},
|
{repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver, policyID: "docker-official-images"},
|
||||||
{repo: "testdata/mock-tuf-allow", expectSuccess: false, isCanonical: false, resolver: defaultResolver, policyID: "non-existent-policy-id", errorStr: errorStr},
|
{repo: "testdata/mock-tuf-allow", expectSuccess: false, isCanonical: false, resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr},
|
||||||
{repo: "testdata/mock-tuf-deny", expectSuccess: false, isCanonical: false, resolver: defaultResolver},
|
{repo: "testdata/mock-tuf-deny", expectSuccess: false, isCanonical: false, resolver: defaultResolver},
|
||||||
{repo: "testdata/mock-tuf-verify-sig", expectSuccess: true, isCanonical: false, resolver: defaultResolver},
|
{repo: "testdata/mock-tuf-verify-sig", expectSuccess: true, isCanonical: false, resolver: defaultResolver},
|
||||||
{repo: "testdata/mock-tuf-wrong-key", expectSuccess: false, isCanonical: false, resolver: defaultResolver},
|
{repo: "testdata/mock-tuf-wrong-key", expectSuccess: false, isCanonical: false, resolver: defaultResolver},
|
||||||
{repo: "testdata/mock-tuf-allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver},
|
{repo: "testdata/mock-tuf-allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver},
|
||||||
{repo: "testdata/mock-tuf-allow-canonical", expectSuccess: false, isCanonical: false, resolver: defaultResolver},
|
{repo: "testdata/mock-tuf-allow-canonical", expectSuccess: false, isCanonical: false, resolver: defaultResolver},
|
||||||
|
{repo: "testdata/mock-tuf-no-rego", expectSuccess: false, isCanonical: false, resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@@ -86,9 +87,9 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
|||||||
resolver, err := policy.CreateImageDetailsResolver(src)
|
resolver, err := policy.CreateImageDetailsResolver(src)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
policy, err := policy.ResolvePolicy(ctx, resolver, tc.policy)
|
policy, err := policy.ResolvePolicy(ctx, resolver, tc.policy)
|
||||||
if tc.errorStr != "" {
|
if tc.resolveErrorStr != "" {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), tc.errorStr)
|
assert.Contains(t, err.Error(), tc.resolveErrorStr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
require.NoErrorf(t, err, "failed to resolve policy")
|
require.NoErrorf(t, err, "failed to resolve policy")
|
||||||
|
|||||||
1
pkg/policy/testdata/mock-tuf-no-rego/doi/policy.yaml
vendored
Normal file
1
pkg/policy/testdata/mock-tuf-no-rego/doi/policy.yaml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
policy: "this is not rego"
|
||||||
11
pkg/policy/testdata/mock-tuf-no-rego/mapping.yaml
vendored
Normal file
11
pkg/policy/testdata/mock-tuf-no-rego/mapping.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# map repos to policies
|
||||||
|
version: v1
|
||||||
|
kind: policy-mapping
|
||||||
|
policies:
|
||||||
|
- id: docker-official-images
|
||||||
|
description: Docker Official Images
|
||||||
|
files:
|
||||||
|
- path: doi/policy.yaml
|
||||||
|
rules:
|
||||||
|
- pattern: "^docker[.]io/library/(.*)$"
|
||||||
|
policy-id: docker-official-images
|
||||||
@@ -40,6 +40,8 @@ type Policy struct {
|
|||||||
Query string
|
Query string
|
||||||
Mapping *config.PolicyMapping
|
Mapping *config.PolicyMapping
|
||||||
ResolvedName string
|
ResolvedName string
|
||||||
|
URI string
|
||||||
|
Digest map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Input struct {
|
type Input struct {
|
||||||
|
|||||||
@@ -28,16 +28,13 @@ func ExampleNewClient_registry() {
|
|||||||
|
|
||||||
// get trusted tuf metadata
|
// get trusted tuf metadata
|
||||||
trustedMetadata := registryClient.GetMetadata()
|
trustedMetadata := registryClient.GetMetadata()
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// top-level target files
|
// top-level target files
|
||||||
targets := trustedMetadata.Targets[metadata.TARGETS].Signed.Targets
|
targets := trustedMetadata.Targets[metadata.TARGETS].Signed.Targets
|
||||||
|
|
||||||
for _, t := range targets {
|
for _, t := range targets {
|
||||||
// download target files
|
// download target files
|
||||||
_, _, err := registryClient.DownloadTarget(t.Path, filepath.Join(tufOutputPath, "download"))
|
_, err := registryClient.DownloadTarget(t.Path, filepath.Join(tufOutputPath, "download"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/docker/attest/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MockTufClient struct {
|
type MockTufClient struct {
|
||||||
@@ -24,10 +26,11 @@ func NewMockTufClient(srcPath string, dstPath string) *MockTufClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dc *MockTufClient) DownloadTarget(target string, filePath string) (actualFilePath string, data []byte, err error) {
|
func (dc *MockTufClient) DownloadTarget(target string, filePath string) (file *TargetFile, err error) {
|
||||||
src, err := os.Open(filepath.Join(dc.srcPath, target))
|
targetPath := filepath.Join(dc.srcPath, target)
|
||||||
|
src, err := os.Open(targetPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer src.Close()
|
defer src.Close()
|
||||||
|
|
||||||
@@ -40,11 +43,11 @@ func (dc *MockTufClient) DownloadTarget(target string, filePath string) (actualF
|
|||||||
|
|
||||||
err = os.MkdirAll(filepath.Dir(dstFilePath), os.ModePerm)
|
err = os.MkdirAll(filepath.Dir(dstFilePath), os.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
dst, err := os.Create(dstFilePath)
|
dst, err := os.Create(dstFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer dst.Close()
|
defer dst.Close()
|
||||||
|
|
||||||
@@ -53,10 +56,10 @@ func (dc *MockTufClient) DownloadTarget(target string, filePath string) (actualF
|
|||||||
|
|
||||||
b, err := io.ReadAll(tee)
|
b, err := io.ReadAll(tee)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return dstFilePath, b, nil
|
return &TargetFile{ActualFilePath: dstFilePath, TargetURI: targetPath, Data: b, Digest: util.SHA256Hex(b)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type MockVersionChecker struct {
|
type MockVersionChecker struct {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Downloader interface {
|
type Downloader interface {
|
||||||
DownloadTarget(target, filePath string) (actualFilePath string, data []byte, err error)
|
DownloadTarget(target, filePath string) (file *TargetFile, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@@ -44,6 +44,13 @@ type Client struct {
|
|||||||
cfg *config.UpdaterConfig
|
cfg *config.UpdaterConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TargetFile struct {
|
||||||
|
ActualFilePath string
|
||||||
|
TargetURI string
|
||||||
|
Digest string
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
// NewClient creates a new TUF client.
|
// NewClient creates a new TUF client.
|
||||||
func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string, versionChecker VersionChecker) (*Client, error) {
|
func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string, versionChecker VersionChecker) (*Client, error) {
|
||||||
var tufSource Source
|
var tufSource Source
|
||||||
@@ -119,40 +126,69 @@ func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string
|
|||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Client) generateTargetURI(target *metadata.TargetFiles, digest string) (string, error) {
|
||||||
|
switch fetcher := t.cfg.Fetcher.(type) {
|
||||||
|
case *RegistryFetcher:
|
||||||
|
return fmt.Sprintf("%s@sha256:%s", t.cfg.RemoteTargetsURL, digest), nil
|
||||||
|
case *fetcher.DefaultFetcher:
|
||||||
|
targetBaseURL := ensureTrailingSlash(t.cfg.RemoteTargetsURL)
|
||||||
|
targetRemotePath := target.Path
|
||||||
|
// if PrefixTargetsWithHash is set, we need to prefix the target name with the hash and handle subdirectories
|
||||||
|
// similar logic to https://github.com/theupdateframework/go-tuf/blob/f95222bdd22d2ac4e5b8ed6fe912b645e213c3b5/metadata/updater/updater.go#L227-L247
|
||||||
|
if t.cfg.PrefixTargetsWithHash {
|
||||||
|
baseName := filepath.Base(targetRemotePath)
|
||||||
|
dirName, ok := strings.CutSuffix(targetRemotePath, "/"+baseName)
|
||||||
|
if !ok {
|
||||||
|
// <hash>.<target-name>
|
||||||
|
targetRemotePath = fmt.Sprintf("%s.%s", digest, baseName)
|
||||||
|
} else {
|
||||||
|
// <dir-prefix>/<hash>.<target-name>
|
||||||
|
targetRemotePath = fmt.Sprintf("%s/%s.%s", dirName, digest, baseName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s%s", targetBaseURL, targetRemotePath), nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported fetcher type: %T", fetcher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DownloadTarget downloads the target file using Updater. The Updater gets the target
|
// DownloadTarget downloads the target file using Updater. The Updater gets the target
|
||||||
// information, verifies if the target is already cached, and if it is not cached,
|
// information, verifies if the target is already cached, and if it is not cached,
|
||||||
// downloads the target file.
|
// downloads the target file.
|
||||||
func (t *Client) DownloadTarget(target string, filePath string) (actualFilePath string, data []byte, err error) {
|
func (t *Client) DownloadTarget(target string, filePath string) (file *TargetFile, err error) {
|
||||||
// search if the desired target is available
|
// search if the desired target is available
|
||||||
targetInfo, err := t.updater.GetTargetInfo(target)
|
targetInfo, err := t.updater.GetTargetInfo(target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if filePath exists and create the directory if it doesn't
|
// check if filePath exists and create the directory if it doesn't
|
||||||
if _, err := os.Stat(filepath.Dir(filePath)); os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Dir(filePath)); os.IsNotExist(err) {
|
||||||
err = os.MkdirAll(filepath.Dir(filePath), os.ModePerm)
|
err = os.MkdirAll(filepath.Dir(filePath), os.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("failed to create target download directory '%s': %w", filepath.Dir(filePath), err)
|
return nil, fmt.Errorf("failed to create target download directory '%s': %w", filepath.Dir(filePath), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// target is available, so let's see if the target is already present locally
|
// target is available, so let's see if the target is already present locally
|
||||||
actualFilePath, data, err = t.updater.FindCachedTarget(targetInfo, filePath)
|
actualFilePath, data, err := t.updater.FindCachedTarget(targetInfo, filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("failed while finding a cached target: %w", err)
|
return nil, fmt.Errorf("failed while finding a cached target: %w", err)
|
||||||
}
|
}
|
||||||
if data != nil {
|
if data != nil {
|
||||||
return actualFilePath, data, err
|
digest := util.SHA256Hex(data)
|
||||||
|
uri, err := t.generateTargetURI(targetInfo, digest)
|
||||||
|
return &TargetFile{ActualFilePath: actualFilePath, TargetURI: uri, Data: data, Digest: digest}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// target is not present locally, so let's try to download it
|
// target is not present locally, so let's try to download it
|
||||||
actualFilePath, data, err = t.updater.DownloadTarget(targetInfo, filePath, "")
|
actualFilePath, data, err = t.updater.DownloadTarget(targetInfo, filePath, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("failed to download target file %s - %w", target, err)
|
return nil, fmt.Errorf("failed to download target file %s - %w", target, err)
|
||||||
}
|
}
|
||||||
|
digest := util.SHA256Hex(data)
|
||||||
return actualFilePath, data, err
|
uri, err := t.generateTargetURI(targetInfo, digest)
|
||||||
|
return &TargetFile{ActualFilePath: actualFilePath, TargetURI: uri, Data: data, Digest: digest}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Client) GetMetadata() trustedmetadata.TrustedMetadata {
|
func (t *Client) GetMetadata() trustedmetadata.TrustedMetadata {
|
||||||
|
|||||||
@@ -122,14 +122,14 @@ func TestDownloadTarget(t *testing.T) {
|
|||||||
targets := trustedMetadata.Targets[metadata.TARGETS].Signed.Targets
|
targets := trustedMetadata.Targets[metadata.TARGETS].Signed.Targets
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
// download target files
|
// download target files
|
||||||
_, _, err := tufClient.DownloadTarget(target.Path, filepath.Join(tufPath, "download"))
|
_, err := tufClient.DownloadTarget(target.Path, filepath.Join(tufPath, "download"))
|
||||||
assert.NoErrorf(t, err, "Failed to download target: %v", err)
|
assert.NoErrorf(t, err, "Failed to download target: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// download delegated target
|
// download delegated target
|
||||||
targetInfo, err := tufClient.updater.GetTargetInfo(delegatedTargetFile)
|
targetInfo, err := tufClient.updater.GetTargetInfo(delegatedTargetFile)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
_, _, err = tufClient.DownloadTarget(targetInfo.Path, filepath.Join(tufPath, targetInfo.Path))
|
_, err = tufClient.DownloadTarget(targetInfo.Path, filepath.Join(tufPath, targetInfo.Path))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,11 +67,11 @@ func (vc *DefaultVersionChecker) CheckVersion(client Downloader) error {
|
|||||||
|
|
||||||
// see https://github.com/Masterminds/semver/blob/v3.2.1/README.md#checking-version-constraints
|
// see https://github.com/Masterminds/semver/blob/v3.2.1/README.md#checking-version-constraints
|
||||||
// for more information on the expected format of the version constraints in the TUF repo
|
// for more information on the expected format of the version constraints in the TUF repo
|
||||||
_, versionConstraintsBytes, err := client.DownloadTarget("version-constraints", "")
|
target, err := client.DownloadTarget("version-constraints", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to download version-constraints: %w", err)
|
return fmt.Errorf("failed to download version-constraints: %w", err)
|
||||||
}
|
}
|
||||||
versionConstraints, err := semver.NewConstraint(string(versionConstraintsBytes))
|
versionConstraints, err := semver.NewConstraint(string(target.Data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse minimum version: %w", err)
|
return fmt.Errorf("failed to parse minimum version: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user