diff --git a/README.md b/README.md index 161acd9..0656118 100644 --- a/README.md +++ b/README.md @@ -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/example-image@1.0?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"] diff --git a/go.mod b/go.mod index 4aff550..e493793 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.22.5 require ( github.com/Masterminds/semver/v3 v3.2.1 - github.com/aws/aws-sdk-go-v2/config v1.27.27 + github.com/aws/aws-sdk-go-v2/config v1.27.28 github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8 github.com/containerd/platforms v0.2.1 github.com/distribution/reference v0.6.0 @@ -47,21 +47,21 @@ require ( github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.30.4 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.28 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect github.com/aws/aws-sdk-go-v2/service/ecr v1.29.1 // indirect github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.24.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect - github.com/aws/smithy-go v1.20.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.30.4 // indirect + github.com/aws/smithy-go v1.20.4 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect diff --git a/go.sum b/go.sum index efc135a..c757431 100644 --- a/go.sum +++ b/go.sum @@ -104,38 +104,38 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= -github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= -github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90= -github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg= -github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI= -github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8= +github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= +github.com/aws/aws-sdk-go-v2/config v1.27.28 h1:OTxWGW/91C61QlneCtnD62NLb4W616/NM1jA8LhJqbg= +github.com/aws/aws-sdk-go-v2/config v1.27.28/go.mod h1:uzVRVtJSU5EFv6Fu82AoVFKozJi2ZCY6WRCXj06rbvs= +github.com/aws/aws-sdk-go-v2/credentials v1.17.28 h1:m8+AHY/ND8CMHJnPoH7PJIRakWGa4gbfbxuY9TGTUXM= +github.com/aws/aws-sdk-go-v2/credentials v1.17.28/go.mod h1:6TF7dSc78ehD1SL6KpRIPKMA1GyyWflIkjqg+qmf4+c= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 h1:yjwoSyDZF8Jth+mUk5lSPJCkMC0lMy6FaCD51jm6ayE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12/go.mod h1:fuR57fAgMk7ot3WcNQfb6rSEn+SUffl7ri+aa8uKysI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16/go.mod h1:2DwJF39FlNAUiX5pAc0UNeiz16lK2t7IaFcm0LFHEgc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 h1:jYfy8UPmd+6kJW5YhY0L1/KftReOGxI/4NtVSTh9O/I= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16/go.mod h1:7ZfEPZxkW42Afq4uQB8H2E2e6ebh6mXTueEpYzjCzcs= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= github.com/aws/aws-sdk-go-v2/service/ecr v1.29.1 h1:ywNLJrn/Qn4enDsz/XnKlvpnLqvJxFGQV2BltWltbis= github.com/aws/aws-sdk-go-v2/service/ecr v1.29.1/go.mod h1:WadVIk+UrTvWuAsCp6BKGX4i2snurpz8mPWhJQnS7Dg= github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.24.1 h1:Eq9i/mvOlGghiKe9NtsmeD9Wlwg8p4fbsqrMb3nWirM= github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.24.1/go.mod h1:VtOgEoLEPV1YADuq+Z2XOK6/wKkGW2YK6DjChZ/GvDs= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18/go.mod h1:++NHzT+nAF7ZPrHPsA+ENvsXkOO8wEu+C6RXltAG4/c= github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 h1:UPTdlTOwWUX49fVi7cymEN6hDqCwe3LNv1vi7TXUutk= github.com/aws/aws-sdk-go-v2/service/kms v1.35.3/go.mod h1:gjDP16zn+WWalyaUqwCCioQ8gU8lzttCCc9jYsiQI/8= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= -github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= -github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 h1:zCsFCKvbj25i7p1u94imVoO447I/sFv8qq+lGJhRN0c= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.5/go.mod h1:ZeDX1SnKsVlejeuz41GiajjZpRSWR7/42q/EyA/QEiM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 h1:SKvPgvdvmiTWoi0GAJ7AsJfOz3ngVkD/ERbs5pUnHNI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5/go.mod h1:20sz31hv/WsPa3HhU3hfrIet2kxM4Pe0r20eBZ20Tac= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.4 h1:iAckBT2OeEK/kBDyN/jDtpEExhjeeA/Im2q4X0rJZT8= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.4/go.mod h1:vmSqFK+BVIwVpDAGZB3CoCXHzurt4qBE8lf+I/kRTh0= +github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4= +github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8 h1:SoFYaT9UyGkR0+nogNyD/Lj+bsixB+SNuAS4ABlEs6M= github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8/go.mod h1:2JF49jcDOrLStIXN/j/K1EKRq8a8R2qRnlZA6/o/c7c= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= diff --git a/pkg/attest/verify.go b/pkg/attest/verify.go index c1d6540..d30082e 100644 --- a/pkg/attest/verify.go +++ b/pkg/attest/verify.go @@ -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, @@ -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, }, diff --git a/pkg/attest/verify_test.go b/pkg/attest/verify_test.go index abf8048..b386619 100644 --- a/pkg/attest/verify_test.go +++ b/pkg/attest/verify_test.go @@ -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) { @@ -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) { diff --git a/pkg/attestation/vsa.go b/pkg/attestation/vsa.go index 0e5804e..e580601 100644 --- a/pkg/attestation/vsa.go +++ b/pkg/attestation/vsa.go @@ -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"` VerificationResult string `json:"verificationResult"` VerifiedLevels []string `json:"verifiedLevels"` } @@ -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 { diff --git a/pkg/config/config.go b/pkg/config/config.go index e724c2d..ae12702 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 df09e4c..e738d95 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 cfab255..6a82d15 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,24 @@ 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)} + } + } + if URI == "" { + return nil, fmt.Errorf("no policy file found in policy mapping") } policy := &Policy{ InputFiles: files, Mapping: mapping, + URI: URI, + Digest: digest, } if imageName != 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) { + 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} + } + } + if URI == "" { + return nil, fmt.Errorf("no policy file found in policy mapping") } policy := &Policy{ InputFiles: files, Mapping: mapping, + URI: URI, + Digest: digest, } if imageName != matchedName { policy.ResolvedName = matchedName diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index f185f7e..b282daf 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -32,7 +32,7 @@ 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" TestDataPath := filepath.Join("..", "..", "test", "testdata") ExampleAttestation := filepath.Join(TestDataPath, "example_attestation.json") @@ -43,22 +43,23 @@ 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 }{ {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, resolveErrorStr: "no policy file found in policy mapping"}, } for _, tc := range testCases { @@ -86,9 +87,9 @@ 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") diff --git a/pkg/policy/testdata/mock-tuf-no-rego/doi/policy.yaml b/pkg/policy/testdata/mock-tuf-no-rego/doi/policy.yaml new file mode 100644 index 0000000..148070a --- /dev/null +++ b/pkg/policy/testdata/mock-tuf-no-rego/doi/policy.yaml @@ -0,0 +1 @@ +policy: "this is not rego" diff --git a/pkg/policy/testdata/mock-tuf-no-rego/mapping.yaml b/pkg/policy/testdata/mock-tuf-no-rego/mapping.yaml new file mode 100644 index 0000000..e71ba54 --- /dev/null +++ b/pkg/policy/testdata/mock-tuf-no-rego/mapping.yaml @@ -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 diff --git a/pkg/policy/types.go b/pkg/policy/types.go index 54cc993..a122264 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 bf9ddbc..b6e4449 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 ae79545..9e0f870 100644 --- a/pkg/tuf/mock.go +++ b/pkg/tuf/mock.go @@ -4,6 +4,8 @@ import ( "io" "os" "path/filepath" + + "github.com/docker/attest/internal/util" ) 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) { - 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() @@ -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() @@ -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 { diff --git a/pkg/tuf/tuf.go b/pkg/tuf/tuf.go index 14fee91..cfcc913 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,69 @@ func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string 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 { + // . + targetRemotePath = fmt.Sprintf("%s.%s", digest, baseName) + } else { + // /. + 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 // 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 ddd935b..934f4dc 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 f751d1e..3c96563 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) }