10 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
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
27 changed files with 425 additions and 176 deletions

View File

@@ -50,7 +50,7 @@ jobs:
token: ${{ secrets.TC_CLOUD_TOKEN }}
- name: go test including e2e
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
if: matrix.os == 'macos-latest' || github.actor == 'dependabot[bot]'
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
- `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

32
go.mod
View File

@@ -4,7 +4,7 @@ go 1.22.5
require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/aws/aws-sdk-go-v2/config v1.27.27
github.com/aws/aws-sdk-go-v2/config v1.27.28
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8
github.com/containerd/platforms v0.2.1
github.com/distribution/reference v0.6.0
@@ -24,7 +24,7 @@ require (
github.com/stretchr/testify v1.9.0
github.com/testcontainers/testcontainers-go/modules/registry v0.32.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
)
@@ -33,7 +33,7 @@ replace github.com/google/go-containerregistry => github.com/docker/go-container
require (
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/compute/metadata v0.5.0 // 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/agnivade/levenshtein v1.1.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.4 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.28 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ecr v1.29.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.24.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
github.com/aws/smithy-go v1.20.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.4 // indirect
github.com/aws/smithy-go v1.20.4 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
@@ -77,7 +77,7 @@ require (
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect
github.com/docker/cli v27.1.1+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/go-connections 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.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
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.7.3/go.mod h1:HJtWUx1P5eqjy/f6Iq5KeytNpbAcGolPhOgyop2LlzA=
cloud.google.com/go/auth v0.8.1 h1:QZW9FjC5lZzN864p13YxvAtGUlQ+KgRL+8Sg45Z6vxo=
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/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
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/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8=
github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
github.com/aws/aws-sdk-go-v2/config v1.27.28 h1:OTxWGW/91C61QlneCtnD62NLb4W616/NM1jA8LhJqbg=
github.com/aws/aws-sdk-go-v2/config v1.27.28/go.mod h1:uzVRVtJSU5EFv6Fu82AoVFKozJi2ZCY6WRCXj06rbvs=
github.com/aws/aws-sdk-go-v2/credentials v1.17.28 h1:m8+AHY/ND8CMHJnPoH7PJIRakWGa4gbfbxuY9TGTUXM=
github.com/aws/aws-sdk-go-v2/credentials v1.17.28/go.mod h1:6TF7dSc78ehD1SL6KpRIPKMA1GyyWflIkjqg+qmf4+c=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 h1:yjwoSyDZF8Jth+mUk5lSPJCkMC0lMy6FaCD51jm6ayE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12/go.mod h1:fuR57fAgMk7ot3WcNQfb6rSEn+SUffl7ri+aa8uKysI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16/go.mod h1:2DwJF39FlNAUiX5pAc0UNeiz16lK2t7IaFcm0LFHEgc=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 h1:jYfy8UPmd+6kJW5YhY0L1/KftReOGxI/4NtVSTh9O/I=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16/go.mod h1:7ZfEPZxkW42Afq4uQB8H2E2e6ebh6mXTueEpYzjCzcs=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/service/ecr v1.29.1 h1:ywNLJrn/Qn4enDsz/XnKlvpnLqvJxFGQV2BltWltbis=
github.com/aws/aws-sdk-go-v2/service/ecr v1.29.1/go.mod h1:WadVIk+UrTvWuAsCp6BKGX4i2snurpz8mPWhJQnS7Dg=
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.24.1 h1:Eq9i/mvOlGghiKe9NtsmeD9Wlwg8p4fbsqrMb3nWirM=
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.24.1/go.mod h1:VtOgEoLEPV1YADuq+Z2XOK6/wKkGW2YK6DjChZ/GvDs=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18/go.mod h1:++NHzT+nAF7ZPrHPsA+ENvsXkOO8wEu+C6RXltAG4/c=
github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 h1:UPTdlTOwWUX49fVi7cymEN6hDqCwe3LNv1vi7TXUutk=
github.com/aws/aws-sdk-go-v2/service/kms v1.35.3/go.mod h1:gjDP16zn+WWalyaUqwCCioQ8gU8lzttCCc9jYsiQI/8=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 h1:zCsFCKvbj25i7p1u94imVoO447I/sFv8qq+lGJhRN0c=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.5/go.mod h1:ZeDX1SnKsVlejeuz41GiajjZpRSWR7/42q/EyA/QEiM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 h1:SKvPgvdvmiTWoi0GAJ7AsJfOz3ngVkD/ERbs5pUnHNI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5/go.mod h1:20sz31hv/WsPa3HhU3hfrIet2kxM4Pe0r20eBZ20Tac=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.4 h1:iAckBT2OeEK/kBDyN/jDtpEExhjeeA/Im2q4X0rJZT8=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.4/go.mod h1:vmSqFK+BVIwVpDAGZB3CoCXHzurt4qBE8lf+I/kRTh0=
github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8 h1:SoFYaT9UyGkR0+nogNyD/Lj+bsixB+SNuAS4ABlEs6M=
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8/go.mod h1:2JF49jcDOrLStIXN/j/K1EKRq8a8R2qRnlZA6/o/c7c=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -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/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/docker v27.1.0+incompatible h1:rEHVQc4GZ0MIQKifQPHSFGV/dVgaZafgRf8fCPtDYBs=
github.com/docker/docker v27.1.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
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/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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.191.0/go.mod h1:tD5dsFGxFza0hnQveGfVk9QQYKcfp+VzgRqyXFxE0+E=
google.golang.org/api v0.192.0 h1:PljqpNAfZaaSpS+TnANfnNAXKdzHM/B9bKhwRlo7JP0=
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.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=

View File

@@ -6,24 +6,12 @@ import (
"os"
"path/filepath"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/pkg/attest"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
"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() {
// create a tuf client
home, err := os.UserHomeDir()
@@ -31,10 +19,7 @@ func ExampleVerify_remote() {
panic(err)
}
tufOutputPath := filepath.Join(home, ".docker", "tuf")
tufClient, err := createTufClient(tufOutputPath)
if err != nil {
panic(err)
}
tufClientOpts := tuf.NewDockerDefaultClientOptions(tufOutputPath)
// create a resolver for remote attestations
image := "registry-1.docker.io/library/notary:server"
@@ -42,10 +27,10 @@ func ExampleVerify_remote() {
// configure policy options
opts := &policy.Options{
TUFClient: tufClient,
LocalTargetsDir: filepath.Join(home, ".docker", "policy"), // location to store policy files downloaded from TUF
LocalPolicyDir: "", // overrides TUF policy for local policy files if set
PolicyID: "", // set to ignore policy mapping and select a policy by id
TUFClientOptions: tufClientOpts,
LocalTargetsDir: filepath.Join(home, ".docker", "policy"), // location to store policy files downloaded from TUF
LocalPolicyDir: "", // overrides TUF policy for local policy files if set
PolicyID: "", // set to ignore policy mapping and select a policy by id
}
src, err := oci.ParseImageSpec(image, oci.WithPlatform(platform))

View File

@@ -8,6 +8,7 @@ import (
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
"github.com/docker/attest/pkg/tuf"
intoto "github.com/in-toto/in-toto-golang/in_toto"
v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
"github.com/stretchr/testify/assert"
@@ -20,11 +21,14 @@ var (
PassMirrorPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-mirror")
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
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"
)
func TestSignVerifyOCILayout(t *testing.T) {
ctx, signer := test.Setup(t)
ctx = tuf.WithDownloader(ctx, tuf.NewMockTufClient(EmptyPolicyDir, test.CreateTempDir(t, "", "tuf-dest")))
testCases := []struct {
name string

View File

@@ -3,6 +3,8 @@ package attest
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
@@ -11,6 +13,7 @@ import (
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
"github.com/docker/attest/pkg/tuf"
intoto "github.com/in-toto/in-toto-golang/in_toto"
)
@@ -20,13 +23,20 @@ func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.Options) (resu
if err != nil {
return nil, fmt.Errorf("failed to create image details resolver: %w", err)
}
if opts.AttestationStyle == "" {
opts.AttestationStyle = config.AttestationStyleReferrers
err = populateDefaultOptions(opts)
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 {
return nil, fmt.Errorf("failed to resolve policy: %w", err)
}
@@ -60,6 +70,36 @@ func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.Options) (resu
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) {
dgst, err := oci.SplitDigest(input.Digest)
if err != nil {
@@ -140,14 +180,34 @@ func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, pctx
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 {
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{
Digest: digest,
PURL: purl,
IsCanonical: canonical,
Digest: digest,
PURL: purl,
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)

View File

@@ -8,11 +8,13 @@ import (
"path/filepath"
"testing"
"github.com/distribution/reference"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
"github.com/docker/attest/pkg/tuf"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -68,6 +70,7 @@ func TestVerifyAttestations(t *testing.T) {
func TestVSA(t *testing.T) {
ctx, signer := test.Setup(t)
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
outputLayout := test.CreateTempDir(t, "", TestTempDir)
@@ -98,7 +101,7 @@ func TestVSA(t *testing.T) {
if assert.NotNil(t, results.Input) {
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)
@@ -120,6 +123,7 @@ func TestVSA(t *testing.T) {
func TestVerificationFailure(t *testing.T) {
ctx, signer := test.Setup(t)
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
outputLayout := test.CreateTempDir(t, "", TestTempDir)
@@ -172,21 +176,24 @@ func TestVerificationFailure(t *testing.T) {
func TestSignVerify(t *testing.T) {
ctx, signer := test.Setup(t)
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
outputLayout := test.CreateTempDir(t, "", TestTempDir)
testCases := []struct {
name string
signTL bool
policyDir string
imageName string
expectError bool
name string
signTL bool
policyDir string
imageName string
expectedNonSuccess Outcome
}{
{name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir},
{name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir},
{name: "no tl", signTL: false, policyDir: PassPolicyDir},
{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(test.UnsignedTestImage)
@@ -218,17 +225,84 @@ func TestSignVerify(t *testing.T) {
LocalPolicyDir: tc.policyDir,
}
results, err := Verify(ctx, spec, policyOpts)
if tc.expectError {
require.Error(t, err)
require.NoError(t, err)
if tc.expectedNonSuccess != "" {
assert.Equal(t, tc.expectedNonSuccess, results.Outcome)
return
}
require.NoError(t, err)
assert.Equal(t, OutcomeSuccess, results.Outcome)
platform, err := oci.ParsePlatform(LinuxAMD64)
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)
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

@@ -13,6 +13,7 @@ import (
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
"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/registry"
"github.com/google/go-containerregistry/pkg/v1/remote"
@@ -27,12 +28,14 @@ var (
LocalPolicyAttached = filepath.Join("..", "..", "test", "testdata", "local-policy-attached")
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
EmptyTUFDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-policies")
TestTempDir = "attest-sign-test"
)
func TestAttestationReferenceTypes(t *testing.T) {
ctx, signer := test.Setup(t)
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"}
for _, tc := range []struct {
name string
@@ -182,6 +185,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
func TestReferencesInDifferentRepo(t *testing.T) {
ctx, signer := test.Setup(t)
ctx = tuf.WithDownloader(ctx, tuf.NewMockTufClient(EmptyTUFDir, test.CreateTempDir(t, "", "tuf-dest")))
repoName := "repo"
for _, tc := range []struct {
name string

View File

@@ -6,7 +6,6 @@ import (
"path/filepath"
"strings"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/pkg/mirror"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/tuf"
@@ -30,7 +29,7 @@ func ExampleNewTUFMirror() {
// configure TUF mirror
metadataURI := "https://docker.github.io/tuf-staging/metadata"
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 {
panic(err)
}

View File

@@ -9,7 +9,6 @@ import (
"strings"
"testing"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/tuf"
"github.com/stretchr/testify/assert"
@@ -26,7 +25,7 @@ func TestGetTufMetadataMirror(t *testing.T) {
defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
assert.NoError(t, err)
tufMetadata, err := m.getMetadataMirror(server.URL + metadataPath)
@@ -44,7 +43,7 @@ func TestGetMetadataManifest(t *testing.T) {
defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
assert.NoError(t, err)
img, err := m.GetMetadataManifest(server.URL + metadataPath)
@@ -83,7 +82,7 @@ func TestGetDelegatedMetadataMirrors(t *testing.T) {
defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
assert.NoError(t, err)
delegations, err := m.GetDelegatedMetadataMirrors()

View File

@@ -3,15 +3,14 @@ package mirror
import (
"fmt"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/pkg/tuf"
)
func NewTUFMirror(root []byte, tufPath, metadataURL, targetsURL string, versionChecker tuf.VersionChecker) (*TUFMirror, error) {
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 {
return nil, fmt.Errorf("failed to create TUF client: %w", err)
}

View File

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

View File

@@ -52,12 +52,8 @@ func ImageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descripto
return nil, fmt.Errorf("no image found for platform %v", platform)
}
func RefToPURL(ref string, platform *v1.Platform) (string, bool, error) {
func RefToPURL(named reference.Named, platform *v1.Platform) (string, bool, error) {
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
if canonical, ok := named.(reference.Canonical); ok {

View File

@@ -3,6 +3,7 @@ package oci_test
import (
"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"
@@ -14,42 +15,51 @@ import (
func TestRefToPurl(t *testing.T) {
arm, err := oci.ParsePlatform("arm64/linux")
require.NoError(t, err)
purl, canonical, err := oci.RefToPURL("alpine", arm)
ref, err := reference.ParseNormalizedNamed("alpine")
require.NoError(t, err)
purl, canonical, err := oci.RefToPURL(ref, arm)
assert.NoError(t, err)
assert.Equal(t, "pkg:docker/alpine@latest?platform=arm64%2Flinux", purl)
assert.False(t, canonical)
purl, canonical, err = oci.RefToPURL("alpine:123", arm)
ref, err = reference.ParseNormalizedNamed("alpine:123")
require.NoError(t, err)
purl, canonical, err = oci.RefToPURL(ref, arm)
assert.NoError(t, err)
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
assert.False(t, canonical)
purl, canonical, err = oci.RefToPURL("google/alpine:123", arm)
ref, err = reference.ParseNormalizedNamed("google/alpine:123")
require.NoError(t, err)
purl, canonical, err = oci.RefToPURL(ref, arm)
assert.NoError(t, err)
assert.Equal(t, "pkg:docker/google/alpine@123?platform=arm64%2Flinux", purl)
assert.False(t, canonical)
purl, canonical, err = oci.RefToPURL("library/alpine:123", arm)
ref, err = reference.ParseNormalizedNamed("library/alpine:123")
require.NoError(t, err)
purl, canonical, err = oci.RefToPURL(ref, arm)
assert.NoError(t, err)
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
assert.False(t, canonical)
purl, canonical, err = oci.RefToPURL("docker.io/library/alpine:123", arm)
ref, err = reference.ParseNormalizedNamed("docker.io/library/alpine:123")
require.NoError(t, err)
purl, canonical, err = oci.RefToPURL(ref, arm)
assert.NoError(t, err)
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
assert.False(t, canonical)
purl, canonical, err = oci.RefToPURL("localhost:5001/library/alpine:123", arm)
ref, err = reference.ParseNormalizedNamed("localhost:5001/library/alpine:123")
require.NoError(t, err)
purl, canonical, err = oci.RefToPURL(ref, arm)
assert.NoError(t, err)
assert.Equal(t, "pkg:docker/localhost%3A5001/library/alpine@123?platform=arm64%2Flinux", purl)
assert.False(t, canonical)
purl, canonical, err = oci.RefToPURL("localhost:5001/alpine:123", arm)
ref, err = reference.ParseNormalizedNamed("localhost:5001/alpine:123")
require.NoError(t, err)
purl, canonical, err = oci.RefToPURL(ref, arm)
assert.NoError(t, err)
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine@123?platform=arm64%2Flinux", purl)
assert.False(t, canonical)
purl, canonical, err = oci.RefToPURL("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b", arm)
ref, err = reference.ParseNormalizedNamed("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b")
require.NoError(t, err)
purl, canonical, err = oci.RefToPURL(ref, arm)
assert.NoError(t, err)
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine?digest=sha256%3Ac5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b&platform=arm64%2Flinux", purl)
assert.True(t, canonical)

View File

@@ -12,6 +12,7 @@ import (
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/config"
"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) {
@@ -57,13 +58,13 @@ func resolveLocalPolicy(opts *Options, mapping *config.PolicyMapping, imageName
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))
for _, f := range mapping.Files {
filename := f.Path
file, err := opts.TUFClient.DownloadTarget(filename, filepath.Join(opts.LocalTargetsDir, filename))
file, err := tufClient.DownloadTarget(filename, filepath.Join(opts.LocalTargetsDir, filename))
if err != nil {
return nil, fmt.Errorf("failed to download policy file %s: %w", filename, err)
}
@@ -154,7 +155,7 @@ func findPolicyMatchImpl(imageName string, mappings *config.PolicyMappings, matc
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 != "" {
localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir)
if err != nil {
@@ -168,21 +169,21 @@ func resolvePolicyByID(opts *Options) (*Policy, error) {
}
// must check tuf
tufMappings, err := config.LoadTUFMappings(opts.TUFClient, opts.LocalTargetsDir)
tufMappings, err := config.LoadTUFMappings(tufClient, opts.LocalTargetsDir)
if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err)
}
policy := tufMappings.Policies[opts.PolicyID]
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, nil
}
func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver, opts *Options) (*Policy, error) {
p, err := resolvePolicyByID(opts)
func ResolvePolicy(ctx context.Context, tufClient tuf.Downloader, detailsResolver oci.ImageDetailsResolver, opts *Options) (*Policy, error) {
p, err := resolvePolicyByID(opts, tufClient)
if err != nil {
return nil, fmt.Errorf("failed to resolve policy by id: %w", err)
}
@@ -209,7 +210,7 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver
return resolveLocalPolicy(opts, match.policy, imageName, match.matchedName)
}
// must check tuf
tufMappings, err := config.LoadTUFMappings(opts.TUFClient, opts.LocalTargetsDir)
tufMappings, err := config.LoadTUFMappings(tufClient, opts.LocalTargetsDir)
if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err)
}
@@ -218,7 +219,7 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver
if match.matchType == matchTypeMatchNoPolicy {
for _, mapping := range tufMappings.Policies {
if mapping.ID == match.rule.PolicyID {
return resolveTUFPolicy(opts, mapping, imageName, match.matchedName)
return resolveTUFPolicy(opts, tufClient, mapping, imageName, match.matchedName)
}
}
}
@@ -229,7 +230,7 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver
return nil, err
}
if match.matchType == matchTypePolicy {
return resolveTUFPolicy(opts, match.policy, imageName, match.matchedName)
return resolveTUFPolicy(opts, tufClient, match.policy, imageName, match.matchedName)
}
return nil, nil
}

View File

@@ -51,29 +51,30 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
policyID string
resolveErrorStr string
}{
{repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver},
{repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver, policyID: "docker-official-images"},
{repo: "testdata/mock-tuf-allow", expectSuccess: false, isCanonical: false, resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr},
{repo: "testdata/mock-tuf-deny", expectSuccess: false, isCanonical: false, resolver: defaultResolver},
{repo: "testdata/mock-tuf-verify-sig", expectSuccess: true, isCanonical: false, resolver: defaultResolver},
{repo: "testdata/mock-tuf-wrong-key", expectSuccess: false, isCanonical: false, resolver: defaultResolver},
{repo: "testdata/mock-tuf-allow", expectSuccess: true, resolver: defaultResolver},
{repo: "testdata/mock-tuf-allow", expectSuccess: true, resolver: defaultResolver, policyID: "docker-official-images"},
{repo: "testdata/mock-tuf-allow", resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr},
{repo: "testdata/mock-tuf-deny", resolver: defaultResolver},
{repo: "testdata/mock-tuf-verify-sig", expectSuccess: true, 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: false, isCanonical: false, resolver: defaultResolver},
{repo: "testdata/mock-tuf-no-rego", expectSuccess: false, isCanonical: false, resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"},
{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 {
t.Run(tc.repo, func(t *testing.T) {
input := &policy.Input{
Digest: "sha256:test-digest",
PURL: "test-purl",
IsCanonical: tc.isCanonical,
Digest: "sha256:test-digest",
PURL: "test-purl",
}
if !tc.isCanonical {
input.Tag = "test"
}
tufClient := tuf.NewMockTufClient(tc.repo, test.CreateTempDir(t, "", "tuf-dest"))
if tc.policy == nil {
tc.policy = &policy.Options{
TUFClient: tufClient,
LocalTargetsDir: test.CreateTempDir(t, "", "tuf-targets"),
PolicyID: tc.policyID,
}
@@ -86,7 +87,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
require.NoError(t, err)
resolver, err := policy.CreateImageDetailsResolver(src)
require.NoError(t, err)
policy, err := policy.ResolvePolicy(ctx, resolver, tc.policy)
policy, err := policy.ResolvePolicy(ctx, tufClient, resolver, tc.policy)
if tc.resolveErrorStr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.resolveErrorStr)

View File

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

View File

@@ -27,7 +27,7 @@ type Result struct {
}
type Options struct {
TUFClient tuf.Downloader
TUFClientOptions *tuf.ClientOptions
LocalTargetsDir string
LocalPolicyDir string
PolicyID string
@@ -45,9 +45,13 @@ type Policy struct {
}
type Input struct {
Digest string `json:"digest"`
PURL string `json:"purl"`
IsCanonical bool `json:"isCanonical"`
Digest string `json:"digest"`
PURL string `json:"purl"`
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 {

View File

@@ -4,7 +4,6 @@ import (
"os"
"path/filepath"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/pkg/tuf"
"github.com/theupdateframework/go-tuf/v2/metadata"
)
@@ -21,7 +20,7 @@ func ExampleNewClient_registry() {
metadataURI := "registry-1.docker.io/docker/tuf-metadata:latest"
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 {
panic(err)
}

View File

@@ -9,7 +9,6 @@ import (
"strings"
"testing"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/internal/util"
"github.com/docker/attest/pkg/oci"
"github.com/google/go-containerregistry/pkg/crane"
@@ -56,7 +55,7 @@ func TestRegistryFetcher(t *testing.T) {
delegatedDir := CreateTempDir(t, dir, delegatedRole)
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)
cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataImgTag, targetsRepo)

View File

@@ -1,6 +1,7 @@
package tuf
import (
"context"
"errors"
"fmt"
"io/fs"
@@ -20,6 +21,24 @@ import (
"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
const (
@@ -35,6 +54,11 @@ var (
DockerTUFRootDefault = embed.RootDefault
)
const (
defaultMetadataSource = "docker/tuf-metadata:latest"
defaultTargetsSource = "docker/tuf-targets"
)
type Downloader interface {
DownloadTarget(target, filePath string) (file *TargetFile, err error)
}
@@ -51,19 +75,37 @@ type TargetFile struct {
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.
func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string, versionChecker VersionChecker) (*Client, error) {
func NewClient(opts *ClientOptions) (*Client, error) {
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
} else {
tufSource = OCISource
}
tufRootDigest := util.SHA256Hex(initialRoot)
tufRootDigest := util.SHA256Hex(opts.InitialRoot)
// 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)
if err != nil {
return nil, fmt.Errorf("failed to create directory '%s': %w", metadataPath, err)
@@ -76,29 +118,29 @@ func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string
return nil, fmt.Errorf("failed to read root.json: %w", err)
}
// 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 {
return nil, fmt.Errorf("Failed to write root.json %w", err)
}
rootBytes = initialRoot
rootBytes = opts.InitialRoot
}
// create updater configuration
cfg, err := config.New(metadataSource, rootBytes) // default config
cfg, err := config.New(opts.MetadataSource, rootBytes) // default config
if err != nil {
return nil, fmt.Errorf("failed to create TUF updater configuration: %w", err)
}
cfg.LocalMetadataDir = metadataPath
cfg.LocalTargetsDir = filepath.Join(metadataPath, "download")
cfg.RemoteTargetsURL = targetsSource
cfg.RemoteTargetsURL = opts.TargetsSource
if tufSource == OCISource {
metadataRepo, metadataTag, found := strings.Cut(metadataSource, ":")
metadataRepo, metadataTag, found := strings.Cut(opts.MetadataSource, ":")
if !found {
fmt.Printf("metadata tag not found in URL, using latest\n")
metadataTag = LatestTag
}
cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataTag, targetsSource)
cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataTag, opts.TargetsSource)
}
// create a new Updater instance
@@ -118,7 +160,7 @@ func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string
cfg: cfg,
}
err = versionChecker.CheckVersion(client)
err = opts.VersionChecker.CheckVersion(client)
if err != nil {
return nil, err
}

View File

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

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 {
print("input:",input)
# print("input:",input)
true
}

View File

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