Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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