Skip to content
This repository has been archived by the owner on Dec 3, 2024. It is now read-only.

feat: add digest and downloadLocation to VSA policy #124

Merged
merged 9 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"resourceUri": "pkg:docker/example.org/[email protected]?platform=linux%2Famd64&digest=sha256%3A49f717386e5462e945232569a97a05831cb83bef8c3369be3bb7ea1793686960",
"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",
"verifiedLevels": ["SLSA_BUILD_LEVEL_3"]
Expand Down
4 changes: 3 additions & 1 deletion pkg/attest/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.
return nil, err
}

vsaPolicy := attestation.VSAPolicy{URI: result.Summary.PolicyURI, DownloadLocation: p.URI, Digest: p.Digest}

return &VerificationResult{
Policy: p,
Outcome: outcome,
Expand All @@ -103,7 +105,7 @@ func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.
},
TimeVerified: time.Now().UTC().Format(time.RFC3339),
ResourceURI: resourceURI,
Policy: attestation.VSAPolicy{URI: result.Summary.PolicyURI},
Policy: vsaPolicy,
VerificationResult: outcomeStr,
VerifiedLevels: result.Summary.SLSALevels,
},
Expand Down
4 changes: 4 additions & 0 deletions pkg/attest/verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ func TestVSA(t *testing.T) {
assert.Equal(t, "PASSED", attestationPredicate.VerificationResult)
assert.Equal(t, "docker-official-images", attestationPredicate.Verifier.ID)
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, map[string]string{"sha256": "d71d6b8f49fcba1295b16f5394dd5863a14e4277eb663d66d8c48e392509afe0"}, attestationPredicate.Policy.Digest)
}

func TestVerificationFailure(t *testing.T) {
Expand Down Expand Up @@ -162,7 +164,9 @@ func TestVerificationFailure(t *testing.T) {
assert.Equal(t, "FAILED", attestationPredicate.VerificationResult)
assert.Equal(t, "docker-official-images", attestationPredicate.Verifier.ID)
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, map[string]string{"sha256": "ad045e1bd7cd602d90196acf68f2c57d7b51565d59e6e30e30d94ae86aa16201"}, attestationPredicate.Policy.Digest)
}

func TestSignVerify(t *testing.T) {
Expand Down
6 changes: 4 additions & 2 deletions pkg/attestation/vsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type VSAPredicate struct {
TimeVerified string `json:"timeVerified"`
ResourceURI string `json:"resourceUri"`
Policy VSAPolicy `json:"policy"`
InputAttestations []VSAInputAttestation `json:"inputAttestations"`
InputAttestations []VSAInputAttestation `json:"inputAttestations,omitempty"`
whalelines marked this conversation as resolved.
Show resolved Hide resolved
VerificationResult string `json:"verificationResult"`
VerifiedLevels []string `json:"verifiedLevels"`
}
Expand All @@ -26,7 +26,9 @@ type VSAVerifier 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 {
Expand Down
4 changes: 2 additions & 2 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ func LoadTUFMappings(tufClient tuf.Downloader, localTargetsDir string) (*PolicyM
return nil, fmt.Errorf("tuf client not set")
}
filename := MappingFilename
_, fileContents, err := tufClient.DownloadTarget(filename, filepath.Join(localTargetsDir, filename))
file, err := tufClient.DownloadTarget(filename, filepath.Join(localTargetsDir, filename))
if err != nil {
return nil, fmt.Errorf("failed to download policy mapping file %s: %w", filename, err)
}
mappings := &policyMappingsFile{}

err = yaml.Unmarshal(fileContents, mappings)
err = yaml.Unmarshal(file.Data, mappings)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", filename, err)
}
Expand Down
8 changes: 4 additions & 4 deletions pkg/mirror/targets.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (m *TUFMirror) GetTUFTargetMirrors() ([]*Image, error) {
targets := md.Targets[metadata.TARGETS].Signed.Targets
for _, t := range targets {
// 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 {
return nil, fmt.Errorf("failed to download target %s: %w", t.Path, err)
}
Expand All @@ -38,7 +38,7 @@ func (m *TUFMirror) GetTUFTargetMirrors() ([]*Image, error) {
}
name := hash.String() + "." + t.Path
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)
if err != nil {
return nil, fmt.Errorf("failed to append role layer to image: %w", err)
Expand Down Expand Up @@ -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 _, target := range roleMeta.Signed.Targets {
// 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 {
return nil, fmt.Errorf("failed to download target %s: %w", target.Path, err)
}
Expand All @@ -89,7 +89,7 @@ func (m *TUFMirror) GetDelegatedTargetMirrors() ([]*Index, error) {
}
name := hash.String() + "." + filename
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)
if err != nil {
return nil, fmt.Errorf("failed to append role layer to image: %w", err)
Expand Down
31 changes: 29 additions & 2 deletions pkg/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"path/filepath"

"github.com/distribution/reference"
"github.com/docker/attest/internal/util"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
Expand All @@ -17,6 +18,8 @@ func resolveLocalPolicy(opts *Options, mapping *config.PolicyMapping, imageName
if opts.LocalPolicyDir == "" {
return nil, fmt.Errorf("local policy dir not set")
}
var URI string
var digest map[string]string
files := make([]*File, 0, len(mapping.Files))
for _, f := range mapping.Files {
filename := f.Path
Expand All @@ -29,10 +32,21 @@ func resolveLocalPolicy(opts *Options, mapping *config.PolicyMapping, imageName
Path: filename,
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)}
}
}
policy := &Policy{
InputFiles: files,
Mapping: mapping,
URI: URI,
Digest: digest,
mrjoelkamp marked this conversation as resolved.
Show resolved Hide resolved
}
if imageName != matchedName {
policy.ResolvedName = matchedName
Expand All @@ -41,21 +55,34 @@ func resolveLocalPolicy(opts *Options, mapping *config.PolicyMapping, imageName
}

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))
for _, f := range mapping.Files {
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 {
return nil, fmt.Errorf("failed to download policy file %s: %w", filename, err)
}
files = append(files, &File{
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}
}
}
policy := &Policy{
InputFiles: files,
Mapping: mapping,
URI: URI,
Digest: digest,
}
if imageName != matchedName {
policy.ResolvedName = matchedName
Expand Down
30 changes: 19 additions & 11 deletions pkg/policy/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ func loadAttestation(t *testing.T, path string) *attestation.Envelope {

func TestRegoEvaluator_Evaluate(t *testing.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"
evalErrorStr := "rego_parse_error:"
TestDataPath := filepath.Join("..", "..", "test", "testdata")
ExampleAttestation := filepath.Join(TestDataPath, "example_attestation.json")

Expand All @@ -43,22 +44,24 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
}

testCases := []struct {
repo string
expectSuccess bool
isCanonical bool
resolver attestation.Resolver
policy *policy.Options
policyID string
errorStr string
repo string
expectSuccess bool
isCanonical bool
resolver attestation.Resolver
policy *policy.Options
policyID string
resolveErrorStr string
evalErrorStr string
}{
{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: 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-verify-sig", expectSuccess: true, 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: false, isCanonical: false, resolver: defaultResolver},
{repo: "testdata/mock-tuf-no-rego", expectSuccess: false, isCanonical: false, resolver: defaultResolver, evalErrorStr: evalErrorStr},
}

for _, tc := range testCases {
Expand Down Expand Up @@ -86,14 +89,19 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
resolver, err := policy.CreateImageDetailsResolver(src)
require.NoError(t, err)
policy, err := policy.ResolvePolicy(ctx, resolver, tc.policy)
if tc.errorStr != "" {
if tc.resolveErrorStr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errorStr)
assert.Contains(t, err.Error(), tc.resolveErrorStr)
return
}
require.NoErrorf(t, err, "failed to resolve policy")
require.NotNil(t, policy, "policy should not be nil")
result, err := re.Evaluate(ctx, tc.resolver, policy, input)
if tc.evalErrorStr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.evalErrorStr)
return
}
require.NoErrorf(t, err, "Evaluate failed")

if tc.expectSuccess {
Expand Down
1 change: 1 addition & 0 deletions pkg/policy/testdata/mock-tuf-no-rego/doi/policy.not-rego
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
this isn't a rego policy
11 changes: 11 additions & 0 deletions pkg/policy/testdata/mock-tuf-no-rego/mapping.yaml
Original file line number Diff line number Diff line change
@@ -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.not-rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images
2 changes: 2 additions & 0 deletions pkg/policy/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ type Policy struct {
Query string
Mapping *config.PolicyMapping
ResolvedName string
URI string
Digest map[string]string
}

type Input struct {
Expand Down
5 changes: 1 addition & 4 deletions pkg/tuf/example_registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,13 @@ func ExampleNewClient_registry() {

// get trusted tuf metadata
trustedMetadata := registryClient.GetMetadata()
if err != nil {
panic(err)
}

// top-level target files
targets := trustedMetadata.Targets[metadata.TARGETS].Signed.Targets

for _, t := range targets {
// download target files
_, _, err := registryClient.DownloadTarget(t.Path, filepath.Join(tufOutputPath, "download"))
_, err := registryClient.DownloadTarget(t.Path, filepath.Join(tufOutputPath, "download"))
if err != nil {
panic(err)
}
Expand Down
17 changes: 10 additions & 7 deletions pkg/tuf/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"io"
"os"
"path/filepath"

"github.com/docker/attest/internal/util"
)

type MockTufClient struct {
Expand All @@ -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) {
src, err := os.Open(filepath.Join(dc.srcPath, target))
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
return nil, err
}
defer src.Close()

Expand All @@ -40,11 +43,11 @@ func (dc *MockTufClient) DownloadTarget(target string, filePath string) (actualF

err = os.MkdirAll(filepath.Dir(dstFilePath), os.ModePerm)
if err != nil {
return "", nil, err
return nil, err
}
dst, err := os.Create(dstFilePath)
if err != nil {
return "", nil, err
return nil, err
}
defer dst.Close()

Expand All @@ -53,10 +56,10 @@ func (dc *MockTufClient) DownloadTarget(target string, filePath string) (actualF

b, err := io.ReadAll(tee)
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 {
Expand Down
Loading
Loading