diff --git a/pkg/attest/verify.go b/pkg/attest/verify.go index c1d6540e..0eba65be 100644 --- a/pkg/attest/verify.go +++ b/pkg/attest/verify.go @@ -86,6 +86,12 @@ func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy. return nil, err } + vsaPolicy := attestation.VSAPolicy{URI: result.Summary.PolicyURI} + // if the policy URI is not set by the result summary then use the policy values + if vsaPolicy.URI == "" { + vsaPolicy = attestation.VSAPolicy{URI: p.URI, Digest: p.Digest} + } + return &VerificationResult{ Policy: p, Outcome: outcome, @@ -103,7 +109,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, }, diff --git a/pkg/attest/verify_test.go b/pkg/attest/verify_test.go index abf80488..2a493f81 100644 --- a/pkg/attest/verify_test.go +++ b/pkg/attest/verify_test.go @@ -112,7 +112,8 @@ 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, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI) + assert.Equal(t, PassPolicyDir+"/policy.rego", attestationPredicate.Policy.URI) + assert.Equal(t, map[string]string{"sha256": "d71d6b8f49fcba1295b16f5394dd5863a14e4277eb663d66d8c48e392509afe0"}, attestationPredicate.Policy.Digest) } func TestVerificationFailure(t *testing.T) { @@ -162,7 +163,8 @@ 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, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI) + assert.Equal(t, FailPolicyDir+"/policy.rego", attestationPredicate.Policy.URI) + assert.Equal(t, map[string]string{"sha256": "ad045e1bd7cd602d90196acf68f2c57d7b51565d59e6e30e30d94ae86aa16201"}, attestationPredicate.Policy.Digest) } func TestSignVerify(t *testing.T) { diff --git a/pkg/attestation/vsa.go b/pkg/attestation/vsa.go index 0e5804e9..82f70228 100644 --- a/pkg/attestation/vsa.go +++ b/pkg/attestation/vsa.go @@ -26,7 +26,8 @@ type VSAVerifier struct { } type VSAPolicy struct { - URI string `json:"uri"` + URI string `json:"uri"` + Digest map[string]string `json:"digest"` } type VSAInputAttestation struct { diff --git a/pkg/config/config.go b/pkg/config/config.go index e724c2d9..ae127028 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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) } diff --git a/pkg/mirror/targets.go b/pkg/mirror/targets.go index df09e4c3..e738d954 100644 --- a/pkg/mirror/targets.go +++ b/pkg/mirror/targets.go @@ -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) } @@ -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) @@ -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) } @@ -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) diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index cfab2554..fa569cd3 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -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" @@ -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 @@ -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, } if imageName != matchedName { policy.ResolvedName = matchedName @@ -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 diff --git a/pkg/policy/types.go b/pkg/policy/types.go index 54cc993e..a122264e 100644 --- a/pkg/policy/types.go +++ b/pkg/policy/types.go @@ -40,6 +40,8 @@ type Policy struct { Query string Mapping *config.PolicyMapping ResolvedName string + URI string + Digest map[string]string } type Input struct { diff --git a/pkg/tuf/example_registry_test.go b/pkg/tuf/example_registry_test.go index bf9ddbca..b6e44495 100644 --- a/pkg/tuf/example_registry_test.go +++ b/pkg/tuf/example_registry_test.go @@ -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) } diff --git a/pkg/tuf/mock.go b/pkg/tuf/mock.go index ae795456..51894832 100644 --- a/pkg/tuf/mock.go +++ b/pkg/tuf/mock.go @@ -24,10 +24,10 @@ 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)) if err != nil { - return "", nil, err + return nil, err } defer src.Close() @@ -40,11 +40,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() @@ -53,10 +53,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, Data: b}, nil } type MockVersionChecker struct { diff --git a/pkg/tuf/registry.go b/pkg/tuf/registry.go index fa47cc33..09af4d80 100644 --- a/pkg/tuf/registry.go +++ b/pkg/tuf/registry.go @@ -81,7 +81,7 @@ func NewRegistryFetcher(metadataRepo, metadataTag, targetsRepo string) *Registry func (d *RegistryFetcher) DownloadFile(urlPath string, maxLength int64, timeout time.Duration) ([]byte, error) { d.timeout = timeout - imgRef, fileName, err := d.parseImgRef(urlPath) + imgRef, fileName, err := d.ParseImgRef(urlPath) if err != nil { return nil, err } @@ -186,7 +186,7 @@ func getDataFromLayer(fileLayer v1.Layer, maxLength int64) ([]byte, error) { } // parseImgRef maintains the Fetcher interface by parsing a URL path to an image reference and file name. -func (d *RegistryFetcher) parseImgRef(urlPath string) (imgRef, fileName string, err error) { +func (d *RegistryFetcher) ParseImgRef(urlPath string) (imgRef, fileName string, err error) { // Check if repo is target or metadata if strings.Contains(urlPath, d.targetsRepo) { // determine if the target path contains subdirectories and set image name accordingly diff --git a/pkg/tuf/registry_test.go b/pkg/tuf/registry_test.go index 45ead27b..6e3123b8 100644 --- a/pkg/tuf/registry_test.go +++ b/pkg/tuf/registry_test.go @@ -206,7 +206,7 @@ func TestParseImgRef(t *testing.T) { metadataTag: LatestTag, targetsRepo: targetsRepo, } - imgRef, file, err := d.parseImgRef(tc.ref) + imgRef, file, err := d.ParseImgRef(tc.ref) assert.NoError(t, err) assert.Equal(t, tc.expectedRef, imgRef) assert.Equal(t, tc.expectedFile, file) diff --git a/pkg/tuf/tuf.go b/pkg/tuf/tuf.go index 14fee910..06c24af4 100644 --- a/pkg/tuf/tuf.go +++ b/pkg/tuf/tuf.go @@ -36,7 +36,7 @@ var ( ) type Downloader interface { - DownloadTarget(target, filePath string) (actualFilePath string, data []byte, err error) + DownloadTarget(target, filePath string) (file *TargetFile, err error) } type Client struct { @@ -44,6 +44,13 @@ type Client struct { cfg *config.UpdaterConfig } +type TargetFile struct { + ActualFilePath string + TargetURI string + Digest string + Data []byte +} + // NewClient creates a new TUF client. func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string, versionChecker VersionChecker) (*Client, error) { var tufSource Source @@ -119,40 +126,73 @@ func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string return client, nil } +func (t *Client) generateTargetURI(target *metadata.TargetFiles, digest string) (string, error) { + targetBaseURL := ensureTrailingSlash(t.cfg.RemoteTargetsURL) + targetRemotePath := target.Path + if t.cfg.PrefixTargetsWithHash { + baseName := filepath.Base(targetRemotePath) + dirName, ok := strings.CutSuffix(targetRemotePath, "/"+baseName) + if !ok { + // . + targetRemotePath = fmt.Sprintf("%s.%s", digest, baseName) + } else { + // /. + targetRemotePath = fmt.Sprintf("%s/%s.%s", dirName, digest, baseName) + } + } + fullURL := fmt.Sprintf("%s%s", targetBaseURL, targetRemotePath) + + switch fetcher := t.cfg.Fetcher.(type) { + case *RegistryFetcher: + ref, _, err := fetcher.ParseImgRef(fullURL) + if err != nil { + return "", fmt.Errorf("failed to parse image reference: %w", err) + } + return ref, nil + case *fetcher.DefaultFetcher: + return fullURL, nil + default: + return "", fmt.Errorf("unsupported fetcher type: %T", fetcher) + } +} + // 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, // 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 targetInfo, err := t.updater.GetTargetInfo(target) if err != nil { - return "", nil, err + return nil, err } // check if filePath exists and create the directory if it doesn't if _, err := os.Stat(filepath.Dir(filePath)); os.IsNotExist(err) { err = os.MkdirAll(filepath.Dir(filePath), os.ModePerm) 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 - actualFilePath, data, err = t.updater.FindCachedTarget(targetInfo, filePath) + actualFilePath, data, err := t.updater.FindCachedTarget(targetInfo, filePath) 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 { - 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 actualFilePath, data, err = t.updater.DownloadTarget(targetInfo, filePath, "") 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) } - - 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 } func (t *Client) GetMetadata() trustedmetadata.TrustedMetadata { diff --git a/pkg/tuf/tuf_test.go b/pkg/tuf/tuf_test.go index ddd935b9..934f4dcb 100644 --- a/pkg/tuf/tuf_test.go +++ b/pkg/tuf/tuf_test.go @@ -122,14 +122,14 @@ func TestDownloadTarget(t *testing.T) { targets := trustedMetadata.Targets[metadata.TARGETS].Signed.Targets for _, target := range targets { // 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) } // download delegated target targetInfo, err := tufClient.updater.GetTargetInfo(delegatedTargetFile) 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) } } diff --git a/pkg/tuf/version.go b/pkg/tuf/version.go index f751d1eb..3c965638 100644 --- a/pkg/tuf/version.go +++ b/pkg/tuf/version.go @@ -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 // 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 { 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 { return fmt.Errorf("failed to parse minimum version: %w", err) }