28 Commits

Author SHA1 Message Date
Jonny Stoten
aed959f858 fix: use a client pointing at Docker's TUF by default (#104)
`policy.Options` now contains the arguments to `tuf.Client`'s constructor rather than an actual Client. If these arguments are not provided, defaults pointing at Docker's TUF repo will be used. An actual TUF client can be passed in on the context (which is useful for testing). If this is not provided `attest.Verify` will create a TUF client using the options on `policy.Options`.

---------

Co-authored-by: Joel Kamp <joel.kamp@docker.com>
2024-08-23 09:33:30 +01:00
James Carnegie
802725caf0 feat: add purl details to policy inputs (#129) 2024-08-21 12:01:11 -05:00
Joel Kamp
9c3f267870 Merge pull request #126 from docker/dependabot/go_modules/go_modules-56f2e24de8
feat(deps): bump github.com/docker/docker from 27.1.0+incompatible to 27.1.1+incompatible in the go_modules group
2024-08-16 09:10:37 -05:00
Joel Kamp
6cc9191e1e Merge branch 'main' into dependabot/go_modules/go_modules-56f2e24de8 2024-08-16 09:06:27 -05:00
Joel Kamp
7ce2817111 Merge pull request #123 from docker/dependabot/go_modules/google.golang.org/api-0.192.0
feat(deps): bump google.golang.org/api from 0.191.0 to 0.192.0
2024-08-16 09:06:00 -05:00
dependabot[bot]
a60aab9338 feat(deps): bump github.com/docker/docker in the go_modules group
Bumps the go_modules group with 1 update: [github.com/docker/docker](https://github.com/docker/docker).


Updates `github.com/docker/docker` from 27.1.0+incompatible to 27.1.1+incompatible
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v27.1.0...v27.1.1)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-16 14:05:57 +00:00
Joel Kamp
2ef3a158ae Merge branch 'main' into dependabot/go_modules/google.golang.org/api-0.192.0 2024-08-16 09:04:20 -05:00
Joel Kamp
4f163f4283 Merge pull request #125 from docker/dependabot/go_modules/github.com/aws/aws-sdk-go-v2/config-1.27.28
feat(deps): bump github.com/aws/aws-sdk-go-v2/config from 1.27.27 to 1.27.28
2024-08-16 09:03:58 -05:00
dependabot[bot]
74e8d8beb3 feat(deps): bump github.com/aws/aws-sdk-go-v2/config
Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.27.27 to 1.27.28.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.27.27...config/v1.27.28)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/config
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-16 08:19:46 +00:00
Joel Kamp
a4a0bf3cbe Merge pull request #124 from docker/feat-generate-vsa-policy-uri
feat: add `digest` and `downloadLocation` to VSA policy
2024-08-14 16:50:16 -05:00
mrjoelkamp
52499053d2 feat: add no policy file error 2024-08-14 16:25:41 -05:00
mrjoelkamp
5f17f97229 test: change test to use yaml file instead 2024-08-14 16:13:36 -05:00
mrjoelkamp
8d8f09661f test: add mapping no rego test 2024-08-14 16:10:54 -05:00
mrjoelkamp
059ee8926c refactor: move fullURL only needed for DefaultFetcher 2024-08-14 15:27:02 -05:00
mrjoelkamp
cb47507650 chore: pr comments 2024-08-14 15:01:01 -05:00
Joel Kamp
7c0966de81 Update README.md
Co-authored-by: David Dooling <141646279+whalelines@users.noreply.github.com>
2024-08-14 14:39:06 -05:00
mrjoelkamp
2bf7dec72e feat: add policy.downloadLocation 2024-08-14 12:52:36 -05:00
mrjoelkamp
6de792c1b5 docs: update README with policy.digest 2024-08-14 11:33:15 -05:00
mrjoelkamp
d2a8348ae8 feat: generate vsa policy value from file 2024-08-14 10:57:15 -05:00
dependabot[bot]
881e9d9582 feat(deps): bump google.golang.org/api from 0.191.0 to 0.192.0
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.191.0 to 0.192.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.191.0...v0.192.0)

---
updated-dependencies:
- dependency-name: google.golang.org/api
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-14 08:58:17 +00:00
Joel Kamp
8c6df28540 Merge pull request #122 from docker/feat-mirror-empty-config-image
feat: mirror empty config image
2024-08-13 10:09:35 -05:00
mrjoelkamp
5162cfa404 refactor: ensure tests are in correct pkg 2024-08-13 10:03:33 -05:00
mrjoelkamp
72f6517b2c refactor: move empty config image test 2024-08-13 08:26:36 -05:00
mrjoelkamp
84cadeb97e feat: output comments 2024-08-13 08:13:27 -05:00
mrjoelkamp
57a61cc266 fix: e2e auth test 2024-08-12 16:54:44 -05:00
mrjoelkamp
5a772633b0 feat: use EmptyConfigImage for mirror 2024-08-12 16:43:42 -05:00
mrjoelkamp
1febc55a19 fix: cyclical imports 2024-08-12 16:36:18 -05:00
mrjoelkamp
0db96d56aa fix: err check not needed 2024-08-12 14:20:24 -05:00
70 changed files with 1246 additions and 855 deletions

View File

@@ -50,7 +50,7 @@ jobs:
token: ${{ secrets.TC_CLOUD_TOKEN }} token: ${{ secrets.TC_CLOUD_TOKEN }}
- name: go test including e2e - name: go test including e2e
if: matrix.os == 'ubuntu-latest' && github.actor != 'dependabot[bot]' if: matrix.os == 'ubuntu-latest' && github.actor != 'dependabot[bot]'
run: go test -tags=e2e -v ./... -coverprofile=coverage.out -covermode=atomic run: go test -tags=e2e -v ./... -coverpkg=./... -coverprofile=coverage.out -covermode=atomic
- name: go test excluding e2e - name: go test excluding e2e
if: matrix.os == 'macos-latest' || github.actor == 'dependabot[bot]' if: matrix.os == 'macos-latest' || github.actor == 'dependabot[bot]'
run: go test -v ./... run: go test -v ./...

View File

@@ -128,7 +128,10 @@ The input to the policy is an object with the following fields:
- `digest` (string): the digest of the image being verified - `digest` (string): the digest of the image being verified
- `purl` (string): the package URL of the image being verified - `purl` (string): the package URL of the image being verified
- `is_canonical` (bool): whether the image being verified was referenced by a 'canonical' name, i.e. one that contains a digest - `platform` (string): the platform of the image being verified
- `normalized_name` (string): defaults are filled out. e.g. if the image is `alpine`, this would be `library/alpine`
- `familiar_name` (string): short version of above (e.g. `alpine`)
- `tag`: (string): tag of the image being verified (if present)
### Builtin Functions ### Builtin Functions
@@ -345,7 +348,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"]

32
go.mod
View File

@@ -4,7 +4,7 @@ go 1.22.5
require ( require (
github.com/Masterminds/semver/v3 v3.2.1 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/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8
github.com/containerd/platforms v0.2.1 github.com/containerd/platforms v0.2.1
github.com/distribution/reference v0.6.0 github.com/distribution/reference v0.6.0
@@ -24,7 +24,7 @@ require (
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/testcontainers/testcontainers-go/modules/registry v0.32.0 github.com/testcontainers/testcontainers-go/modules/registry v0.32.0
github.com/theupdateframework/go-tuf/v2 v2.0.0 github.com/theupdateframework/go-tuf/v2 v2.0.0
google.golang.org/api v0.191.0 google.golang.org/api v0.192.0
sigs.k8s.io/yaml v1.4.0 sigs.k8s.io/yaml v1.4.0
) )
@@ -33,7 +33,7 @@ replace github.com/google/go-containerregistry => github.com/docker/go-container
require ( require (
cloud.google.com/go v0.115.0 // indirect cloud.google.com/go v0.115.0 // indirect
cloud.google.com/go/auth v0.7.3 // indirect cloud.google.com/go/auth v0.8.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
cloud.google.com/go/compute/metadata v0.5.0 // indirect cloud.google.com/go/compute/metadata v0.5.0 // indirect
cloud.google.com/go/iam v1.1.12 // indirect cloud.google.com/go/iam v1.1.12 // indirect
@@ -47,21 +47,21 @@ require (
github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/agnivade/levenshtein v1.1.1 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // 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 v1.30.4 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // 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.11 // 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.15 // 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.15 // 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.0 // 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/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/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/accept-encoding v1.11.4 // 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/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/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/sso v1.22.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // 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.3 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.30.4 // indirect
github.com/aws/smithy-go v1.20.3 // indirect github.com/aws/smithy-go v1.20.4 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
@@ -77,7 +77,7 @@ require (
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect
github.com/docker/cli v27.1.1+incompatible // indirect github.com/docker/cli v27.1.1+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v27.1.0+incompatible // indirect github.com/docker/docker v27.1.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.1 // indirect github.com/docker/docker-credential-helpers v0.8.1 // indirect
github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect

64
go.sum
View File

@@ -1,8 +1,8 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
cloud.google.com/go/auth v0.7.3 h1:98Vr+5jMaCZ5NZk6e/uBgf60phTk/XN84r8QEWB9yjY= cloud.google.com/go/auth v0.8.1 h1:QZW9FjC5lZzN864p13YxvAtGUlQ+KgRL+8Sg45Z6vxo=
cloud.google.com/go/auth v0.7.3/go.mod h1:HJtWUx1P5eqjy/f6Iq5KeytNpbAcGolPhOgyop2LlzA= cloud.google.com/go/auth v0.8.1/go.mod h1:qGVp/Y3kDRSDZ5gFD/XPUfYQ9xW1iI7q8RIRoCyBbJc=
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI= cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I= cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
@@ -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/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 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= 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.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8=
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= 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.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90= 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.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg= 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.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI= 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.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4= 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.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw= 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.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU= 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.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU= 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.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8= 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.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI= 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.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM= 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.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= 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.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= 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 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/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 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/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.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI=
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/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
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.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ=
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/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 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/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.5 h1:zCsFCKvbj25i7p1u94imVoO447I/sFv8qq+lGJhRN0c=
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/sso v1.22.5/go.mod h1:ZeDX1SnKsVlejeuz41GiajjZpRSWR7/42q/EyA/QEiM=
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.5 h1:SKvPgvdvmiTWoi0GAJ7AsJfOz3ngVkD/ERbs5pUnHNI=
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/ssooidc v1.26.5/go.mod h1:20sz31hv/WsPa3HhU3hfrIet2kxM4Pe0r20eBZ20Tac=
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.4 h1:iAckBT2OeEK/kBDyN/jDtpEExhjeeA/Im2q4X0rJZT8=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= 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.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= 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 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/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= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -222,8 +222,8 @@ github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2
github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v27.1.0+incompatible h1:rEHVQc4GZ0MIQKifQPHSFGV/dVgaZafgRf8fCPtDYBs= github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
github.com/docker/docker v27.1.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo= github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
@@ -809,8 +809,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.191.0 h1:cJcF09Z+4HAB2t5qTQM1ZtfL/PemsLFkcFG67qq2afk= google.golang.org/api v0.192.0 h1:PljqpNAfZaaSpS+TnANfnNAXKdzHM/B9bKhwRlo7JP0=
google.golang.org/api v0.191.0/go.mod h1:tD5dsFGxFza0hnQveGfVk9QQYKcfp+VzgRqyXFxE0+E= google.golang.org/api v0.192.0/go.mod h1:9VcphjvAxPKLmSxVSzPlSRXy/5ARMEw5bf58WoVXafQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=

View File

@@ -2,22 +2,13 @@ package test
import ( import (
"context" "context"
"encoding/base64"
"encoding/json"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/policy" "github.com/docker/attest/pkg/policy"
"github.com/docker/attest/pkg/signerverifier" "github.com/docker/attest/pkg/signerverifier"
"github.com/docker/attest/pkg/tlog" "github.com/docker/attest/pkg/tlog"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/google/go-containerregistry/pkg/v1/partial"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/secure-systems-lab/go-securesystemslib/dsse"
) )
@@ -30,6 +21,8 @@ const (
AWSKMSKeyARN = "arn:aws:kms:us-east-1:175142243308:alias/doi-signing" // sandbox AWSKMSKeyARN = "arn:aws:kms:us-east-1:175142243308:alias/doi-signing" // sandbox
) )
var UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
func CreateTempDir(t *testing.T, dir, pattern string) string { func CreateTempDir(t *testing.T, dir, pattern string) string {
// Create a temporary directory for output oci layout // Create a temporary directory for output oci layout
tempDir, err := os.MkdirTemp(dir, pattern) tempDir, err := os.MkdirTemp(dir, pattern)
@@ -89,108 +82,3 @@ func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) {
return ctx, signer return ctx, signer
} }
type AnnotatedStatement struct {
OCIDescriptor *v1.Descriptor
InTotoStatement *intoto.Statement
Annotations map[string]string
}
func ExtractStatementsFromIndex(idx v1.ImageIndex, mediaType string) ([]*AnnotatedStatement, error) {
mfs2, err := idx.IndexManifest()
if err != nil {
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
}
var statements []*AnnotatedStatement
for i := range mfs2.Manifests {
mf := &mfs2.Manifests[i]
if mf.Annotations[attestation.DockerReferenceType] != "attestation-manifest" {
continue
}
attestationImage, err := idx.Image(mf.Digest)
if err != nil {
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
}
layers, err := attestationImage.Layers()
if err != nil {
return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err)
}
for _, layer := range layers {
// parse layer blob as json
mt, err := layer.MediaType()
if err != nil {
return nil, fmt.Errorf("failed to get layer media type: %w", err)
}
if string(mt) != mediaType {
continue
}
r, err := layer.Uncompressed()
if err != nil {
return nil, fmt.Errorf("failed to get layer contents: %w", err)
}
defer r.Close()
inTotoStatement := new(intoto.Statement)
var desc *v1.Descriptor
if strings.HasSuffix(string(mt), "+dsse") {
env := new(attestation.Envelope)
err = json.NewDecoder(r).Decode(env)
if err != nil {
return nil, fmt.Errorf("failed to decode env: %w", err)
}
payload, err := base64.StdEncoding.Strict().DecodeString(env.Payload)
if err != nil {
return nil, fmt.Errorf("failed to decode payload: %w", err)
}
err = json.Unmarshal([]byte(payload), inTotoStatement)
if err != nil {
return nil, fmt.Errorf("failed to decode %s statement: %w", mediaType, err)
}
} else {
desc := new(v1.Descriptor)
err = json.NewDecoder(r).Decode(desc)
if err != nil {
return nil, fmt.Errorf("failed to decode statement: %w", err)
}
}
layerDesc, err := partial.Descriptor(layer)
if err != nil {
return nil, fmt.Errorf("failed to get descriptor for layer: %w", err)
}
annotations := make(map[string]string)
for k, v := range layerDesc.Annotations {
annotations[k] = v
}
statements = append(statements, &AnnotatedStatement{
OCIDescriptor: desc,
InTotoStatement: inTotoStatement,
Annotations: annotations,
})
}
}
return statements, nil
}
func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStatement, error) {
idx, err := layout.ImageIndexFromPath(path)
if err != nil {
return nil, fmt.Errorf("failed to load image index: %w", err)
}
idxm, err := idx.IndexManifest()
if err != nil {
return nil, fmt.Errorf("failed to get digest: %w", err)
}
idxDigest := idxm.Manifests[0].Digest
mfs, err := idx.ImageIndex(idxDigest)
if err != nil {
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
}
return ExtractStatementsFromIndex(mfs, mediaType)
}

2
pkg/attest/README.md Normal file
View File

@@ -0,0 +1,2 @@
## attest
This package implements the top-level signing and verification methods.

View File

@@ -6,24 +6,12 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/pkg/attest" "github.com/docker/attest/pkg/attest"
"github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy" "github.com/docker/attest/pkg/policy"
"github.com/docker/attest/pkg/tuf" "github.com/docker/attest/pkg/tuf"
) )
func createTufClient(outputPath string) (*tuf.Client, error) {
// using oci tuf metadata and targets
metadataURI := "registry-1.docker.io/docker/tuf-metadata:latest"
targetsURI := "registry-1.docker.io/docker/tuf-targets"
// example using http tuf metadata and targets
// metadataURI := "https://docker.github.io/tuf-staging/metadata"
// targetsURI := "https://docker.github.io/tuf-staging/targets"
return tuf.NewClient(embed.RootStaging.Data, outputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker())
}
func ExampleVerify_remote() { func ExampleVerify_remote() {
// create a tuf client // create a tuf client
home, err := os.UserHomeDir() home, err := os.UserHomeDir()
@@ -31,10 +19,7 @@ func ExampleVerify_remote() {
panic(err) panic(err)
} }
tufOutputPath := filepath.Join(home, ".docker", "tuf") tufOutputPath := filepath.Join(home, ".docker", "tuf")
tufClient, err := createTufClient(tufOutputPath) tufClientOpts := tuf.NewDockerDefaultClientOptions(tufOutputPath)
if err != nil {
panic(err)
}
// create a resolver for remote attestations // create a resolver for remote attestations
image := "registry-1.docker.io/library/notary:server" image := "registry-1.docker.io/library/notary:server"
@@ -42,10 +27,10 @@ func ExampleVerify_remote() {
// configure policy options // configure policy options
opts := &policy.Options{ opts := &policy.Options{
TUFClient: tufClient, TUFClientOptions: tufClientOpts,
LocalTargetsDir: filepath.Join(home, ".docker", "policy"), // location to store policy files downloaded from TUF LocalTargetsDir: filepath.Join(home, ".docker", "policy"), // location to store policy files downloaded from TUF
LocalPolicyDir: "", // overrides TUF policy for local policy files if set LocalPolicyDir: "", // overrides TUF policy for local policy files if set
PolicyID: "", // set to ignore policy mapping and select a policy by id PolicyID: "", // set to ignore policy mapping and select a policy by id
} }
src, err := oci.ParseImageSpec(image, oci.WithPlatform(platform)) src, err := oci.ParseImageSpec(image, oci.WithPlatform(platform))

View File

@@ -12,14 +12,14 @@ import (
// this is only relevant if there are (unsigned) in-toto statements. // this is only relevant if there are (unsigned) in-toto statements.
func SignStatements(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) ([]*attestation.Manifest, error) { func SignStatements(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) ([]*attestation.Manifest, error) {
// extract attestation manifests from index // extract attestation manifests from index
attestationManifests, err := attestation.GetAttestationManifestsFromIndex(idx) attestationManifests, err := attestation.ManifestsFromIndex(idx)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load attestation manifests from index: %w", err) return nil, fmt.Errorf("failed to load attestation manifests from index: %w", err)
} }
// sign every attestation layer in each manifest // sign every attestation layer in each manifest
for _, manifest := range attestationManifests { for _, manifest := range attestationManifests {
for _, layer := range manifest.OriginalLayers { for _, layer := range manifest.OriginalLayers {
err = manifest.AddAttestation(ctx, signer, layer.Statement, opts) err = manifest.Add(ctx, signer, layer.Statement, opts)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to sign attestation layer %w", err) return nil, fmt.Errorf("failed to sign attestation layer %w", err)
} }

View File

@@ -1,10 +1,6 @@
package attest package attest
import ( import (
"encoding/json"
"fmt"
"net/http/httptest"
"net/url"
"path/filepath" "path/filepath"
"testing" "testing"
@@ -12,10 +8,7 @@ import (
"github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy" "github.com/docker/attest/pkg/policy"
"github.com/google/go-containerregistry/pkg/registry" "github.com/docker/attest/pkg/tuf"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/static"
"github.com/google/go-containerregistry/pkg/v1/types"
intoto "github.com/in-toto/in-toto-golang/in_toto" intoto "github.com/in-toto/in-toto-golang/in_toto"
v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -23,17 +16,19 @@ import (
) )
var ( var (
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image") NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass") PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
PassMirrorPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-mirror") PassMirrorPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-mirror")
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl") PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail") FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
InputsPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-inputs")
EmptyPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-policies")
TestTempDir = "attest-sign-test" TestTempDir = "attest-sign-test"
) )
func TestSignVerifyOCILayout(t *testing.T) { func TestSignVerifyOCILayout(t *testing.T) {
ctx, signer := test.Setup(t) ctx, signer := test.Setup(t)
ctx = tuf.WithDownloader(ctx, tuf.NewMockTufClient(EmptyPolicyDir, test.CreateTempDir(t, "", "tuf-dest")))
testCases := []struct { testCases := []struct {
name string name string
@@ -42,8 +37,8 @@ func TestSignVerifyOCILayout(t *testing.T) {
expectedAttestations int expectedAttestations int
replace bool replace bool
}{ }{
{"signed replaced", UnsignedTestImage, 0, 4, true}, {"signed replaced", test.UnsignedTestImage, 0, 4, true},
{"without replace", UnsignedTestImage, 4, 4, false}, {"without replace", test.UnsignedTestImage, 4, 4, false},
// image without provenance doesn't fail // image without provenance doesn't fail
{"no provenance (replace)", NoProvenanceImage, 0, 2, true}, {"no provenance (replace)", NoProvenanceImage, 0, 2, true},
{"no provenance (no replace)", NoProvenanceImage, 2, 2, false}, {"no provenance (no replace)", NoProvenanceImage, 2, 2, false},
@@ -70,10 +65,10 @@ func TestSignVerifyOCILayout(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equalf(t, OutcomeSuccess, policy.Outcome, "Policy should have been found") assert.Equalf(t, OutcomeSuccess, policy.Outcome, "Policy should have been found")
var allEnvelopes []*test.AnnotatedStatement var allEnvelopes []*attestation.AnnotatedStatement
for _, predicate := range []string{intoto.PredicateSPDX, v02.PredicateSLSAProvenance, attestation.VSAPredicateType} { for _, predicate := range []string{intoto.PredicateSPDX, v02.PredicateSLSAProvenance, attestation.VSAPredicateType} {
mt, _ := attestation.DSSEMediaType(predicate) mt, _ := attestation.DSSEMediaType(predicate)
statements, err := test.ExtractAnnotatedStatements(outputLayout, mt) statements, err := attestation.ExtractAnnotatedStatements(outputLayout, mt)
require.NoError(t, err) require.NoError(t, err)
allEnvelopes = append(allEnvelopes, statements...) allEnvelopes = append(allEnvelopes, statements...)
@@ -83,150 +78,9 @@ func TestSignVerifyOCILayout(t *testing.T) {
} }
} }
assert.Equalf(t, tc.expectedAttestations, len(allEnvelopes), "expected %d attestations, got %d", tc.expectedAttestations, len(allEnvelopes)) assert.Equalf(t, tc.expectedAttestations, len(allEnvelopes), "expected %d attestations, got %d", tc.expectedAttestations, len(allEnvelopes))
statements, err := test.ExtractAnnotatedStatements(outputLayout, intoto.PayloadType) statements, err := attestation.ExtractAnnotatedStatements(outputLayout, intoto.PayloadType)
require.NoError(t, err) require.NoError(t, err)
assert.Equalf(t, tc.expectedStatements, len(statements), "expected %d statement, got %d", tc.expectedStatements, len(statements)) assert.Equalf(t, tc.expectedStatements, len(statements), "expected %d statement, got %d", tc.expectedStatements, len(statements))
}) })
} }
} }
func TestAddSignedLayerAnnotations(t *testing.T) {
ctx, signer := test.Setup(t)
testCases := []struct {
name string
replace bool
}{
{"replaced", true},
{"not replaced", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
data := []byte("signed")
testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType))
mediaType := types.OCIManifestSchema1
opts := &attestation.SigningOptions{}
originalLayer := &attestation.Layer{
Layer: testLayer,
Statement: &intoto.Statement{
StatementHeader: intoto.StatementHeader{
PredicateType: attestation.VSAPredicateType,
},
},
Annotations: map[string]string{"test": "test"},
}
manifest := &attestation.Manifest{
OriginalDescriptor: &v1.Descriptor{
MediaType: mediaType,
},
OriginalLayers: []*attestation.Layer{
originalLayer,
},
SubjectDescriptor: &v1.Descriptor{},
}
err := manifest.AddAttestation(ctx, signer, originalLayer.Statement, opts)
require.NoError(t, err)
newImg, err := manifest.BuildAttestationImage(attestation.WithReplacedLayers(tc.replace))
require.NoError(t, err)
mf, _ := newImg.RawManifest()
type Annotations struct {
Annotations map[string]string `json:"annotations"`
}
type Layers struct {
Layers []Annotations `json:"layers"`
}
l := &Layers{}
err = json.Unmarshal(mf, l)
require.NoError(t, err)
_, ok := l.Layers[0].Annotations["test"]
assert.Truef(t, ok, "missing annotations")
})
}
}
func TestSimpleStatementSigning(t *testing.T) {
ctx, signer := test.Setup(t)
empty := types.MediaType("application/vnd.oci.empty.v1+json")
testCases := []struct {
name string
replace bool
}{
{"replaced", true},
{"not replaced", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
opts := &attestation.SigningOptions{}
statement := &intoto.Statement{
StatementHeader: intoto.StatementHeader{
PredicateType: attestation.VSAPredicateType,
},
}
statement2 := &intoto.Statement{
StatementHeader: intoto.StatementHeader{
PredicateType: attestation.VSAPredicateType,
},
}
digest, err := v1.NewHash("sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620")
require.NoError(t, err)
subject := &v1.Descriptor{
MediaType: "application/vnd.oci.image.manifest.v1+json",
Digest: digest,
}
manifest, err := NewAttestationManifest(subject)
require.NoError(t, err)
err = manifest.AddAttestation(ctx, signer, statement, opts)
require.NoError(t, err)
err = manifest.AddAttestation(ctx, signer, statement2, opts)
require.NoError(t, err)
// fake that the manfifest was loaded from a real image
manifest.OriginalLayers = manifest.SignedLayers
envelopes, err := oci.ExtractEnvelopes(manifest, attestation.VSAPredicateType)
require.NoError(t, err)
assert.Len(t, envelopes, 2)
newImg, err := manifest.BuildAttestationImage(attestation.WithReplacedLayers(tc.replace))
require.NoError(t, err)
layers, err := newImg.Layers()
require.NoError(t, err)
if tc.replace {
assert.Len(t, layers, 2)
} else {
assert.Len(t, layers, 4)
}
newImgs, err := manifest.BuildReferringArtifacts()
require.NoError(t, err)
assert.Len(t, newImgs, 2)
for _, img := range newImgs {
mf, err := img.Manifest()
require.NoError(t, err)
assert.Contains(t, mf.ArtifactType, "application/vnd.in-toto")
assert.Contains(t, mf.ArtifactType, "+dsse")
assert.Equal(t, subject.MediaType, mf.MediaType)
assert.Equal(t, empty, mf.Config.MediaType)
assert.Equal(t, int64(2), mf.Config.Size)
assert.Equal(t, "{}", string(mf.Config.Data))
layers, err := img.Layers()
require.NoError(t, err)
assert.Len(t, layers, 1)
}
server := httptest.NewServer(registry.New(registry.WithReferrersSupport(true)))
defer server.Close()
u, err := url.Parse(server.URL)
require.NoError(t, err)
indexName := fmt.Sprintf("%s/repo:root", u.Host)
output, err := oci.ParseImageSpecs(indexName)
require.NoError(t, err)
err = oci.SaveReferrers(manifest, output)
require.NoError(t, err)
})
}
}

View File

@@ -3,6 +3,8 @@ package attest
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"path/filepath"
"strings" "strings"
"time" "time"
@@ -11,7 +13,7 @@ import (
"github.com/docker/attest/pkg/config" "github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy" "github.com/docker/attest/pkg/policy"
v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/docker/attest/pkg/tuf"
intoto "github.com/in-toto/in-toto-golang/in_toto" intoto "github.com/in-toto/in-toto-golang/in_toto"
) )
@@ -21,13 +23,20 @@ func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.Options) (resu
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create image details resolver: %w", err) return nil, fmt.Errorf("failed to create image details resolver: %w", err)
} }
if opts.AttestationStyle == "" { err = populateDefaultOptions(opts)
opts.AttestationStyle = config.AttestationStyleReferrers if err != nil {
return nil, err
} }
if opts.ReferrersRepo != "" && opts.AttestationStyle != config.AttestationStyleReferrers {
return nil, fmt.Errorf("referrers repo specified but attestation source not set to referrers") tufClient, ok := tuf.GetDownloader(ctx)
if !ok {
tufClient, err = tuf.NewClient(opts.TUFClientOptions)
if err != nil {
return nil, fmt.Errorf("failed to create TUF client: %w", err)
}
} }
pctx, err := policy.ResolvePolicy(ctx, detailsResolver, opts)
pctx, err := policy.ResolvePolicy(ctx, tufClient, detailsResolver, opts)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to resolve policy: %w", err) return nil, fmt.Errorf("failed to resolve policy: %w", err)
} }
@@ -61,6 +70,36 @@ func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.Options) (resu
return result, nil return result, nil
} }
func populateDefaultOptions(opts *policy.Options) (err error) {
if opts.LocalTargetsDir == "" {
opts.LocalTargetsDir, err = defaultLocalTargetsDir()
if err != nil {
return err
}
}
if opts.TUFClientOptions == nil {
opts.TUFClientOptions = tuf.NewDockerDefaultClientOptions(opts.LocalTargetsDir)
}
if opts.AttestationStyle == "" {
opts.AttestationStyle = config.AttestationStyleReferrers
}
if opts.ReferrersRepo != "" && opts.AttestationStyle != config.AttestationStyleReferrers {
return fmt.Errorf("referrers repo specified but attestation source not set to referrers")
}
return nil
}
func defaultLocalTargetsDir() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get user home directory: %w", err)
}
return filepath.Join(homeDir, ".docker", "tuf"), nil
}
func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.Result) (*VerificationResult, error) { func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.Result) (*VerificationResult, error) {
dgst, err := oci.SplitDigest(input.Digest) dgst, err := oci.SplitDigest(input.Digest)
if err != nil { if err != nil {
@@ -87,6 +126,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,
@@ -104,7 +145,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 +153,7 @@ func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.
}, nil }, nil
} }
func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, pctx *policy.Policy) (*VerificationResult, error) { func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, pctx *policy.Policy) (*VerificationResult, error) {
desc, err := resolver.ImageDescriptor(ctx) desc, err := resolver.ImageDescriptor(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get image descriptor: %w", err) return nil, fmt.Errorf("failed to get image descriptor: %w", err)
@@ -139,14 +180,34 @@ func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, p
name = strings.Replace(name, oldName, pctx.ResolvedName, 1) name = strings.Replace(name, oldName, pctx.ResolvedName, 1)
} }
purl, canonical, err := oci.RefToPURL(name, platform) ref, err := reference.ParseNormalizedNamed(name)
if err != nil {
return nil, fmt.Errorf("failed to parse ref %q: %w", ref, err)
}
purl, canonical, err := oci.RefToPURL(ref, platform)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to convert ref to purl: %w", err) return nil, fmt.Errorf("failed to convert ref to purl: %w", err)
} }
var tag string
if !canonical {
// unlike the function name indicates, this adds latest if no tag is present
ref = reference.TagNameOnly(ref)
}
if tagged, ok := ref.(reference.Tagged); ok {
tag = tagged.Tag()
}
input := &policy.Input{ input := &policy.Input{
Digest: digest, Digest: digest,
PURL: purl, PURL: purl,
IsCanonical: canonical, Platform: platform.String(),
Domain: reference.Domain(ref),
NormalizedName: reference.Path(ref),
FamiliarName: reference.FamiliarName(ref),
}
// rego has null strings
if tag != "" {
input.Tag = tag
} }
evaluator, err := policy.GetPolicyEvaluator(ctx) evaluator, err := policy.GetPolicyEvaluator(ctx)
@@ -164,13 +225,3 @@ func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, p
verificationResult.SubjectDescriptor = desc verificationResult.SubjectDescriptor = desc
return verificationResult, nil return verificationResult, nil
} }
func NewAttestationManifest(subject *v1.Descriptor) (*attestation.Manifest, error) {
return &attestation.Manifest{
OriginalDescriptor: &v1.Descriptor{
MediaType: "application/vnd.oci.image.manifest.v1+json",
},
OriginalLayers: []*attestation.Layer{},
SubjectDescriptor: subject,
}, nil
}

View File

@@ -8,11 +8,13 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/distribution/reference"
"github.com/docker/attest/internal/test" "github.com/docker/attest/internal/test"
"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"
"github.com/docker/attest/pkg/policy" "github.com/docker/attest/pkg/policy"
"github.com/docker/attest/pkg/tuf"
intoto "github.com/in-toto/in-toto-golang/in_toto" intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -31,7 +33,7 @@ func TestVerifyAttestations(t *testing.T) {
env := new(attestation.Envelope) env := new(attestation.Envelope)
err = json.Unmarshal(ex, env) err = json.Unmarshal(ex, env)
assert.NoError(t, err) assert.NoError(t, err)
resolver := &oci.MockResolver{ resolver := &attestation.MockResolver{
Envs: []*attestation.Envelope{env}, Envs: []*attestation.Envelope{env},
} }
@@ -47,7 +49,7 @@ func TestVerifyAttestations(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
mockPE := policy.MockPolicyEvaluator{ mockPE := policy.MockPolicyEvaluator{
EvaluateFunc: func(_ context.Context, _ oci.AttestationResolver, _ *policy.Policy, _ *policy.Input) (*policy.Result, error) { EvaluateFunc: func(_ context.Context, _ attestation.Resolver, _ *policy.Policy, _ *policy.Input) (*policy.Result, error) {
return policy.AllowedResult(), tc.policyEvaluationError return policy.AllowedResult(), tc.policyEvaluationError
}, },
} }
@@ -68,11 +70,12 @@ func TestVerifyAttestations(t *testing.T) {
func TestVSA(t *testing.T) { func TestVSA(t *testing.T) {
ctx, signer := test.Setup(t) ctx, signer := test.Setup(t)
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true)) ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
ctx = tuf.WithDownloader(ctx, tuf.NewMockTufClient(EmptyPolicyDir, test.CreateTempDir(t, "", "tuf-dest")))
// setup an image with signed attestations // setup an image with signed attestations
outputLayout := test.CreateTempDir(t, "", TestTempDir) outputLayout := test.CreateTempDir(t, "", TestTempDir)
opts := &attestation.SigningOptions{} opts := &attestation.SigningOptions{}
attIdx, err := oci.IndexFromPath(UnsignedTestImage) attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
assert.NoError(t, err) assert.NoError(t, err)
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts) signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err) require.NoError(t, err)
@@ -98,7 +101,7 @@ func TestVSA(t *testing.T) {
if assert.NotNil(t, results.Input) { if assert.NotNil(t, results.Input) {
assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", results.Input.Digest) assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", results.Input.Digest)
assert.False(t, results.Input.IsCanonical) assert.NotNil(t, results.Input.Tag)
} }
assert.Equal(t, intoto.StatementInTotoV01, results.VSA.Type) assert.Equal(t, intoto.StatementInTotoV01, results.VSA.Type)
@@ -112,17 +115,20 @@ 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) {
ctx, signer := test.Setup(t) ctx, signer := test.Setup(t)
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true)) ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
ctx = tuf.WithDownloader(ctx, tuf.NewMockTufClient(EmptyPolicyDir, test.CreateTempDir(t, "", "tuf-dest")))
// setup an image with signed attestations // setup an image with signed attestations
outputLayout := test.CreateTempDir(t, "", TestTempDir) outputLayout := test.CreateTempDir(t, "", TestTempDir)
opts := &attestation.SigningOptions{} opts := &attestation.SigningOptions{}
attIdx, err := oci.IndexFromPath(UnsignedTestImage) attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
assert.NoError(t, err) assert.NoError(t, err)
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts) signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err) require.NoError(t, err)
@@ -162,30 +168,35 @@ 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) {
ctx, signer := test.Setup(t) ctx, signer := test.Setup(t)
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true)) ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
ctx = tuf.WithDownloader(ctx, tuf.NewMockTufClient(EmptyPolicyDir, test.CreateTempDir(t, "", "tuf-dest")))
// setup an image with signed attestations // setup an image with signed attestations
outputLayout := test.CreateTempDir(t, "", TestTempDir) outputLayout := test.CreateTempDir(t, "", TestTempDir)
testCases := []struct { testCases := []struct {
name string name string
signTL bool signTL bool
policyDir string policyDir string
imageName string imageName string
expectError bool
expectedNonSuccess Outcome
}{ }{
{name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir}, {name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir},
{name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir}, {name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir},
{name: "no tl", signTL: false, policyDir: PassPolicyDir}, {name: "no tl", signTL: false, policyDir: PassPolicyDir},
{name: "mirror", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "mirror.org/library/test-image:test"}, {name: "mirror", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "mirror.org/library/test-image:test"},
{name: "mirror no match", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectError: true}, {name: "mirror no match", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectedNonSuccess: OutcomeNoPolicy},
{name: "verify inputs", signTL: false, policyDir: InputsPolicyDir},
} }
attIdx, err := oci.IndexFromPath(UnsignedTestImage) attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
assert.NoError(t, err) assert.NoError(t, err)
for _, tc := range testCases { for _, tc := range testCases {
@@ -214,17 +225,84 @@ func TestSignVerify(t *testing.T) {
LocalPolicyDir: tc.policyDir, LocalPolicyDir: tc.policyDir,
} }
results, err := Verify(ctx, spec, policyOpts) results, err := Verify(ctx, spec, policyOpts)
if tc.expectError { require.NoError(t, err)
require.Error(t, err) if tc.expectedNonSuccess != "" {
assert.Equal(t, tc.expectedNonSuccess, results.Outcome)
return return
} }
require.NoError(t, err)
assert.Equal(t, OutcomeSuccess, results.Outcome) assert.Equal(t, OutcomeSuccess, results.Outcome)
platform, err := oci.ParsePlatform(LinuxAMD64) platform, err := oci.ParsePlatform(LinuxAMD64)
require.NoError(t, err) require.NoError(t, err)
expectedPURL, _, err := oci.RefToPURL(attIdx.Name, platform)
ref, err := reference.ParseNormalizedNamed(attIdx.Name)
require.NoError(t, err)
expectedPURL, _, err := oci.RefToPURL(ref, platform)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, expectedPURL, results.Input.PURL) assert.Equal(t, expectedPURL, results.Input.PURL)
}) })
} }
} }
func TestDefaultOptions(t *testing.T) {
testCases := []struct {
name string
tufOpts *tuf.ClientOptions
localTargetsDir string
attestationStyle config.AttestationStyle
referrersRepo string
expectedError string
}{
{name: "empty"},
{name: "tufClient provided", tufOpts: &tuf.ClientOptions{MetadataSource: "a", TargetsSource: "b"}},
{name: "localTargetsDir provided", localTargetsDir: test.CreateTempDir(t, "", TestTempDir)},
{name: "attestationStyle provided", attestationStyle: config.AttestationStyleAttached},
{name: "referrersRepo provided", referrersRepo: "referrers"},
{name: "referrersRepo provided with attached", referrersRepo: "referrers", attestationStyle: config.AttestationStyleAttached, expectedError: "referrers repo specified but attestation source not set to referrers"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
defaultTargets, err := defaultLocalTargetsDir()
require.NoError(t, err)
opts := &policy.Options{
TUFClientOptions: tc.tufOpts,
LocalTargetsDir: tc.localTargetsDir,
AttestationStyle: tc.attestationStyle,
ReferrersRepo: tc.referrersRepo,
}
err = populateDefaultOptions(opts)
if tc.expectedError != "" {
require.Error(t, err)
assert.Equal(t, tc.expectedError, err.Error())
return
}
require.NoError(t, err)
if tc.localTargetsDir != "" {
assert.Equal(t, tc.localTargetsDir, opts.LocalTargetsDir)
} else {
assert.Equal(t, defaultTargets, opts.LocalTargetsDir)
}
if tc.attestationStyle != "" {
assert.Equal(t, tc.attestationStyle, opts.AttestationStyle)
} else {
assert.Equal(t, config.AttestationStyleReferrers, opts.AttestationStyle)
}
if tc.tufOpts != nil {
assert.Equal(t, tc.tufOpts, opts.TUFClientOptions)
} else {
assert.NotNil(t, opts.TUFClientOptions)
}
if tc.referrersRepo != "" {
assert.Equal(t, tc.referrersRepo, opts.ReferrersRepo)
} else {
assert.Empty(t, opts.ReferrersRepo)
}
})
}
}

View File

@@ -0,0 +1,4 @@
## attestations
This package is for components that deal with the creation, storage, and retrieval of signed attestions using OCI.
For more generic OCI components see the `oci` package.

View File

@@ -1,14 +1,17 @@
package attestation package attestation
import ( import (
"bytes"
"context" "context"
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"maps" "maps"
"strings"
"github.com/docker/attest/pkg/oci"
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/google/go-containerregistry/pkg/v1/match" "github.com/google/go-containerregistry/pkg/v1/match"
"github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/partial" "github.com/google/go-containerregistry/pkg/v1/partial"
@@ -18,8 +21,19 @@ import (
"github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/secure-systems-lab/go-securesystemslib/dsse"
) )
// GetAttestationManifestsFromIndex extracts all attestation manifests from an index. // NewManifest creates a new attestation manifest from a descriptor.
func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, error) { func NewManifest(subject *v1.Descriptor) (*Manifest, error) {
return &Manifest{
OriginalDescriptor: &v1.Descriptor{
MediaType: "application/vnd.oci.image.manifest.v1+json",
},
OriginalLayers: []*Layer{},
SubjectDescriptor: subject,
}, nil
}
// ManifestsFromIndex extracts all attestation manifests from an index.
func ManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, error) {
idx, err := index.IndexManifest() idx, err := index.IndexManifest()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err) return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
@@ -42,7 +56,7 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, error)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", desc.Digest.String(), err) return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", desc.Digest.String(), err)
} }
attestationLayers, err := GetAttestationsFromImage(attestationImage) attestationLayers, err := layersFromImage(attestationImage)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get attestations from image: %w", err) return nil, fmt.Errorf("failed to get attestations from image: %w", err)
} }
@@ -57,8 +71,8 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, error)
return attestationManifests, nil return attestationManifests, nil
} }
// GetAttestationsFromImage extracts all attestation layers from an image. // LayersFromImage extracts all attestation layers from an image.
func GetAttestationsFromImage(image v1.Image) ([]*Layer, error) { func layersFromImage(image v1.Image) ([]*Layer, error) {
layers, err := image.Layers() layers, err := image.Layers()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to extract layers from image: %w", err) return nil, fmt.Errorf("failed to extract layers from image: %w", err)
@@ -94,7 +108,7 @@ func GetAttestationsFromImage(image v1.Image) ([]*Layer, error) {
return attestationLayers, nil return attestationLayers, nil
} }
func (manifest *Manifest) AddAttestation(ctx context.Context, signer dsse.SignerVerifier, statement *intoto.Statement, opts *SigningOptions) error { func (manifest *Manifest) Add(ctx context.Context, signer dsse.SignerVerifier, statement *intoto.Statement, opts *SigningOptions) error {
layer, err := createSignedImageLayer(ctx, statement, signer, opts) layer, err := createSignedImageLayer(ctx, statement, signer, opts)
if err != nil { if err != nil {
return fmt.Errorf("failed to create signed layer: %w", err) return fmt.Errorf("failed to create signed layer: %w", err)
@@ -105,7 +119,7 @@ func (manifest *Manifest) AddAttestation(ctx context.Context, signer dsse.Signer
func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*Layer, error) { func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*Layer, error) {
// sign the statement // sign the statement
env, err := SignInTotoStatement(ctx, statement, signer, opts) env, err := signInTotoStatement(ctx, statement, signer, opts)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to sign statement: %w", err) return nil, fmt.Errorf("failed to sign statement: %w", err)
} }
@@ -128,7 +142,7 @@ func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, si
}, nil }, nil
} }
func SignInTotoStatement(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) { func signInTotoStatement(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) {
payload, err := json.Marshal(statement) payload, err := json.Marshal(statement)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to marshal statement: %w", err) return nil, fmt.Errorf("failed to marshal statement: %w", err)
@@ -140,12 +154,12 @@ func SignInTotoStatement(ctx context.Context, statement *intoto.Statement, signe
return env, nil return env, nil
} }
func UpdateIndexImage( func updateImageIndex(
idx v1.ImageIndex, idx v1.ImageIndex,
manifest *Manifest, manifest *Manifest,
options ...func(*ManifestImageOptions) error, options ...func(*ManifestImageOptions) error,
) (v1.ImageIndex, error) { ) (v1.ImageIndex, error) {
image, err := manifest.BuildAttestationImage(options...) image, err := manifest.BuildImage(options...)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to build image: %w", err) return nil, fmt.Errorf("failed to build image: %w", err)
} }
@@ -170,7 +184,7 @@ func UpdateIndexImage(
func UpdateIndexImages(idx v1.ImageIndex, manifest []*Manifest, options ...func(*ManifestImageOptions) error) (v1.ImageIndex, error) { func UpdateIndexImages(idx v1.ImageIndex, manifest []*Manifest, options ...func(*ManifestImageOptions) error) (v1.ImageIndex, error) {
var err error var err error
for _, m := range manifest { for _, m := range manifest {
idx, err = UpdateIndexImage(idx, m, options...) idx, err = updateImageIndex(idx, m, options...)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to add image to index: %w", err) return nil, fmt.Errorf("failed to add image to index: %w", err)
} }
@@ -204,7 +218,7 @@ func WithReplacedLayers(replaceLayers bool) func(*ManifestImageOptions) error {
} }
// build an image with signed attestations, optionally replacing existing layers with signed layers. // build an image with signed attestations, optionally replacing existing layers with signed layers.
func (manifest *Manifest) BuildAttestationImage(options ...func(*ManifestImageOptions) error) (v1.Image, error) { func (manifest *Manifest) BuildImage(options ...func(*ManifestImageOptions) error) (v1.Image, error) {
opts, err := newOptions(options...) opts, err := newOptions(options...)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create options: %w", err) return nil, fmt.Errorf("failed to create options: %w", err)
@@ -229,7 +243,7 @@ func (manifest *Manifest) BuildAttestationImage(options ...func(*ManifestImageOp
} }
// so that we attach all attestations to a single attestations image - as per current buildkit // so that we attach all attestations to a single attestations image - as per current buildkit
opts.laxReferrers = true opts.laxReferrers = true
newImg, err := buildImage(resultLayers, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts) newImg, err := buildImageFromLayers(resultLayers, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to build image: %w", err) return nil, fmt.Errorf("failed to build image: %w", err)
} }
@@ -241,7 +255,7 @@ func (manifest *Manifest) BuildReferringArtifacts() ([]v1.Image, error) {
var images []v1.Image var images []v1.Image
for _, layer := range manifest.SignedLayers { for _, layer := range manifest.SignedLayers {
opts := &ManifestImageOptions{} opts := &ManifestImageOptions{}
newImg, err := buildImage([]*Layer{layer}, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts) newImg, err := buildImageFromLayers([]*Layer{layer}, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to build image: %w", err) return nil, fmt.Errorf("failed to build image: %w", err)
} }
@@ -250,8 +264,8 @@ func (manifest *Manifest) BuildReferringArtifacts() ([]v1.Image, error) {
return images, nil return images, nil
} }
// build an image containing only layers. // build an image containing only layers provided.
func buildImage(layers []*Layer, manifest *v1.Descriptor, subject *v1.Descriptor, opts *ManifestImageOptions) (v1.Image, error) { func buildImageFromLayers(layers []*Layer, manifest *v1.Descriptor, subject *v1.Descriptor, opts *ManifestImageOptions) (v1.Image, error) {
newImg := empty.Image newImg := empty.Image
var err error var err error
if len(layers) == 0 { if len(layers) == 0 {
@@ -296,46 +310,135 @@ func buildImage(layers []*Layer, manifest *v1.Descriptor, subject *v1.Descriptor
} }
if !opts.laxReferrers { if !opts.laxReferrers {
// as per https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidance-for-an-empty-descriptor // as per https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidance-for-an-empty-descriptor
newImg = &EmptyConfigImage{newImg} newImg = &oci.EmptyConfigImage{Image: newImg}
} }
return newImg, nil return newImg, nil
} }
type EmptyConfigImage struct { func ExtractEnvelopes(manifest *Manifest, predicateType string) ([]*Envelope, error) {
v1.Image var envs []*Envelope
} dsseMediaType, err := DSSEMediaType(predicateType)
func (i *EmptyConfigImage) RawConfigFile() ([]byte, error) {
return []byte("{}"), nil
}
func (i *EmptyConfigImage) Manifest() (*v1.Manifest, error) {
mf, err := i.Image.Manifest()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get manifest: %w", err) return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err)
} }
mf.Config = v1.Descriptor{ for _, attestationLayer := range manifest.OriginalLayers {
MediaType: "application/vnd.oci.empty.v1+json", mt, err := attestationLayer.Layer.MediaType()
Size: 2, if err != nil {
Digest: v1.Hash{Algorithm: "sha256", Hex: "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"}, return nil, fmt.Errorf("failed to get layer media type: %w", err)
Data: []byte("{}"), }
if string(mt) == dsseMediaType {
reader, err := attestationLayer.Layer.Uncompressed()
if err != nil {
return nil, fmt.Errorf("failed to get layer contents: %w", err)
}
defer reader.Close()
env := new(Envelope)
err = json.NewDecoder(reader).Decode(&env)
if err != nil {
return nil, fmt.Errorf("failed to decode envelope: %w", err)
}
envs = append(envs, env)
}
} }
return mf, nil
return envs, nil
} }
func (i *EmptyConfigImage) RawManifest() ([]byte, error) { func ExtractStatementsFromIndex(idx v1.ImageIndex, mediaType string) ([]*AnnotatedStatement, error) {
mf, err := i.Manifest() mfs2, err := idx.IndexManifest()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get manifest: %w", err) return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
} }
return json.Marshal(mf)
var statements []*AnnotatedStatement
for i := range mfs2.Manifests {
mf := &mfs2.Manifests[i]
if mf.Annotations[DockerReferenceType] != "attestation-manifest" {
continue
}
attestationImage, err := idx.Image(mf.Digest)
if err != nil {
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
}
layers, err := attestationImage.Layers()
if err != nil {
return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err)
}
for _, layer := range layers {
// parse layer blob as json
mt, err := layer.MediaType()
if err != nil {
return nil, fmt.Errorf("failed to get layer media type: %w", err)
}
if string(mt) != mediaType {
continue
}
r, err := layer.Uncompressed()
if err != nil {
return nil, fmt.Errorf("failed to get layer contents: %w", err)
}
defer r.Close()
inTotoStatement := new(intoto.Statement)
var desc *v1.Descriptor
if strings.HasSuffix(string(mt), "+dsse") {
env := new(Envelope)
err = json.NewDecoder(r).Decode(env)
if err != nil {
return nil, fmt.Errorf("failed to decode env: %w", err)
}
payload, err := base64.StdEncoding.Strict().DecodeString(env.Payload)
if err != nil {
return nil, fmt.Errorf("failed to decode payload: %w", err)
}
err = json.Unmarshal([]byte(payload), inTotoStatement)
if err != nil {
return nil, fmt.Errorf("failed to decode %s statement: %w", mediaType, err)
}
} else {
desc := new(v1.Descriptor)
err = json.NewDecoder(r).Decode(desc)
if err != nil {
return nil, fmt.Errorf("failed to decode statement: %w", err)
}
}
layerDesc, err := partial.Descriptor(layer)
if err != nil {
return nil, fmt.Errorf("failed to get descriptor for layer: %w", err)
}
annotations := make(map[string]string)
for k, v := range layerDesc.Annotations {
annotations[k] = v
}
statements = append(statements, &AnnotatedStatement{
OCIDescriptor: desc,
InTotoStatement: inTotoStatement,
Annotations: annotations,
})
}
}
return statements, nil
} }
func (i *EmptyConfigImage) Digest() (v1.Hash, error) { func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStatement, error) {
mb, err := i.RawManifest() idx, err := layout.ImageIndexFromPath(path)
if err != nil { if err != nil {
return v1.Hash{}, err return nil, fmt.Errorf("failed to load image index: %w", err)
} }
digest, _, err := v1.SHA256(bytes.NewReader(mb))
return digest, err idxm, err := idx.IndexManifest()
if err != nil {
return nil, fmt.Errorf("failed to get digest: %w", err)
}
idxDigest := idxm.Manifests[0].Digest
mfs, err := idx.ImageIndex(idxDigest)
if err != nil {
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
}
return ExtractStatementsFromIndex(mfs, mediaType)
} }

View File

@@ -1,21 +1,18 @@
package test package attestation_test
import ( import (
"path/filepath"
"testing" "testing"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/attestation"
intoto "github.com/in-toto/in-toto-golang/in_toto" intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
var UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image") const ExpectedStatements = 4
const (
ExpectedStatements = 4
)
func TestExtractAnnotatedStatements(t *testing.T) { func TestExtractAnnotatedStatements(t *testing.T) {
statements, err := ExtractAnnotatedStatements(UnsignedTestImage, intoto.PayloadType) statements, err := attestation.ExtractAnnotatedStatements(test.UnsignedTestImage, intoto.PayloadType)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equalf(t, len(statements), ExpectedStatements, "expected %d statement, got %d", ExpectedStatements, len(statements)) assert.Equalf(t, len(statements), ExpectedStatements, "expected %d statement, got %d", ExpectedStatements, len(statements))
} }

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"time" "time"
"github.com/docker/attest/pkg/attest"
"github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/signerverifier" "github.com/docker/attest/pkg/signerverifier"
@@ -62,13 +61,13 @@ func ExampleManifest() {
} }
// create a new manifest to hold the attestation // create a new manifest to hold the attestation
manifest, err := attest.NewAttestationManifest(desc) manifest, err := attestation.NewManifest(desc)
if err != nil { if err != nil {
panic(err) panic(err)
} }
// sign and add the attestation to the manifest // sign and add the attestation to the manifest
err = manifest.AddAttestation(context.Background(), signer, statement, opts) err = manifest.Add(context.Background(), signer, statement, opts)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -79,7 +78,11 @@ func ExampleManifest() {
} }
// save the manifest to the registry as a referrers artifact // save the manifest to the registry as a referrers artifact
err = oci.SaveReferrers(manifest, output) artifacts, err := manifest.BuildReferringArtifacts()
if err != nil {
panic(err)
}
err = oci.SaveImagesNoTag(artifacts, output)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@@ -1,36 +1,35 @@
package oci package attestation
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/oci"
att "github.com/docker/attest/pkg/attestation"
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/layout" "github.com/google/go-containerregistry/pkg/v1/layout"
) )
// implementation of AttestationResolver that closes over attestations from an oci layout. // implementation of Resolver that closes over attestations from an oci layout.
type LayoutResolver struct { type LayoutResolver struct {
*attestation.Manifest *Manifest
*ImageSpec *oci.ImageSpec
} }
func NewOCILayoutAttestationResolver(src *ImageSpec) (*LayoutResolver, error) { func NewOCILayoutResolver(src *oci.ImageSpec) (*LayoutResolver, error) {
r := &LayoutResolver{ r := &LayoutResolver{
ImageSpec: src, ImageSpec: src,
} }
_, err := r.fetchAttestationManifest() _, err := r.fetchManifest()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return r, nil return r, nil
} }
func (r *LayoutResolver) fetchAttestationManifest() (*attestation.Manifest, error) { func (r *LayoutResolver) fetchManifest() (*Manifest, error) {
if r.Manifest == nil { if r.Manifest == nil {
m, err := attestationManifestFromOCILayout(r.Identifier, r.ImageSpec.Platform) m, err := manifestFromOCILayout(r.Identifier, r.ImageSpec.Platform)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -40,9 +39,9 @@ func (r *LayoutResolver) fetchAttestationManifest() (*attestation.Manifest, erro
return r.Manifest, nil return r.Manifest, nil
} }
func (r *LayoutResolver) Attestations(_ context.Context, predicateType string) ([]*att.Envelope, error) { func (r *LayoutResolver) Attestations(_ context.Context, predicateType string) ([]*Envelope, error) {
var envs []*att.Envelope var envs []*Envelope
dsseMediaType, err := attestation.DSSEMediaType(predicateType) dsseMediaType, err := DSSEMediaType(predicateType)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err) return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err)
} }
@@ -55,7 +54,7 @@ func (r *LayoutResolver) Attestations(_ context.Context, predicateType string) (
if mts != dsseMediaType { if mts != dsseMediaType {
continue continue
} }
env := new(att.Envelope) env := new(Envelope)
// parse layer blob as json // parse layer blob as json
r, err := attestationLayer.Layer.Uncompressed() r, err := attestationLayer.Layer.Uncompressed()
if err != nil { if err != nil {
@@ -83,7 +82,7 @@ func (r *LayoutResolver) ImagePlatform(_ context.Context) (*v1.Platform, error)
return r.ImageSpec.Platform, nil return r.ImageSpec.Platform, nil
} }
func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*attestation.Manifest, error) { func manifestFromOCILayout(path string, platform *v1.Platform) (*Manifest, error) {
idx, err := layout.ImageIndexFromPath(path) idx, err := layout.ImageIndexFromPath(path)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -120,11 +119,11 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*atte
} }
for i := range mfs2.Manifests { for i := range mfs2.Manifests {
mf := &mfs2.Manifests[i] mf := &mfs2.Manifests[i]
if mf.Annotations[att.DockerReferenceType] != attestation.AttestationManifestType { if mf.Annotations[DockerReferenceType] != AttestationManifestType {
continue continue
} }
if mf.Annotations[att.DockerReferenceDigest] != subjectDescriptor.Digest.String() { if mf.Annotations[DockerReferenceDigest] != subjectDescriptor.Digest.String() {
continue continue
} }
@@ -132,11 +131,11 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*atte
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err) return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
} }
layers, err := attestation.GetAttestationsFromImage(attestationImage) layers, err := layersFromImage(attestationImage)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get attestations from image: %w", err) return nil, fmt.Errorf("failed to get attestations from image: %w", err)
} }
attest := &attestation.Manifest{ attest := &Manifest{
OriginalLayers: layers, OriginalLayers: layers,
OriginalDescriptor: mf, OriginalDescriptor: mf,
SubjectName: idxDescriptor.Annotations["org.opencontainers.image.ref.name"], SubjectName: idxDescriptor.Annotations["org.opencontainers.image.ref.name"],

View File

@@ -1,4 +1,4 @@
package oci_test package attestation_test
import ( import (
"strings" "strings"
@@ -24,7 +24,7 @@ func TestAttestationFromOCILayout(t *testing.T) {
} }
opts := &attestation.SigningOptions{} opts := &attestation.SigningOptions{}
attIdx, err := oci.IndexFromPath(oci.UnsignedTestImage) attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
require.NoError(t, err) require.NoError(t, err)
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts) signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err) require.NoError(t, err)

View File

@@ -1,17 +1,17 @@
package oci package attestation
import ( import (
"context" "context"
"github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/oci"
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
) )
type MockResolver struct { type MockResolver struct {
Envs []*attestation.Envelope Envs []*Envelope
} }
func (r MockResolver) Attestations(_ context.Context, _ string) ([]*attestation.Envelope, error) { func (r MockResolver) Attestations(_ context.Context, _ string) ([]*Envelope, error) {
return r.Envs, nil return r.Envs, nil
} }
@@ -32,7 +32,7 @@ func (r MockResolver) ImageDescriptor(_ context.Context) (*v1.Descriptor, error)
} }
func (r MockResolver) ImagePlatform(_ context.Context) (*v1.Platform, error) { func (r MockResolver) ImagePlatform(_ context.Context) (*v1.Platform, error) {
return ParsePlatform("linux/amd64") return oci.ParsePlatform("linux/amd64")
} }
type MockRegistryResolver struct { type MockRegistryResolver struct {

View File

@@ -1,22 +1,21 @@
package oci package attestation
import ( import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/oci"
att "github.com/docker/attest/pkg/attestation"
"github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote"
) )
type ReferrersResolver struct { type ReferrersResolver struct {
referrersRepo string referrersRepo string
ImageDetailsResolver oci.ImageDetailsResolver
} }
func NewReferrersAttestationResolver(src ImageDetailsResolver, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) { func NewReferrersResolver(src oci.ImageDetailsResolver, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) {
res := &ReferrersResolver{ res := &ReferrersResolver{
ImageDetailsResolver: src, ImageDetailsResolver: src,
} }
@@ -36,8 +35,8 @@ func WithReferrersRepo(repo string) func(*ReferrersResolver) error {
} }
} }
func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateType string) ([]*attestation.Manifest, error) { func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateType string) ([]*Manifest, error) {
dsseMediaType, err := attestation.DSSEMediaType(predicateType) dsseMediaType, err := DSSEMediaType(predicateType)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err) return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err)
} }
@@ -54,19 +53,16 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy
return nil, fmt.Errorf("failed to get descriptor: %w", err) return nil, fmt.Errorf("failed to get descriptor: %w", err)
} }
subjectDigest := desc.Digest.String() subjectDigest := desc.Digest.String()
if err != nil {
return nil, fmt.Errorf("failed to get digest: %w", err)
}
var referrersSubjectRef name.Digest var referrersSubjectRef name.Digest
if r.referrersRepo != "" { if r.referrersRepo != "" {
referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", strings.TrimPrefix(r.referrersRepo, RegistryPrefix), subjectDigest)) referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", strings.TrimPrefix(r.referrersRepo, oci.RegistryPrefix), subjectDigest))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create referrers reference: %w", err) return nil, fmt.Errorf("failed to create referrers reference: %w", err)
} }
} else { } else {
referrersSubjectRef = subjectRef.Context().Digest(subjectDigest) referrersSubjectRef = subjectRef.Context().Digest(subjectDigest)
} }
options := WithOptions(ctx, nil) options := oci.WithOptions(ctx, nil)
options = append(options, remote.WithFilter("artifactType", dsseMediaType)) options = append(options, remote.WithFilter("artifactType", dsseMediaType))
referrersIndex, err := remote.Referrers(referrersSubjectRef, options...) referrersIndex, err := remote.Referrers(referrersSubjectRef, options...)
if err != nil { if err != nil {
@@ -76,16 +72,16 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get index manifest: %w", err) return nil, fmt.Errorf("failed to get index manifest: %w", err)
} }
aManifests := make([]*attestation.Manifest, 0) aManifests := make([]*Manifest, 0)
for i := range referrersIndexManifest.Manifests { for i := range referrersIndexManifest.Manifests {
m := referrersIndexManifest.Manifests[i] m := referrersIndexManifest.Manifests[i]
remoteRef := referrersSubjectRef.Context().Digest(m.Digest.String()) remoteRef := referrersSubjectRef.Context().Digest(m.Digest.String())
options = WithOptions(ctx, nil) options = oci.WithOptions(ctx, nil)
attestationImage, err := remote.Image(remoteRef, options...) attestationImage, err := remote.Image(remoteRef, options...)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get referred image: %w", err) return nil, fmt.Errorf("failed to get referred image: %w", err)
} }
layers, err := attestation.GetAttestationsFromImage(attestationImage) layers, err := layersFromImage(attestationImage)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get attestations from image: %w", err) return nil, fmt.Errorf("failed to get attestations from image: %w", err)
} }
@@ -99,7 +95,7 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy
if string(mt) != dsseMediaType { if string(mt) != dsseMediaType {
return nil, fmt.Errorf("expected layer media type %s, got %s", dsseMediaType, mt) return nil, fmt.Errorf("expected layer media type %s, got %s", dsseMediaType, mt)
} }
attest := &attestation.Manifest{ attest := &Manifest{
SubjectName: imageName, SubjectName: imageName,
OriginalLayers: layers, OriginalLayers: layers,
OriginalDescriptor: &m, OriginalDescriptor: &m,
@@ -110,12 +106,12 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy
return aManifests, nil return aManifests, nil
} }
func (r *ReferrersResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) { func (r *ReferrersResolver) Attestations(ctx context.Context, predicateType string) ([]*Envelope, error) {
manifests, err := r.resolveAttestations(ctx, predicateType) manifests, err := r.resolveAttestations(ctx, predicateType)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to resolve attestations: %w", err) return nil, fmt.Errorf("failed to resolve attestations: %w", err)
} }
var envs []*att.Envelope var envs []*Envelope
for _, attest := range manifests { for _, attest := range manifests {
es, err := ExtractEnvelopes(attest, predicateType) es, err := ExtractEnvelopes(attest, predicateType)
if err != nil { if err != nil {

View File

@@ -8,34 +8,34 @@ import (
"testing" "testing"
"github.com/docker/attest/internal/test" "github.com/docker/attest/internal/test"
"github.com/docker/attest/internal/util"
"github.com/docker/attest/pkg/attest" "github.com/docker/attest/pkg/attest"
"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"
"github.com/docker/attest/pkg/policy" "github.com/docker/attest/pkg/policy"
"github.com/docker/attest/pkg/tuf"
"github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/registry" "github.com/google/go-containerregistry/pkg/registry"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var ( var (
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image") NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass") PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
LocalPolicy = filepath.Join("..", "..", "test", "testdata", "local-policy") LocalPolicy = filepath.Join("..", "..", "test", "testdata", "local-policy")
LocalPolicyAttached = filepath.Join("..", "..", "test", "testdata", "local-policy-attached") LocalPolicyAttached = filepath.Join("..", "..", "test", "testdata", "local-policy-attached")
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl") PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail") FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
EmptyTUFDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-policies")
TestTempDir = "attest-sign-test" TestTempDir = "attest-sign-test"
) )
func TestAttestationReferenceTypes(t *testing.T) { func TestAttestationReferenceTypes(t *testing.T) {
ctx, signer := test.Setup(t) ctx, signer := test.Setup(t)
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true)) ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
ctx = tuf.WithDownloader(ctx, tuf.NewMockTufClient(EmptyTUFDir, test.CreateTempDir(t, "", "tuf-dest")))
platforms := []string{"linux/amd64", "linux/arm64"} platforms := []string{"linux/amd64", "linux/arm64"}
for _, tc := range []struct { for _, tc := range []struct {
name string name string
@@ -94,7 +94,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
opts := &attestation.SigningOptions{ opts := &attestation.SigningOptions{
SkipTL: true, SkipTL: true,
} }
attIdx, err := oci.IndexFromPath(UnsignedTestImage) attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
require.NoError(t, err) require.NoError(t, err)
indexName := fmt.Sprintf("%s/repo:root", u.Host) indexName := fmt.Sprintf("%s/repo:root", u.Host)
@@ -120,7 +120,9 @@ func TestAttestationReferenceTypes(t *testing.T) {
output, err := oci.ParseImageSpec(outputRepo) output, err := oci.ParseImageSpec(outputRepo)
require.NoError(t, err) require.NoError(t, err)
for _, attIdx := range signedManifests { for _, attIdx := range signedManifests {
err = oci.SaveReferrers(attIdx, []*oci.ImageSpec{output}) images, err := attIdx.BuildReferringArtifacts()
require.NoError(t, err)
err = oci.SaveImagesNoTag(images, []*oci.ImageSpec{output})
require.NoError(t, err) require.NoError(t, err)
} }
@@ -183,6 +185,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
func TestReferencesInDifferentRepo(t *testing.T) { func TestReferencesInDifferentRepo(t *testing.T) {
ctx, signer := test.Setup(t) ctx, signer := test.Setup(t)
ctx = tuf.WithDownloader(ctx, tuf.NewMockTufClient(EmptyTUFDir, test.CreateTempDir(t, "", "tuf-dest")))
repoName := "repo" repoName := "repo"
for _, tc := range []struct { for _, tc := range []struct {
name string name string
@@ -213,7 +216,7 @@ func TestReferencesInDifferentRepo(t *testing.T) {
opts := &attestation.SigningOptions{ opts := &attestation.SigningOptions{
SkipTL: true, SkipTL: true,
} }
attIdx, err := oci.IndexFromPath(UnsignedTestImage) attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
require.NoError(t, err) require.NoError(t, err)
indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName) indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName)
@@ -226,7 +229,7 @@ func TestReferencesInDifferentRepo(t *testing.T) {
// push signed attestation image to the ref server // push signed attestation image to the ref server
for _, signedManifest := range signedManifests { for _, signedManifest := range signedManifests {
// push references using subject-digest.att convention // push references using subject-digest.att convention
image, err := signedManifest.BuildAttestationImage() image, err := signedManifest.BuildImage()
require.NoError(t, err) require.NoError(t, err)
err = oci.PushImageToRegistry(image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerURL.Host, repoName)) err = oci.PushImageToRegistry(image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerURL.Host, repoName))
require.NoError(t, err) require.NoError(t, err)
@@ -239,7 +242,7 @@ func TestReferencesInDifferentRepo(t *testing.T) {
opts := &attestation.SigningOptions{ opts := &attestation.SigningOptions{
SkipTL: true, SkipTL: true,
} }
attIdx, err := oci.IndexFromPath(UnsignedTestImage) attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
require.NoError(t, err) require.NoError(t, err)
indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName) indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName)
@@ -294,7 +297,7 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) {
opts := &attestation.SigningOptions{ opts := &attestation.SigningOptions{
SkipTL: true, SkipTL: true,
} }
attIdx, err := oci.IndexFromPath(UnsignedTestImage) attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
require.NoError(t, err) require.NoError(t, err)
indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName) indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName)
@@ -327,14 +330,3 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) {
} }
} }
} }
func TestEmptyConfigImageDigest(t *testing.T) {
empty := empty.Image
img := attestation.EmptyConfigImage{empty}
mf, err := img.RawManifest()
require.NoError(t, err)
hash := util.SHA256Hex(mf)
digest, err := img.Digest()
require.NoError(t, err)
assert.Equal(t, digest.Hex, hash)
}

101
pkg/attestation/registry.go Normal file
View File

@@ -0,0 +1,101 @@
package attestation
import (
"context"
"fmt"
"github.com/docker/attest/pkg/oci"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
)
type RegistryResolver struct {
*oci.RegistryImageDetailsResolver
*Manifest
}
func NewRegistryResolver(src *oci.RegistryImageDetailsResolver) (*RegistryResolver, error) {
return &RegistryResolver{
RegistryImageDetailsResolver: src,
}, nil
}
func (r *RegistryResolver) Attestations(ctx context.Context, predicateType string) ([]*Envelope, error) {
if r.Manifest == nil {
attest, err := FetchManifest(ctx, r.Identifier, r.ImageSpec.Platform)
if err != nil {
return nil, err
}
r.Manifest = attest
}
return ExtractEnvelopes(r.Manifest, predicateType)
}
func attestationDigestForImage(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) {
for i := range ix.Manifests {
m := &ix.Manifests[i]
if v, ok := m.Annotations[DockerReferenceType]; ok && v == attestType {
if d, ok := m.Annotations[DockerReferenceDigest]; ok && d == imageDigest {
return m.Digest.String(), nil
}
}
}
return "", fmt.Errorf("no attestation found for image %s", imageDigest)
}
func FetchManifest(ctx context.Context, image string, platform *v1.Platform) (*Manifest, error) {
// we want to get to the image index, so ignoring platform for now
options := oci.WithOptions(ctx, nil)
ref, err := name.ParseReference(image)
if err != nil {
return nil, fmt.Errorf("failed to parse reference: %w", err)
}
index, err := remote.Index(ref, options...)
if err != nil {
return nil, fmt.Errorf("failed to get index: %w", err)
}
indexManifest, err := index.IndexManifest()
if err != nil {
return nil, fmt.Errorf("failed to get index manifest: %w", err)
}
subjectDescriptor, err := oci.ImageDescriptor(indexManifest, platform)
if err != nil {
return nil, fmt.Errorf("failed to obtain image for platform: %w", err)
}
digest := subjectDescriptor.Digest.String()
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), digest))
if err != nil {
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
}
attestationDigest, err := attestationDigestForImage(indexManifest, digest, "attestation-manifest")
if err != nil {
return nil, fmt.Errorf("failed to obtain attestation for image: %w", err)
}
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), attestationDigest))
if err != nil {
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
}
remoteDescriptor, err := remote.Get(ref, options...)
if err != nil {
return nil, fmt.Errorf("failed to get attestation: %w", err)
}
attestationImage, err := remoteDescriptor.Image()
if err != nil {
return nil, fmt.Errorf("failed to get attestation image: %w", err)
}
layers, err := layersFromImage(attestationImage)
if err != nil {
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
}
attest := &Manifest{
OriginalLayers: layers,
OriginalDescriptor: &remoteDescriptor.Descriptor,
SubjectName: image,
SubjectDescriptor: subjectDescriptor,
}
return attest, nil
}

View File

@@ -1,4 +1,4 @@
package oci_test package attestation_test
import ( import (
"fmt" "fmt"
@@ -25,7 +25,7 @@ func TestRegistry(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
opts := &attestation.SigningOptions{} opts := &attestation.SigningOptions{}
attIdx, err := oci.IndexFromPath(oci.UnsignedTestImage) attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
require.NoError(t, err) require.NoError(t, err)
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts) signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err) require.NoError(t, err)

View File

@@ -0,0 +1,12 @@
package attestation
import (
"context"
"github.com/docker/attest/pkg/oci"
)
type Resolver interface {
oci.ImageDetailsResolver
Attestations(ctx context.Context, mediaType string) ([]*Envelope, error)
}

View File

@@ -6,12 +6,19 @@ import (
"crypto/rand" "crypto/rand"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http/httptest"
"net/url"
"testing" "testing"
"time" "time"
"github.com/docker/attest/internal/test" "github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/signerverifier" "github.com/docker/attest/pkg/signerverifier"
"github.com/google/go-containerregistry/pkg/registry"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/static"
"github.com/google/go-containerregistry/pkg/v1/types"
intoto "github.com/in-toto/in-toto-golang/in_toto" intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -149,3 +156,146 @@ func TestSignVerifyAttestation(t *testing.T) {
}) })
} }
} }
func TestAddSignedLayerAnnotations(t *testing.T) {
ctx, signer := test.Setup(t)
testCases := []struct {
name string
replace bool
}{
{"replaced", true},
{"not replaced", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
data := []byte("signed")
testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType))
mediaType := types.OCIManifestSchema1
opts := &attestation.SigningOptions{}
originalLayer := &attestation.Layer{
Layer: testLayer,
Statement: &intoto.Statement{
StatementHeader: intoto.StatementHeader{
PredicateType: attestation.VSAPredicateType,
},
},
Annotations: map[string]string{"test": "test"},
}
manifest := &attestation.Manifest{
OriginalDescriptor: &v1.Descriptor{
MediaType: mediaType,
},
OriginalLayers: []*attestation.Layer{
originalLayer,
},
SubjectDescriptor: &v1.Descriptor{},
}
err := manifest.Add(ctx, signer, originalLayer.Statement, opts)
require.NoError(t, err)
newImg, err := manifest.BuildImage(attestation.WithReplacedLayers(tc.replace))
require.NoError(t, err)
mf, _ := newImg.RawManifest()
type Annotations struct {
Annotations map[string]string `json:"annotations"`
}
type Layers struct {
Layers []Annotations `json:"layers"`
}
l := &Layers{}
err = json.Unmarshal(mf, l)
require.NoError(t, err)
_, ok := l.Layers[0].Annotations["test"]
assert.Truef(t, ok, "missing annotations")
})
}
}
func TestSimpleStatementSigning(t *testing.T) {
ctx, signer := test.Setup(t)
empty := types.MediaType("application/vnd.oci.empty.v1+json")
testCases := []struct {
name string
replace bool
}{
{"replaced", true},
{"not replaced", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
opts := &attestation.SigningOptions{}
statement := &intoto.Statement{
StatementHeader: intoto.StatementHeader{
PredicateType: attestation.VSAPredicateType,
},
}
statement2 := &intoto.Statement{
StatementHeader: intoto.StatementHeader{
PredicateType: attestation.VSAPredicateType,
},
}
digest, err := v1.NewHash("sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620")
require.NoError(t, err)
subject := &v1.Descriptor{
MediaType: "application/vnd.oci.image.manifest.v1+json",
Digest: digest,
}
manifest, err := attestation.NewManifest(subject)
require.NoError(t, err)
err = manifest.Add(ctx, signer, statement, opts)
require.NoError(t, err)
err = manifest.Add(ctx, signer, statement2, opts)
require.NoError(t, err)
// fake that the manfifest was loaded from a real image
manifest.OriginalLayers = manifest.SignedLayers
envelopes, err := attestation.ExtractEnvelopes(manifest, attestation.VSAPredicateType)
require.NoError(t, err)
assert.Len(t, envelopes, 2)
newImg, err := manifest.BuildImage(attestation.WithReplacedLayers(tc.replace))
require.NoError(t, err)
layers, err := newImg.Layers()
require.NoError(t, err)
if tc.replace {
assert.Len(t, layers, 2)
} else {
assert.Len(t, layers, 4)
}
newImgs, err := manifest.BuildReferringArtifacts()
require.NoError(t, err)
assert.Len(t, newImgs, 2)
for _, img := range newImgs {
mf, err := img.Manifest()
require.NoError(t, err)
assert.Contains(t, mf.ArtifactType, "application/vnd.in-toto")
assert.Contains(t, mf.ArtifactType, "+dsse")
assert.Equal(t, subject.MediaType, mf.MediaType)
assert.Equal(t, empty, mf.Config.MediaType)
assert.Equal(t, int64(2), mf.Config.Size)
assert.Equal(t, "{}", string(mf.Config.Data))
layers, err := img.Layers()
require.NoError(t, err)
assert.Len(t, layers, 1)
}
server := httptest.NewServer(registry.New(registry.WithReferrersSupport(true)))
defer server.Close()
u, err := url.Parse(server.URL)
require.NoError(t, err)
indexName := fmt.Sprintf("%s/repo:root", u.Host)
output, err := oci.ParseImageSpecs(indexName)
require.NoError(t, err)
artifacts, err := manifest.BuildReferringArtifacts()
require.NoError(t, err)
err = oci.SaveImagesNoTag(artifacts, output)
require.NoError(t, err)
})
}
}

View File

@@ -64,6 +64,12 @@ type Extension struct {
Ext *DockerDSSEExtension `json:"ext"` Ext *DockerDSSEExtension `json:"ext"`
} }
type AnnotatedStatement struct {
OCIDescriptor *v1.Descriptor
InTotoStatement *intoto.Statement
Annotations map[string]string
}
type DockerDSSEExtension struct { type DockerDSSEExtension struct {
TL *DockerTLExtension `json:"tl"` TL *DockerTLExtension `json:"tl"`
} }
@@ -83,6 +89,12 @@ type SigningOptions struct {
SkipTL bool SkipTL bool
} }
type Options struct {
NoReferrers bool
Attach bool
ReferrersRepo string
}
func DSSEMediaType(predicateType string) (string, error) { func DSSEMediaType(predicateType string) (string, error) {
var predicateName string var predicateName string
switch predicateType { switch predicateType {

View File

@@ -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 {

View File

@@ -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)
} }

2
pkg/mirror/README.md Normal file
View File

@@ -0,0 +1,2 @@
## mirror
This package contains components to mirror TUF metadata and targets to OCI.

View File

@@ -6,7 +6,6 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/pkg/mirror" "github.com/docker/attest/pkg/mirror"
"github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/tuf" "github.com/docker/attest/pkg/tuf"
@@ -30,7 +29,7 @@ func ExampleNewTUFMirror() {
// configure TUF mirror // configure TUF mirror
metadataURI := "https://docker.github.io/tuf-staging/metadata" metadataURI := "https://docker.github.io/tuf-staging/metadata"
targetsURI := "https://docker.github.io/tuf-staging/targets" targetsURI := "https://docker.github.io/tuf-staging/targets"
m, err := mirror.NewTUFMirror(embed.RootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker()) m, err := mirror.NewTUFMirror(tuf.DockerTUFRootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker())
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"github.com/docker/attest/pkg/oci"
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/mutate"
@@ -17,7 +18,7 @@ import (
// ----------------- // -----------------
// GetMetadataManifest returns an image with TUF root metadata as layers. // GetMetadataManifest returns an image with TUF root metadata as layers.
func (m *TUFMirror) GetMetadataManifest(metadataURL string) (v1.Image, error) { func (m *TUFMirror) GetMetadataManifest(metadataURL string) (*oci.EmptyConfigImage, error) {
metadata, err := m.getMetadataMirror(metadataURL) metadata, err := m.getMetadataMirror(metadataURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get metadata: %w", err) return nil, fmt.Errorf("failed to get metadata: %w", err)
@@ -26,7 +27,7 @@ func (m *TUFMirror) GetMetadataManifest(metadataURL string) (v1.Image, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to build metadata manifest: %w", err) return nil, fmt.Errorf("failed to build metadata manifest: %w", err)
} }
return manifest, nil return &oci.EmptyConfigImage{Image: manifest}, nil
} }
// getMetadataMirror returns a TufMetadata struct with TUF metadata as map of file names to bytes. // getMetadataMirror returns a TufMetadata struct with TUF metadata as map of file names to bytes.
@@ -183,7 +184,7 @@ func (m *TUFMirror) buildDelegatedMetadataManifests(delegated []DelegatedTargetM
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to append delegated targets layer to image: %w", err) return nil, fmt.Errorf("failed to append delegated targets layer to image: %w", err)
} }
manifests = append(manifests, &Image{Image: img, Tag: role.Name}) manifests = append(manifests, &Image{Image: &oci.EmptyConfigImage{Image: img}, Tag: role.Name})
} }
return manifests, nil return manifests, nil
} }

View File

@@ -9,22 +9,26 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/internal/test" "github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/tuf" "github.com/docker/attest/pkg/tuf"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/theupdateframework/go-tuf/v2/metadata" "github.com/theupdateframework/go-tuf/v2/metadata"
) )
const (
metadataPath = "/metadata"
targetsPath = "/targets"
)
func TestGetTufMetadataMirror(t *testing.T) { func TestGetTufMetadataMirror(t *testing.T) {
server := httptest.NewServer(http.FileServer(http.Dir(filepath.Join("..", "..", "test", "testdata", "tuf", "test-repo")))) server := httptest.NewServer(http.FileServer(http.Dir(filepath.Join("..", "..", "test", "testdata", "tuf", "test-repo"))))
defer server.Close() defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp") path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker()) m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
assert.NoError(t, err) assert.NoError(t, err)
tufMetadata, err := m.getMetadataMirror(server.URL + "/metadata") tufMetadata, err := m.getMetadataMirror(server.URL + metadataPath)
assert.NoError(t, err) assert.NoError(t, err)
// check that all roles are not empty // check that all roles are not empty
@@ -39,10 +43,10 @@ func TestGetMetadataManifest(t *testing.T) {
defer server.Close() defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp") path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker()) m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
assert.NoError(t, err) assert.NoError(t, err)
img, err := m.GetMetadataManifest(server.URL + "/metadata") img, err := m.GetMetadataManifest(server.URL + metadataPath)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, img) assert.NotNil(t, img)
@@ -78,7 +82,7 @@ func TestGetDelegatedMetadataMirrors(t *testing.T) {
defer server.Close() defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp") path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker()) m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
assert.NoError(t, err) assert.NoError(t, err)
delegations, err := m.GetDelegatedMetadataMirrors() delegations, err := m.GetDelegatedMetadataMirrors()

View File

@@ -3,15 +3,14 @@ package mirror
import ( import (
"fmt" "fmt"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/pkg/tuf" "github.com/docker/attest/pkg/tuf"
) )
func NewTUFMirror(root []byte, tufPath, metadataURL, targetsURL string, versionChecker tuf.VersionChecker) (*TUFMirror, error) { func NewTUFMirror(root []byte, tufPath, metadataURL, targetsURL string, versionChecker tuf.VersionChecker) (*TUFMirror, error) {
if root == nil { if root == nil {
root = embed.RootDefault.Data root = tuf.DockerTUFRootDefault.Data
} }
tufClient, err := tuf.NewClient(root, tufPath, metadataURL, targetsURL, versionChecker) tufClient, err := tuf.NewClient(&tuf.ClientOptions{InitialRoot: root, Path: tufPath, MetadataSource: metadataURL, TargetsSource: targetsURL, VersionChecker: versionChecker})
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create TUF client: %w", err) return nil, fmt.Errorf("failed to create TUF client: %w", err)
} }

View File

@@ -5,6 +5,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/docker/attest/pkg/oci"
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/mutate"
@@ -22,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)
} }
@@ -37,12 +38,12 @@ 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)
} }
targetMirrors = append(targetMirrors, &Image{Image: img, Tag: name}) targetMirrors = append(targetMirrors, &Image{Image: &oci.EmptyConfigImage{Image: img}, Tag: name})
} }
return targetMirrors, nil return targetMirrors, nil
} }
@@ -68,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)
} }
@@ -88,14 +89,15 @@ 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)
} }
emptyConfigImage := &oci.EmptyConfigImage{Image: img}
// append image to index with annotation // append image to index with annotation
index = mutate.AppendManifests(index, mutate.IndexAddendum{ index = mutate.AppendManifests(index, mutate.IndexAddendum{
Add: img, Add: emptyConfigImage,
Descriptor: v1.Descriptor{ Descriptor: v1.Descriptor{
Annotations: map[string]string{ Annotations: map[string]string{
tufFileAnnotation: fmt.Sprintf("%s/%s", subdir, name), tufFileAnnotation: fmt.Sprintf("%s/%s", subdir, name),

View File

@@ -8,7 +8,6 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/internal/test" "github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/tuf" "github.com/docker/attest/pkg/tuf"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -27,7 +26,7 @@ func TestGetTufTargetsMirror(t *testing.T) {
defer server.Close() defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp") path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker()) m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
assert.NoError(t, err) assert.NoError(t, err)
targets, err := m.GetTUFTargetMirrors() targets, err := m.GetTUFTargetMirrors()
@@ -61,7 +60,7 @@ func TestTargetDelegationMetadata(t *testing.T) {
defer server.Close() defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp") path := test.CreateTempDir(t, "", "tuf_temp")
tm, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker()) tm, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
assert.NoError(t, err) assert.NoError(t, err)
targets, err := tm.TUFClient.LoadDelegatedTargets("test-role", "targets") targets, err := tm.TUFClient.LoadDelegatedTargets("test-role", "targets")
@@ -74,7 +73,7 @@ func TestGetDelegatedTargetMirrors(t *testing.T) {
defer server.Close() defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp") path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker()) m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
assert.NoError(t, err) assert.NoError(t, err)
mirrors, err := m.GetDelegatedTargetMirrors() mirrors, err := m.GetDelegatedTargetMirrors()

View File

@@ -1,6 +1,7 @@
package mirror package mirror
import ( import (
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/tuf" "github.com/docker/attest/pkg/tuf"
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/theupdateframework/go-tuf/v2/metadata" "github.com/theupdateframework/go-tuf/v2/metadata"
@@ -32,7 +33,7 @@ type DelegatedTargetMetadata struct {
} }
type Image struct { type Image struct {
Image v1.Image Image *oci.EmptyConfigImage
Tag string Tag string
} }

2
pkg/oci/README.md Normal file
View File

@@ -0,0 +1,2 @@
## oci
This package is for generic OCI components. For attestation specific components see the `attestation` package.

View File

@@ -1,19 +1,17 @@
//go:build e2e //go:build e2e
package mirror_test package oci_test
import ( import (
"path/filepath"
"testing" "testing"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/oci"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestRegistryAuth(t *testing.T) { func TestRegistryAuth(t *testing.T) {
UnsignedTestImage := filepath.Join("..", "..", "test", "testdata", "unsigned-test-image") attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
require.NoError(t, err) require.NoError(t, err)
// test cases for ecr, gcr and dockerhub // test cases for ecr, gcr and dockerhub
testCases := []struct { testCases := []struct {

View File

@@ -2,14 +2,11 @@ package oci
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"strings" "strings"
"github.com/containerd/platforms" "github.com/containerd/platforms"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/docker/attest/pkg/attestation"
att "github.com/docker/attest/pkg/attestation"
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
@@ -45,36 +42,7 @@ func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
return options return options
} }
func ExtractEnvelopes(manifest *attestation.Manifest, predicateType string) ([]*att.Envelope, error) { func ImageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descriptor, error) {
var envs []*att.Envelope
dsseMediaType, err := attestation.DSSEMediaType(predicateType)
if err != nil {
return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err)
}
for _, attestationLayer := range manifest.OriginalLayers {
mt, err := attestationLayer.Layer.MediaType()
if err != nil {
return nil, fmt.Errorf("failed to get layer media type: %w", err)
}
if string(mt) == dsseMediaType {
reader, err := attestationLayer.Layer.Uncompressed()
if err != nil {
return nil, fmt.Errorf("failed to get layer contents: %w", err)
}
defer reader.Close()
env := new(att.Envelope)
err = json.NewDecoder(reader).Decode(&env)
if err != nil {
return nil, fmt.Errorf("failed to decode envelope: %w", err)
}
envs = append(envs, env)
}
}
return envs, nil
}
func imageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descriptor, error) {
for i := range ix.Manifests { for i := range ix.Manifests {
m := &ix.Manifests[i] m := &ix.Manifests[i]
if (m.MediaType == ocispec.MediaTypeImageManifest || m.MediaType == "application/vnd.docker.distribution.manifest.v2+json") && m.Platform.Equals(*platform) { if (m.MediaType == ocispec.MediaTypeImageManifest || m.MediaType == "application/vnd.docker.distribution.manifest.v2+json") && m.Platform.Equals(*platform) {
@@ -84,24 +52,8 @@ func imageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descripto
return nil, fmt.Errorf("no image found for platform %v", platform) return nil, fmt.Errorf("no image found for platform %v", platform)
} }
func attestationDigestForDigest(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) { func RefToPURL(named reference.Named, platform *v1.Platform) (string, bool, error) {
for i := range ix.Manifests {
m := &ix.Manifests[i]
if v, ok := m.Annotations[att.DockerReferenceType]; ok && v == attestType {
if d, ok := m.Annotations[att.DockerReferenceDigest]; ok && d == imageDigest {
return m.Digest.String(), nil
}
}
}
return "", fmt.Errorf("no attestation found for image %s", imageDigest)
}
func RefToPURL(ref string, platform *v1.Platform) (string, bool, error) {
var isCanonical bool var isCanonical bool
named, err := reference.ParseNormalizedNamed(ref)
if err != nil {
return "", false, fmt.Errorf("failed to parse ref %q: %w", ref, err)
}
var qualifiers []packageurl.Qualifier var qualifiers []packageurl.Qualifier
if canonical, ok := named.(reference.Canonical); ok { if canonical, ok := named.(reference.Canonical); ok {
@@ -150,7 +102,7 @@ func SplitDigest(digest string) (common.DigestSet, error) {
} }
func ReplaceTagInSpec(src *ImageSpec, digest v1.Hash) (*ImageSpec, error) { func ReplaceTagInSpec(src *ImageSpec, digest v1.Hash) (*ImageSpec, error) {
newName, err := replaceTag(src.Identifier, digest) newName, err := ReplaceTag(src.Identifier, digest)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse repo name: %w", err) return nil, fmt.Errorf("failed to parse repo name: %w", err)
} }
@@ -162,7 +114,7 @@ func ReplaceTagInSpec(src *ImageSpec, digest v1.Hash) (*ImageSpec, error) {
} }
// so that the index tag is replaced with a tag unique to the image digest and doesn't overwrite it. // so that the index tag is replaced with a tag unique to the image digest and doesn't overwrite it.
func replaceTag(image string, digest v1.Hash) (string, error) { func ReplaceTag(image string, digest v1.Hash) (string, error) {
if strings.HasPrefix(image, LocalPrefix) { if strings.HasPrefix(image, LocalPrefix) {
return image, nil return image, nil
} }

View File

@@ -1,56 +1,65 @@
package oci package oci_test
import ( import (
"path/filepath"
"testing" "testing"
"github.com/distribution/reference"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/oci"
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/layout" "github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
func TestRefToPurl(t *testing.T) { func TestRefToPurl(t *testing.T) {
arm, err := ParsePlatform("arm64/linux") arm, err := oci.ParsePlatform("arm64/linux")
require.NoError(t, err) require.NoError(t, err)
purl, canonical, err := RefToPURL("alpine", arm) ref, err := reference.ParseNormalizedNamed("alpine")
require.NoError(t, err)
purl, canonical, err := oci.RefToPURL(ref, arm)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "pkg:docker/alpine@latest?platform=arm64%2Flinux", purl) assert.Equal(t, "pkg:docker/alpine@latest?platform=arm64%2Flinux", purl)
assert.False(t, canonical) assert.False(t, canonical)
ref, err = reference.ParseNormalizedNamed("alpine:123")
purl, canonical, err = RefToPURL("alpine:123", arm) require.NoError(t, err)
purl, canonical, err = oci.RefToPURL(ref, arm)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl) assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
assert.False(t, canonical) assert.False(t, canonical)
ref, err = reference.ParseNormalizedNamed("google/alpine:123")
purl, canonical, err = RefToPURL("google/alpine:123", arm) require.NoError(t, err)
purl, canonical, err = oci.RefToPURL(ref, arm)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "pkg:docker/google/alpine@123?platform=arm64%2Flinux", purl) assert.Equal(t, "pkg:docker/google/alpine@123?platform=arm64%2Flinux", purl)
assert.False(t, canonical) assert.False(t, canonical)
ref, err = reference.ParseNormalizedNamed("library/alpine:123")
purl, canonical, err = RefToPURL("library/alpine:123", arm) require.NoError(t, err)
purl, canonical, err = oci.RefToPURL(ref, arm)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl) assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
assert.False(t, canonical) assert.False(t, canonical)
ref, err = reference.ParseNormalizedNamed("docker.io/library/alpine:123")
purl, canonical, err = RefToPURL("docker.io/library/alpine:123", arm) require.NoError(t, err)
purl, canonical, err = oci.RefToPURL(ref, arm)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl) assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
assert.False(t, canonical) assert.False(t, canonical)
ref, err = reference.ParseNormalizedNamed("localhost:5001/library/alpine:123")
purl, canonical, err = RefToPURL("localhost:5001/library/alpine:123", arm) require.NoError(t, err)
purl, canonical, err = oci.RefToPURL(ref, arm)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "pkg:docker/localhost%3A5001/library/alpine@123?platform=arm64%2Flinux", purl) assert.Equal(t, "pkg:docker/localhost%3A5001/library/alpine@123?platform=arm64%2Flinux", purl)
assert.False(t, canonical) assert.False(t, canonical)
ref, err = reference.ParseNormalizedNamed("localhost:5001/alpine:123")
purl, canonical, err = RefToPURL("localhost:5001/alpine:123", arm) require.NoError(t, err)
purl, canonical, err = oci.RefToPURL(ref, arm)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine@123?platform=arm64%2Flinux", purl) assert.Equal(t, "pkg:docker/localhost%3A5001/alpine@123?platform=arm64%2Flinux", purl)
assert.False(t, canonical) assert.False(t, canonical)
ref, err = reference.ParseNormalizedNamed("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b")
purl, canonical, err = RefToPURL("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b", arm) require.NoError(t, err)
purl, canonical, err = oci.RefToPURL(ref, arm)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine?digest=sha256%3Ac5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b&platform=arm64%2Flinux", purl) assert.Equal(t, "pkg:docker/localhost%3A5001/alpine?digest=sha256%3Ac5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b&platform=arm64%2Flinux", purl)
assert.True(t, canonical) assert.True(t, canonical)
@@ -58,7 +67,7 @@ func TestRefToPurl(t *testing.T) {
// Test fix for https://github.com/docker/secure-artifacts-team-issues/issues/202 // Test fix for https://github.com/docker/secure-artifacts-team-issues/issues/202
func TestImageDigestForPlatform(t *testing.T) { func TestImageDigestForPlatform(t *testing.T) {
idx, err := layout.ImageIndexFromPath(UnsignedTestImage) idx, err := layout.ImageIndexFromPath(test.UnsignedTestImage)
assert.NoError(t, err) assert.NoError(t, err)
idxm, err := idx.IndexManifest() idxm, err := idx.IndexManifest()
@@ -72,16 +81,16 @@ func TestImageDigestForPlatform(t *testing.T) {
mfs2, err := mfs.IndexManifest() mfs2, err := mfs.IndexManifest()
assert.NoError(t, err) assert.NoError(t, err)
p, err := ParsePlatform("linux/amd64") p, err := oci.ParsePlatform("linux/amd64")
assert.NoError(t, err) assert.NoError(t, err)
desc, err := imageDescriptor(mfs2, p) desc, err := oci.ImageDescriptor(mfs2, p)
assert.NoError(t, err) assert.NoError(t, err)
digest := desc.Digest.String() digest := desc.Digest.String()
assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", digest) assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", digest)
p, err = ParsePlatform("linux/arm64") p, err = oci.ParsePlatform("linux/arm64")
assert.NoError(t, err) assert.NoError(t, err)
desc, err = imageDescriptor(mfs2, p) desc, err = oci.ImageDescriptor(mfs2, p)
assert.NoError(t, err) assert.NoError(t, err)
digest = desc.Digest.String() digest = desc.Digest.String()
assert.Equal(t, "sha256:7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e", digest) assert.Equal(t, "sha256:7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e", digest)
@@ -95,14 +104,14 @@ func TestWithoutTag(t *testing.T) {
{name: "image:tag", expected: "index.docker.io/library/image"}, {name: "image:tag", expected: "index.docker.io/library/image"},
{name: "image", expected: "index.docker.io/library/image"}, {name: "image", expected: "index.docker.io/library/image"},
{name: "image:sha256-digest.att", expected: "index.docker.io/library/image"}, {name: "image:sha256-digest.att", expected: "index.docker.io/library/image"},
{name: RegistryPrefix + "image:tag", expected: RegistryPrefix + "index.docker.io/library/image"}, {name: oci.RegistryPrefix + "image:tag", expected: oci.RegistryPrefix + "index.docker.io/library/image"},
{name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image"}, {name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image"},
{name: RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: RegistryPrefix + "index.docker.io/library/image"}, {name: oci.RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: oci.RegistryPrefix + "index.docker.io/library/image"},
{name: RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: RegistryPrefix + "127.0.0.1:36555/repo"}, {name: oci.RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: oci.RegistryPrefix + "127.0.0.1:36555/repo"},
} }
for _, c := range tc { for _, c := range tc {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
notag, _ := WithoutTag(c.name) notag, _ := oci.WithoutTag(c.name)
assert.Equal(t, c.expected, notag) assert.Equal(t, c.expected, notag)
}) })
} }
@@ -116,11 +125,11 @@ func TestReplaceTag(t *testing.T) {
{name: "image:tag", expected: "index.docker.io/library/image:sha256-digest.att"}, {name: "image:tag", expected: "index.docker.io/library/image:sha256-digest.att"},
{name: "image", expected: "index.docker.io/library/image:sha256-digest.att"}, {name: "image", expected: "index.docker.io/library/image:sha256-digest.att"},
{name: "image:sha256-digest.att", expected: "index.docker.io/library/image:sha256-digest.att"}, {name: "image:sha256-digest.att", expected: "index.docker.io/library/image:sha256-digest.att"},
{name: RegistryPrefix + "image:tag", expected: RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"}, {name: oci.RegistryPrefix + "image:tag", expected: oci.RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"},
{name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image:sha256-digest.att"}, {name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image:sha256-digest.att"},
{name: LocalPrefix + "foobar", expected: LocalPrefix + "foobar"}, {name: oci.LocalPrefix + "foobar", expected: oci.LocalPrefix + "foobar"},
{name: RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"}, {name: oci.RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: oci.RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"},
{name: RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: RegistryPrefix + "127.0.0.1:36555/repo:sha256-digest.att"}, {name: oci.RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: oci.RegistryPrefix + "127.0.0.1:36555/repo:sha256-digest.att"},
} }
digest := v1.Hash{ digest := v1.Hash{
@@ -129,7 +138,7 @@ func TestReplaceTag(t *testing.T) {
} }
for _, c := range tc { for _, c := range tc {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
replaced, err := replaceTag(c.name, digest) replaced, err := oci.ReplaceTag(c.name, digest)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, c.expected, replaced) assert.Equal(t, c.expected, replaced)
}) })

View File

@@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/docker/attest/pkg/attestation"
"github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/empty"
@@ -13,6 +12,7 @@ import (
"github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote"
) )
// PushImageToRegistry pushes an image to the registry with the specified name.
func PushImageToRegistry(image v1.Image, imageName string) error { func PushImageToRegistry(image v1.Image, imageName string) error {
ref, err := name.ParseReference(imageName) ref, err := name.ParseReference(imageName)
if err != nil { if err != nil {
@@ -23,6 +23,7 @@ func PushImageToRegistry(image v1.Image, imageName string) error {
return remote.Write(ref, image, MultiKeychainOption()) return remote.Write(ref, image, MultiKeychainOption())
} }
// PushIndexToRegistry pushes an index to the registry with the specified name.
func PushIndexToRegistry(index v1.ImageIndex, imageName string) error { func PushIndexToRegistry(index v1.ImageIndex, imageName string) error {
// Parse the index name // Parse the index name
ref, err := name.ParseReference(imageName) ref, err := name.ParseReference(imageName)
@@ -34,6 +35,7 @@ func PushIndexToRegistry(index v1.ImageIndex, imageName string) error {
return remote.WriteIndex(ref, index, MultiKeychainOption()) return remote.WriteIndex(ref, index, MultiKeychainOption())
} }
// SaveIndexAsOCILayout saves an image as an OCI layout to the specified path.
func SaveImageAsOCILayout(image v1.Image, path string) error { func SaveImageAsOCILayout(image v1.Image, path string) error {
// Save the image to the local filesystem // Save the image to the local filesystem
err := os.MkdirAll(path, os.ModePerm) err := os.MkdirAll(path, os.ModePerm)
@@ -48,6 +50,7 @@ func SaveImageAsOCILayout(image v1.Image, path string) error {
return l.AppendImage(image) return l.AppendImage(image)
} }
// SaveIndexAsOCILayout saves an index as an OCI layout to the specified path.
func SaveIndexAsOCILayout(image v1.ImageIndex, path string) error { func SaveIndexAsOCILayout(image v1.ImageIndex, path string) error {
// Save the index to the local filesystem // Save the index to the local filesystem
err := os.MkdirAll(path, os.ModePerm) err := os.MkdirAll(path, os.ModePerm)
@@ -62,6 +65,7 @@ func SaveIndexAsOCILayout(image v1.ImageIndex, path string) error {
return nil return nil
} }
// SaveIndex saves an index to the specified outputs.
func SaveIndex(outputs []*ImageSpec, index v1.ImageIndex, indexName string) error { func SaveIndex(outputs []*ImageSpec, index v1.ImageIndex, indexName string) error {
// split output by comma and write or push each one // split output by comma and write or push each one
for _, output := range outputs { for _, output := range outputs {
@@ -89,6 +93,7 @@ func SaveIndex(outputs []*ImageSpec, index v1.ImageIndex, indexName string) erro
return nil return nil
} }
// SaveImage saves an image to the specified output.
func SaveImage(output *ImageSpec, image v1.Image, imageName string) error { func SaveImage(output *ImageSpec, image v1.Image, imageName string) error {
if output.Type == OCI { if output.Type == OCI {
idx := v1.ImageIndex(empty.Index) idx := v1.ImageIndex(empty.Index)
@@ -113,26 +118,23 @@ func SaveImage(output *ImageSpec, image v1.Image, imageName string) error {
return nil return nil
} }
func SaveReferrers(manifest *attestation.Manifest, outputs []*ImageSpec) error { // SaveImagesNoTag saves a list of images by digest to the specified outputs.
func SaveImagesNoTag(images []v1.Image, outputs []*ImageSpec) error {
for _, output := range outputs { for _, output := range outputs {
// OCI layout output for referrers not supported // OCI layout output not supported
if output.Type == OCI { if output.Type == OCI {
continue continue
} }
images, err := manifest.BuildReferringArtifacts()
if err != nil {
return fmt.Errorf("failed to build image: %w", err)
}
for _, image := range images { for _, image := range images {
digest, err := image.Digest() digest, err := image.Digest()
if err != nil { if err != nil {
return fmt.Errorf("failed to get attestation image digest: %w", err) return fmt.Errorf("failed to get image digest: %w", err)
} }
attOut, err := ReplaceDigestInSpec(output, digest) spec, err := ReplaceDigestInSpec(output, digest)
if err != nil { if err != nil {
return fmt.Errorf("failed to create attestation image spec: %w", err) return fmt.Errorf("failed to create image spec: %w", err)
} }
err = PushImageToRegistry(image, attOut.Identifier) err = PushImageToRegistry(image, spec.Identifier)
if err != nil { if err != nil {
return fmt.Errorf("failed to push image: %w", err) return fmt.Errorf("failed to push image: %w", err)
} }

View File

@@ -4,11 +4,9 @@ import (
"fmt" "fmt"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"path/filepath"
"testing" "testing"
"github.com/docker/attest/internal/test" "github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/attest"
"github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/oci"
"github.com/google/go-containerregistry/pkg/registry" "github.com/google/go-containerregistry/pkg/registry"
@@ -19,9 +17,8 @@ import (
) )
func TestSavingIndex(t *testing.T) { func TestSavingIndex(t *testing.T) {
UnsignedTestImage := filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
outputLayout := test.CreateTempDir(t, "", "mirror-test") outputLayout := test.CreateTempDir(t, "", "mirror-test")
attIdx, err := oci.IndexFromPath(UnsignedTestImage) attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
require.NoError(t, err) require.NoError(t, err)
server := httptest.NewServer(registry.New()) server := httptest.NewServer(registry.New())
@@ -80,9 +77,9 @@ func TestSavingReferrers(t *testing.T) {
MediaType: "application/vnd.oci.image.manifest.v1+json", MediaType: "application/vnd.oci.image.manifest.v1+json",
Digest: digest, Digest: digest,
} }
manifest, err := attest.NewAttestationManifest(subject) manifest, err := attestation.NewManifest(subject)
require.NoError(t, err) require.NoError(t, err)
err = manifest.AddAttestation(ctx, signer, statement, opts) err = manifest.Add(ctx, signer, statement, opts)
require.NoError(t, err) require.NoError(t, err)
server := httptest.NewServer(registry.New(registry.WithReferrersSupport(true))) server := httptest.NewServer(registry.New(registry.WithReferrersSupport(true)))
defer server.Close() defer server.Close()
@@ -93,16 +90,18 @@ func TestSavingReferrers(t *testing.T) {
indexName := fmt.Sprintf("%s/repo:root", u.Host) indexName := fmt.Sprintf("%s/repo:root", u.Host)
output, err := oci.ParseImageSpecs(indexName) output, err := oci.ParseImageSpecs(indexName)
require.NoError(t, err) require.NoError(t, err)
err = oci.SaveReferrers(manifest, output) artifacts, err := manifest.BuildReferringArtifacts()
require.NoError(t, err)
err = oci.SaveImagesNoTag(artifacts, output)
require.NoError(t, err) require.NoError(t, err)
reg := &oci.MockRegistryResolver{ reg := &attestation.MockRegistryResolver{
Subject: subject, Subject: subject,
MockResolver: &oci.MockResolver{}, MockResolver: &attestation.MockResolver{},
ImageNameStr: indexName, ImageNameStr: indexName,
} }
require.NoError(t, err) require.NoError(t, err)
refResolver, err := oci.NewReferrersAttestationResolver(reg) refResolver, err := attestation.NewReferrersResolver(reg)
require.NoError(t, err) require.NoError(t, err)
attestations, err := refResolver.Attestations(ctx, attestation.VSAPredicateType) attestations, err := refResolver.Attestations(ctx, attestation.VSAPredicateType)
require.NoError(t, err) require.NoError(t, err)

View File

@@ -4,18 +4,11 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/docker/attest/pkg/attestation"
att "github.com/docker/attest/pkg/attestation"
"github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote"
) )
type RegistryResolver struct {
*RegistryImageDetailsResolver
*attestation.Manifest
}
type RegistryImageDetailsResolver struct { type RegistryImageDetailsResolver struct {
*ImageSpec *ImageSpec
descriptor *v1.Descriptor descriptor *v1.Descriptor
@@ -27,12 +20,6 @@ func NewRegistryImageDetailsResolver(src *ImageSpec) (*RegistryImageDetailsResol
}, nil }, nil
} }
func NewRegistryAttestationResolver(src *RegistryImageDetailsResolver) (*RegistryResolver, error) {
return &RegistryResolver{
RegistryImageDetailsResolver: src,
}, nil
}
func (r *RegistryImageDetailsResolver) ImageName(_ context.Context) (string, error) { func (r *RegistryImageDetailsResolver) ImageName(_ context.Context) (string, error) {
return r.Identifier, nil return r.Identifier, nil
} }
@@ -72,70 +59,3 @@ func (r *RegistryImageDetailsResolver) ImageDescriptor(ctx context.Context) (*v1
} }
return r.descriptor, nil return r.descriptor, nil
} }
func (r *RegistryResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
if r.Manifest == nil {
attest, err := FetchAttestationManifest(ctx, r.Identifier, r.ImageSpec.Platform)
if err != nil {
return nil, err
}
r.Manifest = attest
}
return ExtractEnvelopes(r.Manifest, predicateType)
}
func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Platform) (*attestation.Manifest, error) {
// we want to get to the image index, so ignoring platform for now
options := WithOptions(ctx, nil)
ref, err := name.ParseReference(image)
if err != nil {
return nil, fmt.Errorf("failed to parse reference: %w", err)
}
index, err := remote.Index(ref, options...)
if err != nil {
return nil, fmt.Errorf("failed to get index: %w", err)
}
indexManifest, err := index.IndexManifest()
if err != nil {
return nil, fmt.Errorf("failed to get index manifest: %w", err)
}
subjectDescriptor, err := imageDescriptor(indexManifest, platform)
if err != nil {
return nil, fmt.Errorf("failed to obtain image for platform: %w", err)
}
digest := subjectDescriptor.Digest.String()
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), digest))
if err != nil {
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
}
attestationDigest, err := attestationDigestForDigest(indexManifest, digest, "attestation-manifest")
if err != nil {
return nil, fmt.Errorf("failed to obtain attestation for image: %w", err)
}
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), attestationDigest))
if err != nil {
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
}
remoteDescriptor, err := remote.Get(ref, options...)
if err != nil {
return nil, fmt.Errorf("failed to get attestation: %w", err)
}
attestationImage, err := remoteDescriptor.Image()
if err != nil {
return nil, fmt.Errorf("failed to get attestation image: %w", err)
}
layers, err := attestation.GetAttestationsFromImage(attestationImage)
if err != nil {
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
}
attest := &attestation.Manifest{
OriginalLayers: layers,
OriginalDescriptor: &remoteDescriptor.Descriptor,
SubjectName: image,
SubjectDescriptor: subjectDescriptor,
}
return attest, nil
}

View File

@@ -3,15 +3,9 @@ package oci
import ( import (
"context" "context"
att "github.com/docker/attest/pkg/attestation"
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
) )
type AttestationResolver interface {
ImageDetailsResolver
Attestations(ctx context.Context, mediaType string) ([]*att.Envelope, error)
}
type ImageDetailsResolver interface { type ImageDetailsResolver interface {
ImageName(ctx context.Context) (string, error) ImageName(ctx context.Context) (string, error)
ImagePlatform(ctx context.Context) (*v1.Platform, error) ImagePlatform(ctx context.Context) (*v1.Platform, error)

View File

@@ -1,6 +1,8 @@
package oci package oci
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"strings" "strings"
@@ -26,12 +28,6 @@ type (
} }
) )
type AttestationOptions struct {
NoReferrers bool
Attach bool
ReferrersRepo string
}
type ImageSpecOption func(*ImageSpec) error type ImageSpecOption func(*ImageSpec) error
type ImageSpec struct { type ImageSpec struct {
@@ -180,3 +176,42 @@ func WithoutTag(image string) (string, error) {
repo := ref.Context().Name() repo := ref.Context().Name()
return prefix + repo, nil return prefix + repo, nil
} }
type EmptyConfigImage struct {
v1.Image
}
func (i *EmptyConfigImage) RawConfigFile() ([]byte, error) {
return []byte("{}"), nil
}
func (i *EmptyConfigImage) Manifest() (*v1.Manifest, error) {
mf, err := i.Image.Manifest()
if err != nil {
return nil, fmt.Errorf("failed to get manifest: %w", err)
}
mf.Config = v1.Descriptor{
MediaType: "application/vnd.oci.empty.v1+json",
Size: 2,
Digest: v1.Hash{Algorithm: "sha256", Hex: "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"},
Data: []byte("{}"),
}
return mf, nil
}
func (i *EmptyConfigImage) RawManifest() ([]byte, error) {
mf, err := i.Manifest()
if err != nil {
return nil, fmt.Errorf("failed to get manifest: %w", err)
}
return json.Marshal(mf)
}
func (i *EmptyConfigImage) Digest() (v1.Hash, error) {
mb, err := i.RawManifest()
if err != nil {
return v1.Hash{}, err
}
digest, _, err := v1.SHA256(bytes.NewReader(mb))
return digest, err
}

21
pkg/oci/types_test.go Normal file
View File

@@ -0,0 +1,21 @@
package oci
import (
"testing"
"github.com/docker/attest/internal/util"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEmptyConfigImageDigest(t *testing.T) {
empty := empty.Image
img := EmptyConfigImage{Image: empty}
mf, err := img.RawManifest()
require.NoError(t, err)
hash := util.SHA256Hex(mf)
digest, err := img.Digest()
require.NoError(t, err)
assert.Equal(t, digest.Hex, hash)
}

2
pkg/policy/README.md Normal file
View File

@@ -0,0 +1,2 @@
## policy
This package is for attestation policy mapping and evaluation.

View File

@@ -4,7 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/attestation"
) )
type policyEvaluatorCtxKeyType struct{} type policyEvaluatorCtxKeyType struct{}
@@ -26,5 +26,5 @@ func GetPolicyEvaluator(ctx context.Context) (Evaluator, error) {
} }
type Evaluator interface { type Evaluator interface {
Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *Input) (*Result, error) Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error)
} }

View File

@@ -3,14 +3,14 @@ package policy
import ( import (
"context" "context"
"github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/attestation"
) )
type MockPolicyEvaluator struct { type MockPolicyEvaluator struct {
EvaluateFunc func(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *Input) (*Result, error) EvaluateFunc func(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error)
} }
func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *Input) (*Result, error) { func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error) {
if pe.EvaluateFunc != nil { if pe.EvaluateFunc != nil {
return pe.EvaluateFunc(ctx, resolver, pctx, input) return pe.EvaluateFunc(ctx, resolver, pctx, input)
} }
@@ -19,7 +19,7 @@ func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver oci.Attest
func GetMockPolicy() Evaluator { func GetMockPolicy() Evaluator {
return &MockPolicyEvaluator{ return &MockPolicyEvaluator{
EvaluateFunc: func(_ context.Context, _ oci.AttestationResolver, _ *Policy, _ *Input) (*Result, error) { EvaluateFunc: func(_ context.Context, _ attestation.Resolver, _ *Policy, _ *Input) (*Result, error) {
return AllowedResult(), nil return AllowedResult(), nil
}, },
} }

View File

@@ -8,14 +8,19 @@ 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/config" "github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/tuf"
) )
func resolveLocalPolicy(opts *Options, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) { func resolveLocalPolicy(opts *Options, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
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
@@ -28,10 +33,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
@@ -39,22 +58,38 @@ func resolveLocalPolicy(opts *Options, mapping *config.PolicyMapping, imageName
return policy, nil return policy, nil
} }
func resolveTUFPolicy(opts *Options, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) { func resolveTUFPolicy(opts *Options, tufClient tuf.Downloader, 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 := 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
@@ -120,7 +155,7 @@ func findPolicyMatchImpl(imageName string, mappings *config.PolicyMappings, matc
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
} }
func resolvePolicyByID(opts *Options) (*Policy, error) { func resolvePolicyByID(opts *Options, tufClient tuf.Downloader) (*Policy, error) {
if opts.PolicyID != "" { if opts.PolicyID != "" {
localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir) localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir)
if err != nil { if err != nil {
@@ -134,21 +169,21 @@ func resolvePolicyByID(opts *Options) (*Policy, error) {
} }
// must check tuf // must check tuf
tufMappings, err := config.LoadTUFMappings(opts.TUFClient, opts.LocalTargetsDir) tufMappings, err := config.LoadTUFMappings(tufClient, opts.LocalTargetsDir)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err) return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err)
} }
policy := tufMappings.Policies[opts.PolicyID] policy := tufMappings.Policies[opts.PolicyID]
if policy != nil { if policy != nil {
return resolveTUFPolicy(opts, policy, "", "") return resolveTUFPolicy(opts, tufClient, policy, "", "")
} }
return nil, fmt.Errorf("policy with id %s not found", opts.PolicyID) return nil, fmt.Errorf("policy with id %s not found", opts.PolicyID)
} }
return nil, nil return nil, nil
} }
func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver, opts *Options) (*Policy, error) { func ResolvePolicy(ctx context.Context, tufClient tuf.Downloader, detailsResolver oci.ImageDetailsResolver, opts *Options) (*Policy, error) {
p, err := resolvePolicyByID(opts) p, err := resolvePolicyByID(opts, tufClient)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to resolve policy by id: %w", err) return nil, fmt.Errorf("failed to resolve policy by id: %w", err)
} }
@@ -175,7 +210,7 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver
return resolveLocalPolicy(opts, match.policy, imageName, match.matchedName) return resolveLocalPolicy(opts, match.policy, imageName, match.matchedName)
} }
// must check tuf // must check tuf
tufMappings, err := config.LoadTUFMappings(opts.TUFClient, opts.LocalTargetsDir) tufMappings, err := config.LoadTUFMappings(tufClient, opts.LocalTargetsDir)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err) return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err)
} }
@@ -184,7 +219,7 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver
if match.matchType == matchTypeMatchNoPolicy { if match.matchType == matchTypeMatchNoPolicy {
for _, mapping := range tufMappings.Policies { for _, mapping := range tufMappings.Policies {
if mapping.ID == match.rule.PolicyID { if mapping.ID == match.rule.PolicyID {
return resolveTUFPolicy(opts, mapping, imageName, match.matchedName) return resolveTUFPolicy(opts, tufClient, mapping, imageName, match.matchedName)
} }
} }
} }
@@ -195,7 +230,7 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver
return nil, err return nil, err
} }
if match.matchType == matchTypePolicy { if match.matchType == matchTypePolicy {
return resolveTUFPolicy(opts, match.policy, imageName, match.matchedName) return resolveTUFPolicy(opts, tufClient, match.policy, imageName, match.matchedName)
} }
return nil, nil return nil, nil
} }
@@ -211,28 +246,28 @@ func normalizeImageName(imageName string) (string, error) {
func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsResolver, error) { func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsResolver, error) {
switch imageSource.Type { switch imageSource.Type {
case oci.OCI: case oci.OCI:
return oci.NewOCILayoutAttestationResolver(imageSource) return attestation.NewOCILayoutResolver(imageSource)
case oci.Docker: case oci.Docker:
return oci.NewRegistryImageDetailsResolver(imageSource) return oci.NewRegistryImageDetailsResolver(imageSource)
} }
return nil, fmt.Errorf("unsupported image source type: %s", imageSource.Type) return nil, fmt.Errorf("unsupported image source type: %s", imageSource.Type)
} }
func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *config.PolicyMapping) (oci.AttestationResolver, error) { func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *config.PolicyMapping) (attestation.Resolver, error) {
if mapping.Attestations != nil { if mapping.Attestations != nil {
if mapping.Attestations.Style == config.AttestationStyleAttached { if mapping.Attestations.Style == config.AttestationStyleAttached {
switch resolver := resolver.(type) { switch resolver := resolver.(type) {
case *oci.RegistryImageDetailsResolver: case *oci.RegistryImageDetailsResolver:
return oci.NewRegistryAttestationResolver(resolver) return attestation.NewRegistryResolver(resolver)
case *oci.LayoutResolver: case *attestation.LayoutResolver:
return resolver, nil return resolver, nil
default: default:
return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver) return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver)
} }
} }
if mapping.Attestations.Repo != "" { if mapping.Attestations.Repo != "" {
return oci.NewReferrersAttestationResolver(resolver, oci.WithReferrersRepo(mapping.Attestations.Repo)) return attestation.NewReferrersResolver(resolver, attestation.WithReferrersRepo(mapping.Attestations.Repo))
} }
} }
return oci.NewReferrersAttestationResolver(resolver) return attestation.NewReferrersResolver(resolver)
} }

View File

@@ -32,47 +32,49 @@ 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")
re := policy.NewRegoEvaluator(true) re := policy.NewRegoEvaluator(true)
defaultResolver := oci.MockResolver{ defaultResolver := attestation.MockResolver{
Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)}, Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)},
} }
testCases := []struct { testCases := []struct {
repo string repo string
expectSuccess bool expectSuccess bool
isCanonical bool isCanonical bool
resolver oci.AttestationResolver 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, resolver: defaultResolver},
{repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver, policyID: "docker-official-images"}, {repo: "testdata/mock-tuf-allow", expectSuccess: true, 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", 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", resolver: defaultResolver},
{repo: "testdata/mock-tuf-verify-sig", expectSuccess: true, isCanonical: false, resolver: defaultResolver}, {repo: "testdata/mock-tuf-verify-sig", expectSuccess: true, resolver: defaultResolver},
{repo: "testdata/mock-tuf-wrong-key", expectSuccess: false, isCanonical: false, resolver: defaultResolver}, {repo: "testdata/mock-tuf-wrong-key", 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", resolver: defaultResolver},
{repo: "testdata/mock-tuf-no-rego", resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.repo, func(t *testing.T) { t.Run(tc.repo, func(t *testing.T) {
input := &policy.Input{ input := &policy.Input{
Digest: "sha256:test-digest", Digest: "sha256:test-digest",
PURL: "test-purl", PURL: "test-purl",
IsCanonical: tc.isCanonical, }
if !tc.isCanonical {
input.Tag = "test"
} }
tufClient := tuf.NewMockTufClient(tc.repo, test.CreateTempDir(t, "", "tuf-dest")) tufClient := tuf.NewMockTufClient(tc.repo, test.CreateTempDir(t, "", "tuf-dest"))
if tc.policy == nil { if tc.policy == nil {
tc.policy = &policy.Options{ tc.policy = &policy.Options{
TUFClient: tufClient,
LocalTargetsDir: test.CreateTempDir(t, "", "tuf-targets"), LocalTargetsDir: test.CreateTempDir(t, "", "tuf-targets"),
PolicyID: tc.policyID, PolicyID: tc.policyID,
} }
@@ -85,10 +87,10 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
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, tufClient, 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")
@@ -117,10 +119,10 @@ func TestLoadingMappings(t *testing.T) {
} }
func TestCreateAttestationResolver(t *testing.T) { func TestCreateAttestationResolver(t *testing.T) {
mockResolver := oci.MockResolver{ mockResolver := attestation.MockResolver{
Envs: []*attestation.Envelope{}, Envs: []*attestation.Envelope{},
} }
layoutResolver := &oci.LayoutResolver{} layoutResolver := &attestation.LayoutResolver{}
registryResolver := &oci.RegistryImageDetailsResolver{} registryResolver := &oci.RegistryImageDetailsResolver{}
nilRepoReferrers := &config.PolicyMapping{ nilRepoReferrers := &config.PolicyMapping{
@@ -166,11 +168,11 @@ func TestCreateAttestationResolver(t *testing.T) {
return return
} }
switch resolver.(type) { switch resolver.(type) {
case *oci.ReferrersResolver: case *attestation.ReferrersResolver:
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleReferrers) assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleReferrers)
case *oci.RegistryResolver: case *attestation.RegistryResolver:
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached) assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached)
case *oci.LayoutResolver: case *attestation.LayoutResolver:
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached) assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached)
} }
}) })

View File

@@ -7,8 +7,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
att "github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/oci"
intoto "github.com/in-toto/in-toto-golang/in_toto" intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/rego" "github.com/open-policy-agent/opa/rego"
@@ -36,7 +35,7 @@ func NewRegoEvaluator(debug bool) Evaluator {
} }
} }
func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *Input) (*Result, error) { func (re *regoEvaluator) Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error) {
var regoOpts []func(*rego.Rego) var regoOpts []func(*rego.Rego)
// Create a new in-memory store // Create a new in-memory store
@@ -170,7 +169,7 @@ func handleErrors2(f func(rCtx *rego.BuiltinContext, a, b *ast.Term) (*ast.Term,
} }
} }
func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin { func RegoFunctions(resolver attestation.Resolver) []*tester.Builtin {
return []*tester.Builtin{ return []*tester.Builtin{
{ {
Decl: verifyDecl, Decl: verifyDecl,
@@ -197,7 +196,7 @@ func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin {
} }
} }
func fetchInTotoAttestations(resolver oci.AttestationResolver) rego.Builtin1 { func fetchInTotoAttestations(resolver attestation.Resolver) rego.Builtin1 {
return func(rCtx rego.BuiltinContext, predicateTypeTerm *ast.Term) (*ast.Term, error) { return func(rCtx rego.BuiltinContext, predicateTypeTerm *ast.Term) (*ast.Term, error) {
predicateTypeStr, ok := predicateTypeTerm.Value.(ast.String) predicateTypeStr, ok := predicateTypeTerm.Value.(ast.String)
if !ok { if !ok {
@@ -228,8 +227,8 @@ func fetchInTotoAttestations(resolver oci.AttestationResolver) rego.Builtin1 {
} }
func verifyInTotoEnvelope(rCtx *rego.BuiltinContext, envTerm, optsTerm *ast.Term) (*ast.Term, error) { func verifyInTotoEnvelope(rCtx *rego.BuiltinContext, envTerm, optsTerm *ast.Term) (*ast.Term, error) {
env := new(att.Envelope) env := new(attestation.Envelope)
opts := new(att.VerifyOptions) opts := new(attestation.VerifyOptions)
err := ast.As(envTerm.Value, env) err := ast.As(envTerm.Value, env)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to cast envelope: %w", err) return nil, fmt.Errorf("failed to cast envelope: %w", err)
@@ -239,7 +238,7 @@ func verifyInTotoEnvelope(rCtx *rego.BuiltinContext, envTerm, optsTerm *ast.Term
return nil, fmt.Errorf("failed to cast verifier options: %w", err) return nil, fmt.Errorf("failed to cast verifier options: %w", err)
} }
payload, err := att.VerifyDSSE(rCtx.Context, env, opts) payload, err := attestation.VerifyDSSE(rCtx.Context, env, opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -2,6 +2,10 @@ package attest
import rego.v1 import rego.v1
result := { default canonical = false
"success": input.isCanonical,
canonical if {
not input.tag
} }
result := {"success": canonical}

View File

@@ -0,0 +1 @@
policy: "this is not rego"

View 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

View File

@@ -27,7 +27,7 @@ type Result struct {
} }
type Options struct { type Options struct {
TUFClient tuf.Downloader TUFClientOptions *tuf.ClientOptions
LocalTargetsDir string LocalTargetsDir string
LocalPolicyDir string LocalPolicyDir string
PolicyID string PolicyID string
@@ -40,12 +40,18 @@ 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 {
Digest string `json:"digest"` Digest string `json:"digest"`
PURL string `json:"purl"` PURL string `json:"purl"`
IsCanonical bool `json:"isCanonical"` Tag string `json:"tag,omitempty"`
Domain string `json:"domain"`
NormalizedName string `json:"normalized_name"`
FamiliarName string `json:"familiar_name"`
Platform string `json:"platform"`
} }
type File struct { type File struct {

View File

@@ -0,0 +1,2 @@
## signerverifier
This package implements methods to sign and verify attestation envelopes.

2
pkg/tlog/README.md Normal file
View File

@@ -0,0 +1,2 @@
## tlog
This package implements transparency logging.

2
pkg/tuf/README.md Normal file
View File

@@ -0,0 +1,2 @@
## tuf
This package implements TUF clients for http and oci data sources.

View File

@@ -4,7 +4,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/pkg/tuf" "github.com/docker/attest/pkg/tuf"
"github.com/theupdateframework/go-tuf/v2/metadata" "github.com/theupdateframework/go-tuf/v2/metadata"
) )
@@ -21,23 +20,20 @@ func ExampleNewClient_registry() {
metadataURI := "registry-1.docker.io/docker/tuf-metadata:latest" metadataURI := "registry-1.docker.io/docker/tuf-metadata:latest"
targetsURI := "registry-1.docker.io/docker/tuf-targets" targetsURI := "registry-1.docker.io/docker/tuf-targets"
registryClient, err := tuf.NewClient(embed.RootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker()) registryClient, err := tuf.NewClient(&tuf.ClientOptions{tuf.DockerTUFRootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker()})
if err != nil { if err != nil {
panic(err) panic(err)
} }
// 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)
} }

View File

@@ -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 {

View File

@@ -9,7 +9,6 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/internal/util" "github.com/docker/attest/internal/util"
"github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/oci"
"github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/crane"
@@ -32,6 +31,9 @@ const (
tufTargetMediaType = "application/vnd.tuf.target" tufTargetMediaType = "application/vnd.tuf.target"
testRole = "test-role" testRole = "test-role"
tufMetadataRepo = "tuf-metadata" tufMetadataRepo = "tuf-metadata"
targetsPath = "/tuf-targets"
metadataPath = "/tuf-metadata"
targetsRepo = "test" + targetsPath
) )
func TestRegistryFetcher(t *testing.T) { func TestRegistryFetcher(t *testing.T) {
@@ -44,16 +46,16 @@ func TestRegistryFetcher(t *testing.T) {
}() }()
LoadRegistryTestData(t, regAddr, OCITUFTestDataPath) LoadRegistryTestData(t, regAddr, OCITUFTestDataPath)
metadataRepo := regAddr.Host + "/tuf-metadata" metadataRepo := regAddr.Host + metadataPath
metadataImgTag := LatestTag metadataImgTag := LatestTag
targetsRepo := regAddr.Host + "/tuf-targets" targetsRepo := regAddr.Host + targetsPath
targetFile := "test.txt" targetFile := "test.txt"
delegatedRole := testRole delegatedRole := testRole
dir := CreateTempDir(t, "", "tuf_temp") dir := CreateTempDir(t, "", "tuf_temp")
delegatedDir := CreateTempDir(t, dir, delegatedRole) delegatedDir := CreateTempDir(t, dir, delegatedRole)
delegatedTargetFile := fmt.Sprintf("%s/%s", delegatedRole, targetFile) delegatedTargetFile := fmt.Sprintf("%s/%s", delegatedRole, targetFile)
cfg, err := config.New(metadataRepo, embed.RootDev.Data) cfg, err := config.New(metadataRepo, DockerTUFRootDev.Data)
assert.NoError(t, err) assert.NoError(t, err)
cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataImgTag, targetsRepo) cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataImgTag, targetsRepo)
@@ -122,7 +124,7 @@ func TestFindFileInManifest(t *testing.T) {
// make test image manifest // make test image manifest
file := "test.json" file := "test.json"
data := []byte("test") data := []byte("test")
hash := v1.Hash{Algorithm: "sha256", Hex: util.SHA256Hex(data)} hash := v1.Hash{Hex: util.SHA256Hex(data)}
img := empty.Image img := empty.Image
img = mutate.MediaType(img, types.OCIManifestSchema1) img = mutate.MediaType(img, types.OCIManifestSchema1)
img = mutate.ConfigMediaType(img, types.OCIConfigJSON) img = mutate.ConfigMediaType(img, types.OCIConfigJSON)
@@ -150,7 +152,6 @@ func TestFindFileInManifest(t *testing.T) {
indexManifest, err := idx.RawManifest() indexManifest, err := idx.RawManifest()
assert.NoError(t, err) assert.NoError(t, err)
// cache image layer // cache image layer
targetsRepo := "test/tuf-targets"
d := &RegistryFetcher{ d := &RegistryFetcher{
cache: NewImageCache(), cache: NewImageCache(),
targetsRepo: targetsRepo, targetsRepo: targetsRepo,
@@ -183,9 +184,8 @@ func TestFindFileInManifest(t *testing.T) {
} }
func TestParseImgRef(t *testing.T) { func TestParseImgRef(t *testing.T) {
metadataRepo := "test/tuf-metadata" metadataRepo := "test" + metadataPath
metadataTag := LatestTag metadataTag := LatestTag
targetsRepo := "test/tuf-targets"
delegatedRole := testRole delegatedRole := testRole
testCases := []struct { testCases := []struct {
name string name string

View File

@@ -1,6 +1,7 @@
package tuf package tuf
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
@@ -20,6 +21,24 @@ import (
"github.com/theupdateframework/go-tuf/v2/metadata/updater" "github.com/theupdateframework/go-tuf/v2/metadata/updater"
) )
type tufCtxKeyType struct{}
var DownloaderCtxKey tufCtxKeyType
// WithDownloader sets Downloader in context.
func WithDownloader(ctx context.Context, downloader Downloader) context.Context {
return context.WithValue(ctx, DownloaderCtxKey, downloader)
}
// GetDownloader returns the Downloader from context and `true` if it exists, otherwise `nil` and `false`.
func GetDownloader(ctx context.Context) (Downloader, bool) {
t, ok := ctx.Value(DownloaderCtxKey).(Downloader)
if !ok {
return nil, false
}
return t, true
}
type Source string type Source string
const ( const (
@@ -35,8 +54,13 @@ var (
DockerTUFRootDefault = embed.RootDefault DockerTUFRootDefault = embed.RootDefault
) )
const (
defaultMetadataSource = "docker/tuf-metadata:latest"
defaultTargetsSource = "docker/tuf-targets"
)
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,19 +68,44 @@ type Client struct {
cfg *config.UpdaterConfig cfg *config.UpdaterConfig
} }
type TargetFile struct {
ActualFilePath string
TargetURI string
Digest string
Data []byte
}
type ClientOptions struct {
InitialRoot []byte
Path string
MetadataSource string
TargetsSource string
VersionChecker VersionChecker
}
func NewDockerDefaultClientOptions(tufPath string) *ClientOptions {
return &ClientOptions{
InitialRoot: DockerTUFRootDefault.Data,
Path: tufPath,
MetadataSource: defaultMetadataSource,
TargetsSource: defaultTargetsSource,
VersionChecker: NewDefaultVersionChecker(),
}
}
// 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(opts *ClientOptions) (*Client, error) {
var tufSource Source var tufSource Source
if strings.HasPrefix(metadataSource, "https://") || strings.HasPrefix(metadataSource, "http://") { if strings.HasPrefix(opts.MetadataSource, "https://") || strings.HasPrefix(opts.MetadataSource, "http://") {
tufSource = HTTPSource tufSource = HTTPSource
} else { } else {
tufSource = OCISource tufSource = OCISource
} }
tufRootDigest := util.SHA256Hex(initialRoot) tufRootDigest := util.SHA256Hex(opts.InitialRoot)
// create a directory for each initial root.json // create a directory for each initial root.json
metadataPath := filepath.Join(tufPath, tufRootDigest) metadataPath := filepath.Join(opts.Path, tufRootDigest)
err := os.MkdirAll(metadataPath, os.ModePerm) err := os.MkdirAll(metadataPath, os.ModePerm)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create directory '%s': %w", metadataPath, err) return nil, fmt.Errorf("failed to create directory '%s': %w", metadataPath, err)
@@ -69,29 +118,29 @@ func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string
return nil, fmt.Errorf("failed to read root.json: %w", err) return nil, fmt.Errorf("failed to read root.json: %w", err)
} }
// write the root.json file to the metadata directory // write the root.json file to the metadata directory
err = os.WriteFile(rootFile, initialRoot, 0o666) // #nosec G306 err = os.WriteFile(rootFile, opts.InitialRoot, 0o666) // #nosec G306
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to write root.json %w", err) return nil, fmt.Errorf("Failed to write root.json %w", err)
} }
rootBytes = initialRoot rootBytes = opts.InitialRoot
} }
// create updater configuration // create updater configuration
cfg, err := config.New(metadataSource, rootBytes) // default config cfg, err := config.New(opts.MetadataSource, rootBytes) // default config
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create TUF updater configuration: %w", err) return nil, fmt.Errorf("failed to create TUF updater configuration: %w", err)
} }
cfg.LocalMetadataDir = metadataPath cfg.LocalMetadataDir = metadataPath
cfg.LocalTargetsDir = filepath.Join(metadataPath, "download") cfg.LocalTargetsDir = filepath.Join(metadataPath, "download")
cfg.RemoteTargetsURL = targetsSource cfg.RemoteTargetsURL = opts.TargetsSource
if tufSource == OCISource { if tufSource == OCISource {
metadataRepo, metadataTag, found := strings.Cut(metadataSource, ":") metadataRepo, metadataTag, found := strings.Cut(opts.MetadataSource, ":")
if !found { if !found {
fmt.Printf("metadata tag not found in URL, using latest\n") fmt.Printf("metadata tag not found in URL, using latest\n")
metadataTag = LatestTag metadataTag = LatestTag
} }
cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataTag, targetsSource) cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataTag, opts.TargetsSource)
} }
// create a new Updater instance // create a new Updater instance
@@ -111,7 +160,7 @@ func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string
cfg: cfg, cfg: cfg,
} }
err = versionChecker.CheckVersion(client) err = opts.VersionChecker.CheckVersion(client)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -119,40 +168,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 {

View File

@@ -9,8 +9,8 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/docker/attest/internal/embed"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/theupdateframework/go-tuf/v2/metadata" "github.com/theupdateframework/go-tuf/v2/metadata"
) )
@@ -65,18 +65,18 @@ func TestRootInit(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
_, err := NewClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker) _, err := NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
assert.NoErrorf(t, err, "Failed to create TUF client: %v", err) assert.NoErrorf(t, err, "Failed to create TUF client: %v", err)
// recreation should work with same root // recreation should work with same root
_, err = NewClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker) _, err = NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
assert.NoErrorf(t, err, "Failed to recreate TUF client: %v", err) assert.NoErrorf(t, err, "Failed to recreate TUF client: %v", err)
_, err = NewClient([]byte("broken"), tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker) _, err = NewClient(&ClientOptions{[]byte("broken"), tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
assert.Errorf(t, err, "Expected error recreating TUF client with broken root: %v", err) assert.Errorf(t, err, "Expected error recreating TUF client with broken root: %v", err)
_, err = NewClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysBadVersionChecker) _, err = NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysBadVersionChecker})
assert.Errorf(t, err, "Expected error creating TUF client with bad attest version: %v", err) assert.Errorf(t, err, "Expected error recreating TUF client with bad version checker")
} }
} }
@@ -108,11 +108,13 @@ func TestDownloadTarget(t *testing.T) {
}{ }{
{"http", server.URL + "/metadata", server.URL + "/targets"}, {"http", server.URL + "/metadata", server.URL + "/targets"},
{"oci", regAddr.Host + "/tuf-metadata:latest", regAddr.Host + "/tuf-targets"}, {"oci", regAddr.Host + "/tuf-metadata:latest", regAddr.Host + "/tuf-targets"},
{"http, download before init", server.URL + "/metadata", server.URL + "/targets"},
} }
for _, tc := range testCases { for _, tc := range testCases {
tufClient, err := NewClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker) tufClient, err := NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
assert.NoErrorf(t, err, "Failed to create TUF client: %v", err) require.NoErrorf(t, err, "Failed to create TUF client: %v", err)
require.NotNil(t, tufClient.updater, "Failed to create updater")
// get trusted tuf metadata // get trusted tuf metadata
trustedMetadata := tufClient.updater.GetTrustedMetadataSet() trustedMetadata := tufClient.updater.GetTrustedMetadataSet()
@@ -122,14 +124,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)
} }
} }

View File

@@ -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)
} }

4
scripts/README.md Normal file
View File

@@ -0,0 +1,4 @@
## scripts
This directory contains project scripts.
`gen-testdata.sh` - used to generate static test data saved in `/test/testdata/`

2
test/README.md Normal file
View File

@@ -0,0 +1,2 @@
## test
This directory contains static `testdata` used to run go tests.

View File

@@ -0,0 +1,43 @@
package attest
import rego.v1
keys := [{
"id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4",
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgH23D1i2+ZIOtVjmfB7iFvX8AhVN\n9CPJ4ie9axw+WRHozGnRy99U2dRge3zueBBg2MweF0zrToXGig2v3YOrdw==\n-----END PUBLIC KEY-----",
"from": "2023-12-15T14:00:00Z",
"to": null,
"status": "active",
"signing-format": "dssev1",
}]
provs(pred) := p if {
res := attest.fetch(pred)
not res.error
p := res.value
}
atts := union({
provs("https://slsa.dev/provenance/v0.2"),
provs("https://spdx.dev/Document"),
})
success if {
input.domain == "docker.io"
input.familiar_name == "test-image"
input.normalized_name == "library/test-image"
input.platform == "linux/amd64"
input.tag == "test"
}
result := {
"success": success,
"violations": set(),
"attestations": set(),
"summary": {
"subjects": set(),
"slsa_level": "SLSA_BUILD_LEVEL_3",
"verifier": "docker-official-images",
"policy_uri": "https://docker.com/official/policy/v0.1",
},
}

View File

@@ -0,0 +1,18 @@
version: v1
kind: policy-mapping
policies:
- id: docker-official-images
description: Docker Official Images
files:
- path: doi/policy.rego
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images
- pattern: "repo$"
policy-id: docker-official-images
- pattern: "test-image$"
policy-id: docker-official-images
- pattern: "image-signer-verifier-test$"
policy-id: docker-official-images
- pattern: "library/(.*)$"
rewrite: docker.io/library/$1

View File

@@ -38,7 +38,7 @@ subjects contains subject if {
} }
success if { success if {
print("input:",input) # print("input:",input)
true true
} }

View File

@@ -0,0 +1,4 @@
version: v1
kind: policy-mapping
policies:
rules: