101 Commits

Author SHA1 Message Date
Joel Kamp
a363be7f3a Merge pull request #150 from docker/dependabot/go_modules/google.golang.org/api-0.196.0
feat(deps): bump google.golang.org/api from 0.195.0 to 0.196.0
2024-09-04 16:47:27 -05:00
dependabot[bot]
99846a3483 feat(deps): bump google.golang.org/api from 0.195.0 to 0.196.0
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.195.0 to 0.196.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.195.0...v0.196.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-09-04 21:43:32 +00:00
Joel Kamp
f760b12bb2 Merge pull request #151 from docker/dependabot/go_modules/github.com/sigstore/sigstore/pkg/signature/kms/gcp-1.8.9
feat(deps): bump github.com/sigstore/sigstore/pkg/signature/kms/gcp from 1.8.8 to 1.8.9
2024-09-04 16:41:22 -05:00
Joel Kamp
bab474669f Merge pull request #155 from docker/dependabot/go_modules/github.com/aws/aws-sdk-go-v2/config-1.27.33
feat(deps): bump github.com/aws/aws-sdk-go-v2/config from 1.27.31 to 1.27.33
2024-09-04 16:40:18 -05:00
dependabot[bot]
0705a71115 feat(deps): bump github.com/sigstore/sigstore/pkg/signature/kms/gcp
Bumps [github.com/sigstore/sigstore/pkg/signature/kms/gcp](https://github.com/sigstore/sigstore) from 1.8.8 to 1.8.9.
- [Release notes](https://github.com/sigstore/sigstore/releases)
- [Commits](https://github.com/sigstore/sigstore/compare/v1.8.8...v1.8.9)

---
updated-dependencies:
- dependency-name: github.com/sigstore/sigstore/pkg/signature/kms/gcp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-04 21:37:46 +00:00
dependabot[bot]
b00e02af01 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.31 to 1.27.33.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.27.31...config/v1.27.33)

---
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-09-04 21:36:36 +00:00
Joel Kamp
ff53657cc9 Merge pull request #153 from docker/dependabot/github_actions/actions/create-github-app-token-1.10.4
chore(deps): bump actions/create-github-app-token from 1.10.3 to 1.10.4
2024-09-04 16:34:38 -05:00
Joel Kamp
c8383f3f5a Merge pull request #149 from docker/dependabot/go_modules/github.com/sigstore/sigstore/pkg/signature/kms/aws-1.8.9
feat(deps): bump github.com/sigstore/sigstore/pkg/signature/kms/aws from 1.8.8 to 1.8.9
2024-09-04 16:33:50 -05:00
Joel Kamp
dc247bd348 Merge pull request #143 from docker/dependabot/go_modules/github.com/open-policy-agent/opa-0.68.0
feat(deps): bump github.com/open-policy-agent/opa from 0.67.1 to 0.68.0
2024-09-04 16:33:28 -05:00
Joel Kamp
ce7d173150 Merge pull request #154 from docker/feat--add-slsa-v1-predicate-type
feat: add slsa v1 predicate type
2024-09-04 16:32:21 -05:00
mrjoelkamp
fb69d9a09b feat: add slsa v1 predicate type 2024-09-04 16:15:56 -05:00
James Carnegie
48e58a9115 Verify input image/platform against attestation subjects before passing to rego (#148)
* feat: verify subjects before passing to rego
2024-09-04 10:20:00 +01:00
dependabot[bot]
bfacaf1de0 chore(deps): bump actions/create-github-app-token from 1.10.3 to 1.10.4
Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 1.10.3 to 1.10.4.
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Commits](31c86eb3b3...3378cda945)

---
updated-dependencies:
- dependency-name: actions/create-github-app-token
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-04 08:46:09 +00:00
dependabot[bot]
67ad27ac22 feat(deps): bump github.com/sigstore/sigstore/pkg/signature/kms/aws
Bumps [github.com/sigstore/sigstore/pkg/signature/kms/aws](https://github.com/sigstore/sigstore) from 1.8.8 to 1.8.9.
- [Release notes](https://github.com/sigstore/sigstore/releases)
- [Commits](https://github.com/sigstore/sigstore/compare/v1.8.8...v1.8.9)

---
updated-dependencies:
- dependency-name: github.com/sigstore/sigstore/pkg/signature/kms/aws
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-04 08:41:30 +00:00
James Carnegie
41847ef238 fix: escape ! remove .* (global match) (#146) 2024-09-03 12:24:26 +01:00
James Carnegie
1f806f33a8 feat: validate mapping files on load (#147) 2024-09-03 12:21:24 +01:00
James Carnegie
8982778507 refactor! remove pkg directory (#145)
* refactor!: remove pkg directory

* chore: include breaking changes in draft
2024-09-02 16:17:50 +01:00
James Carnegie
23849c1c2e fix: use canonical names inside TUF fetcher (#144)
* fix: use canonical names inside TUF fetcher
* keep hold of reference to Config
2024-08-30 17:03:29 +01:00
dependabot[bot]
16834292de feat(deps): bump github.com/open-policy-agent/opa from 0.67.1 to 0.68.0
Bumps [github.com/open-policy-agent/opa](https://github.com/open-policy-agent/opa) from 0.67.1 to 0.68.0.
- [Release notes](https://github.com/open-policy-agent/opa/releases)
- [Changelog](https://github.com/open-policy-agent/opa/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-policy-agent/opa/compare/v0.67.1...v0.68.0)

---
updated-dependencies:
- dependency-name: github.com/open-policy-agent/opa
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-30 08:37:19 +00:00
dependabot[bot]
bada1df262 feat(deps): bump google.golang.org/api from 0.194.0 to 0.195.0 (#139)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.194.0 to 0.195.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.194.0...v0.195.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: James Carnegie <kipz@users.noreply.github.com>
2024-08-30 09:29:19 +01:00
James Carnegie
4778d3de6a fix: tuf oci image parsing (#142)
* fix: tuf oci image parsing
2024-08-29 12:27:13 -05:00
James Carnegie
a4ac09e7da refactor! don't use ctx for policy evaluator (#140)
* refactor! don't use ctx for policy evaluator
2024-08-29 17:43:45 +01:00
Joel Kamp
9250552c5b Merge pull request #138 from docker/feat-add-tuf-resolver-tests
feat: add policy resolver tests
2024-08-29 10:28:34 -05:00
mrjoelkamp
2acc30693f fix: remove mock tuf client output 2024-08-29 10:03:07 -05:00
mrjoelkamp
5db1b5c4c1 feat: add tuf resolver test 2024-08-28 17:08:46 -05:00
Jonny Stoten
6f94d59a96 refactor!: add policy.Resolver struct to reduce parameters (#130)
* Add `policy.Resolver` struct to reduce parameters

* Pass image name directly rather than resolver

* Move policy match stuff to its own file
2024-08-28 11:27:00 +01:00
dependabot[bot]
95319494b5 feat(deps): bump github.com/testcontainers/testcontainers-go/modules/registry (#127)
Bumps [github.com/testcontainers/testcontainers-go/modules/registry](https://github.com/testcontainers/testcontainers-go) from 0.32.0 to 0.33.0.
- [Release notes](https://github.com/testcontainers/testcontainers-go/releases)
- [Commits](https://github.com/testcontainers/testcontainers-go/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: github.com/testcontainers/testcontainers-go/modules/registry
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: James Carnegie <kipz@users.noreply.github.com>
2024-08-28 09:58:09 +01:00
dependabot[bot]
64046df6f8 feat(deps): bump github.com/aws/aws-sdk-go-v2/config (#134)
Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.27.28 to 1.27.31.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.27.28...config/v1.27.31)

---
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-28 08:57:31 +00:00
dependabot[bot]
57b6df0ab5 feat(deps): bump google.golang.org/api from 0.192.0 to 0.194.0 (#131)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.192.0 to 0.194.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.192.0...v0.194.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-28 09:56:53 +01:00
dependabot[bot]
857be568b5 feat(deps): bump github.com/Masterminds/semver/v3 from 3.2.1 to 3.3.0 (#136)
Bumps [github.com/Masterminds/semver/v3](https://github.com/Masterminds/semver) from 3.2.1 to 3.3.0.
- [Release notes](https://github.com/Masterminds/semver/releases)
- [Changelog](https://github.com/Masterminds/semver/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Masterminds/semver/compare/v3.2.1...v3.3.0)

---
updated-dependencies:
- dependency-name: github.com/Masterminds/semver/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-28 09:55:28 +01:00
James Carnegie
9d39c5ae3d feat!: remove MockTUFClient (#135)
* feat! remove MockTUFClient

*Breaking*
- use LocalPolicyDir and nil TUFClient instead

Other:
- add stateful Verifier
2024-08-28 09:53:52 +01:00
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
Joel Kamp
d97d20eb93 Merge pull request #121 from docker/dependabot/go_modules/google.golang.org/api-0.191.0
feat(deps): bump google.golang.org/api from 0.190.0 to 0.191.0
2024-08-08 15:28:52 -05:00
Joel Kamp
42390b5fc2 Merge branch 'main' into dependabot/go_modules/google.golang.org/api-0.191.0 2024-08-08 15:18:54 -05:00
Joel Kamp
70e6345942 Merge pull request #119 from docker/dependabot/go_modules/github.com/sigstore/cosign/v2-2.4.0
feat(deps): bump github.com/sigstore/cosign/v2 from 2.3.0 to 2.4.0
2024-08-08 15:18:19 -05:00
dependabot[bot]
f853875eea feat(deps): bump github.com/sigstore/cosign/v2 from 2.3.0 to 2.4.0
Bumps [github.com/sigstore/cosign/v2](https://github.com/sigstore/cosign) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/sigstore/cosign/releases)
- [Changelog](https://github.com/sigstore/cosign/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sigstore/cosign/compare/v2.3.0...v2.4.0)

---
updated-dependencies:
- dependency-name: github.com/sigstore/cosign/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-08 20:11:27 +00:00
Joel Kamp
050497e5a7 Merge pull request #118 from docker/dependabot/go_modules/github.com/sigstore/sigstore/pkg/signature/kms/aws-1.8.8
feat(deps): bump github.com/sigstore/sigstore/pkg/signature/kms/aws from 1.8.7 to 1.8.8
2024-08-08 15:09:20 -05:00
dependabot[bot]
d69334a1e6 feat(deps): bump google.golang.org/api from 0.190.0 to 0.191.0
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.190.0 to 0.191.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.190.0...v0.191.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-08 20:00:21 +00:00
dependabot[bot]
a84268b133 feat(deps): bump github.com/sigstore/sigstore/pkg/signature/kms/aws
Bumps [github.com/sigstore/sigstore/pkg/signature/kms/aws](https://github.com/sigstore/sigstore) from 1.8.7 to 1.8.8.
- [Release notes](https://github.com/sigstore/sigstore/releases)
- [Commits](https://github.com/sigstore/sigstore/compare/v1.8.7...v1.8.8)

---
updated-dependencies:
- dependency-name: github.com/sigstore/sigstore/pkg/signature/kms/aws
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-08 20:00:19 +00:00
Joel Kamp
2cd2e2da96 Merge pull request #117 from docker/dependabot/go_modules/github.com/sigstore/sigstore/pkg/signature/kms/gcp-1.8.8
feat(deps): bump github.com/sigstore/sigstore/pkg/signature/kms/gcp from 1.8.7 to 1.8.8
2024-08-08 14:58:19 -05:00
Joel Kamp
f1ece6893f Merge branch 'main' into dependabot/go_modules/github.com/sigstore/sigstore/pkg/signature/kms/gcp-1.8.8 2024-08-08 14:48:59 -05:00
Joel Kamp
116b9ea770 Merge pull request #120 from docker/refactor-referrers-output
feat!: push attestation artifacts by digest
2024-08-08 14:48:42 -05:00
mrjoelkamp
d291912208 refactor!: move oci output from mirror to oci pkg
BREAKING_CHANGE: output methods to save and push images are now part of the oci pkg
2024-08-08 14:23:46 -05:00
mrjoelkamp
9cad88a687 fix: EmptyConfigImage digest 2024-08-08 13:06:56 -05:00
mrjoelkamp
77ccbc097b feat: use docker/go-containerregistry 2024-08-08 11:37:45 -05:00
mrjoelkamp
45927967c8 test: debug push 2024-08-08 10:04:41 -05:00
mrjoelkamp
9aa56e564d feat: push attestation artifacts by digest 2024-08-07 15:19:48 -05:00
dependabot[bot]
6d0a6de520 feat(deps): bump github.com/sigstore/sigstore/pkg/signature/kms/gcp
Bumps [github.com/sigstore/sigstore/pkg/signature/kms/gcp](https://github.com/sigstore/sigstore) from 1.8.7 to 1.8.8.
- [Release notes](https://github.com/sigstore/sigstore/releases)
- [Commits](https://github.com/sigstore/sigstore/compare/v1.8.7...v1.8.8)

---
updated-dependencies:
- dependency-name: github.com/sigstore/sigstore/pkg/signature/kms/gcp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-07 09:10:08 +00:00
Joel Kamp
8767951fa2 Merge pull request #114 from docker/dependabot/go_modules/google.golang.org/api-0.190.0
feat(deps): bump google.golang.org/api from 0.189.0 to 0.190.0
2024-08-06 10:15:47 -05:00
Joel Kamp
f18b5877d3 Merge branch 'main' into dependabot/go_modules/google.golang.org/api-0.190.0 2024-08-06 10:09:55 -05:00
Joel Kamp
93fd9daeb9 Merge pull request #116 from docker/dependabot/go_modules/github.com/open-policy-agent/opa-0.67.1
feat(deps): bump github.com/open-policy-agent/opa from 0.67.0 to 0.67.1
2024-08-06 10:08:32 -05:00
Joel Kamp
5df79de1c7 Merge branch 'main' into dependabot/go_modules/github.com/open-policy-agent/opa-0.67.1 2024-08-06 09:37:08 -05:00
Joel Kamp
5b5e43b07a Merge pull request #113 from docker/fix-oci-layout-referrers
fix: let OCI layouts use referrers attestations
2024-08-06 09:24:53 -05:00
dependabot[bot]
4c5135eb1b feat(deps): bump github.com/open-policy-agent/opa from 0.67.0 to 0.67.1
Bumps [github.com/open-policy-agent/opa](https://github.com/open-policy-agent/opa) from 0.67.0 to 0.67.1.
- [Release notes](https://github.com/open-policy-agent/opa/releases)
- [Changelog](https://github.com/open-policy-agent/opa/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-policy-agent/opa/compare/v0.67.0...v0.67.1)

---
updated-dependencies:
- dependency-name: github.com/open-policy-agent/opa
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-06 09:01:35 +00:00
mrjoelkamp
0133423f0d feat: add nil pointer test 2024-08-05 16:50:40 -05:00
mrjoelkamp
501b9b442d feat: add CreateAttestationResolver tests 2024-08-05 16:31:39 -05:00
Joel Kamp
d84ed4821c Merge branch 'main' into fix-oci-layout-referrers 2024-08-05 16:01:36 -05:00
Joel Kamp
c9e2ddd448 Merge pull request #115 from docker/chore--disable-codecov-patch-status
chore: disable codecov patch status
2024-08-05 16:01:25 -05:00
mrjoelkamp
165241de42 chore: disable codecov patch status 2024-08-05 15:56:06 -05:00
mrjoelkamp
c7d17faf05 fix: layout attestation resolver 2024-08-05 15:32:24 -05:00
mrjoelkamp
58021646e3 feat: add oci layout test 2024-08-05 11:24:28 -05:00
mrjoelkamp
3e7a85e9b8 fix: nil pointer dereference 2024-08-05 11:24:05 -05:00
mrjoelkamp
bb7a9a257e chore: remove duplicate code 2024-08-05 11:23:32 -05:00
mrjoelkamp
c690d1090c chore: use prefix const 2024-08-05 11:22:49 -05:00
mrjoelkamp
1d1c258f9c fix: referrers resolver only works for registry resolvers 2024-08-05 10:20:40 -05:00
mrjoelkamp
5d096e226f refactor: fix import cycle for mock resolver 2024-08-05 10:19:50 -05:00
dependabot[bot]
7fc7ceaba0 feat(deps): bump google.golang.org/api from 0.189.0 to 0.190.0
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.189.0 to 0.190.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.189.0...v0.190.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-02 08:34:34 +00:00
mrjoelkamp
78ec0b7666 fix: use referrers repo img spec and resolver 2024-08-01 15:24:35 -05:00
Joel Kamp
053f764b8f Merge branch 'main' into fix-oci-layout-referrers 2024-08-01 13:48:53 -05:00
mrjoelkamp
ad3b8b9e49 fix: let OCI layouts use referrers attestations 2024-08-01 13:41:49 -05:00
James Carnegie
9582e69968 fix: standardize casing of initialisms (#112)
* fix: standardize casing of initialisms
* fix: rename intoto -> inToto and Intoto to InToto
* fix: fix all linting errors
2024-08-01 15:35:15 +01:00
James Carnegie
b0b37f73f3 fix: upgrade go to 1.22.x for testing (#110) 2024-08-01 15:34:35 +01:00
dependabot[bot]
d21fc7853c feat(deps): bump github.com/docker/docker (#105)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 27.0.3+incompatible to 27.1.0+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v27.0.3...v27.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-31 16:50:39 +01:00
Jonny Stoten
008c14e3f3 chore: reformat with gofumpt (#109)
* Reformat with gofumpt

* Suppress issue about laxer perms than 0600

---------

Co-authored-by: Joel Kamp <joel.kamp@docker.com>
2024-07-31 15:48:00 +01:00
130 changed files with 4014 additions and 2901 deletions

View File

@@ -14,6 +14,9 @@ categories:
- title: "🧰 Maintenance"
labels:
- "chore"
- title: "💥 Breaking Changes"
labels:
- "breaking"
change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
@@ -21,6 +24,7 @@ version-resolver:
major:
labels:
- "major"
- "breaking"
minor:
labels:
- "minor"
@@ -40,11 +44,13 @@ autolabeler:
branch:
- '/docs{0,1}\/.+/'
- '/tests{0,1}\/.+/'
- '/chore{0,1}\/.+/'
- '/chore\/.+/'
- '/refactor\/.+/'
title:
- "/docs/i"
- "/test/i"
- "/chore/i"
- "/refactor/i"
- label: "bug"
branch:
- '/fix\/.+/'
@@ -60,3 +66,6 @@ autolabeler:
title:
- "/feat/i"
- "/add/i"
- label: "breaking"
title:
- "/^[a-zA-Z]+!:/i"

View File

@@ -10,7 +10,7 @@ jobs:
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@31c86eb3b33c9b601a1f60f98dcbfd1d70f379b4 # v1.10.3
uses: actions/create-github-app-token@3378cda945da322a8db4b193e19d46352ebe2de5 # v1.10.4
with:
app-id: ${{ vars.ATTEST_RELEASE_APP_ID }}
private-key: ${{ secrets.ATTEST_RELEASE_APP_PRIVATE_KEY }}

View File

@@ -12,7 +12,7 @@ jobs:
id-token: write
strategy:
matrix:
go-version: [1.21.x]
go-version: [1.22.x]
# temp disable windows tests see https://github.com/docker/image-signer-verifier/pull/154
# os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-latest, macos-latest]
@@ -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

@@ -66,7 +66,7 @@ See [Policy Mapping](#policy-mapping) for more details.
The `attest.Verify` function returns a `VerificationSummary` object, which contains the results of the policy evaluation.
See [example_verify_test.go](./pkg/attest/example_verify_test.go) for an example of how to verify an image against a policy.
See [example_verify_test.go](./example_verify_test.go) for an example of how to verify an image against a policy.
## Signing Attestations
@@ -76,7 +76,7 @@ This function takes a statement and DSSE signer, and returns a signed DSSE envel
For the common use case of signing a statement and adding it to a manifest, e.g. for pushing to a registry as a referrer to the image being attested, the `attestation.AttestationManifest` type can be used.
See [example_attestation_manifest_test.go](./pkg/attestation/example_attestation_manifest_test.go)
See also [example_sign_test.go](./pkg/attest/example_sign_test.go) for an example of how to sign all attached in-toto statements on an image, e.g. those produced by buildkit.
See also [example_sign_test.go](./example_sign_test.go) for an example of how to sign all attached in-toto statements on an image, e.g. those produced by buildkit.
# Rego Policy
@@ -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
@@ -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",
"resourceUri": "pkg:docker/example.org/example-image@1.0?platform=linux%2Famd64&digest=sha256%3A49f717386e5462e945232569a97a05831cb83bef8c3369be3bb7ea1793686960",
"policy": {
"uri": "https://example.org/internal-policy/v1"
"uri": "https://example.org/internal-policy/v1",
"downloadLocation": "https://docker.github.io/tuf-staging/targets/docker/d71d6b8f49fcba1295b16f5394dd5863a14e4277eb663d66d8c48e392509afe0.policy.rego",
"digest": {
"sha256": "d71d6b8f49fcba1295b16f5394dd5863a14e4277eb663d66d8c48e392509afe0"
}
},
"verificationResult": "PASSED",
"verifiedLevels": ["SLSA_BUILD_LEVEL_3"]

4
attestation/README.md Normal file
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

@@ -2,12 +2,16 @@ package attestation
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"maps"
"strings"
"github.com/docker/attest/oci"
v1 "github.com/google/go-containerregistry/pkg/v1"
"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/mutate"
"github.com/google/go-containerregistry/pkg/v1/partial"
@@ -17,19 +21,32 @@ import (
"github.com/secure-systems-lab/go-securesystemslib/dsse"
)
// GetAttestationManifestsFromIndex extracts all attestation manifests from an index
func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*AttestationManifest, error) {
// NewManifest creates a new attestation manifest from a descriptor.
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()
if err != nil {
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
}
subjects := make(map[string]*v1.Descriptor)
for _, subject := range idx.Manifests {
subjects[subject.Digest.String()] = &subject
for i := range idx.Manifests {
subject := &idx.Manifests[i]
subjects[subject.Digest.String()] = subject
}
var attestationManifests []*AttestationManifest
for _, desc := range idx.Manifests {
var attestationManifests []*Manifest
for i := range idx.Manifests {
desc := idx.Manifests[i]
if desc.Annotations[DockerReferenceType] == AttestationManifestType {
subject := subjects[desc.Annotations[DockerReferenceDigest]]
if subject == nil {
@@ -39,27 +56,28 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*AttestationManife
if err != nil {
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 {
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
}
attestationManifests = append(attestationManifests,
&AttestationManifest{
&Manifest{
OriginalDescriptor: &desc,
SubjectDescriptor: subject,
OriginalLayers: attestationLayers})
OriginalLayers: attestationLayers,
})
}
}
return attestationManifests, nil
}
// GetAttestationsFromImage extracts all attestation layers from an image
func GetAttestationsFromImage(image v1.Image) ([]*AttestationLayer, error) {
// LayersFromImage extracts all attestation layers from an image.
func layersFromImage(image v1.Image) ([]*Layer, error) {
layers, err := image.Layers()
if err != nil {
return nil, fmt.Errorf("failed to extract layers from image: %w", err)
}
var attestationLayers []*AttestationLayer
var attestationLayers []*Layer
for _, layer := range layers {
// parse layer blob as json
r, err := layer.Uncompressed()
@@ -78,19 +96,19 @@ func GetAttestationsFromImage(image v1.Image) ([]*AttestationLayer, error) {
// copy original annotations
ann := maps.Clone(layerDesc.Annotations)
// only decode intoto statements
var stmt = new(intoto.Statement)
stmt := new(intoto.Statement)
if mt == types.MediaType(intoto.PayloadType) {
err = json.NewDecoder(r).Decode(&stmt)
if err != nil {
return nil, fmt.Errorf("failed to decode statement layer contents: %w", err)
}
}
attestationLayers = append(attestationLayers, &AttestationLayer{Layer: layer, Statement: stmt, Annotations: ann})
attestationLayers = append(attestationLayers, &Layer{Layer: layer, Statement: stmt, Annotations: ann})
}
return attestationLayers, nil
}
func (manifest *AttestationManifest) 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)
if err != nil {
return fmt.Errorf("failed to create signed layer: %w", err)
@@ -99,9 +117,9 @@ func (manifest *AttestationManifest) AddAttestation(ctx context.Context, signer
return nil
}
func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*AttestationLayer, error) {
func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*Layer, error) {
// sign the statement
env, err := SignInTotoStatement(ctx, statement, signer, opts)
env, err := signInTotoStatement(ctx, statement, signer, opts)
if err != nil {
return nil, fmt.Errorf("failed to sign statement: %w", err)
}
@@ -114,7 +132,7 @@ func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, si
if err != nil {
return nil, fmt.Errorf("failed to marshal envelope: %w", err)
}
return &AttestationLayer{
return &Layer{
Statement: statement,
Annotations: map[string]string{
InTotoPredicateType: statement.PredicateType,
@@ -124,7 +142,7 @@ func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, si
}, 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)
if err != nil {
return nil, fmt.Errorf("failed to marshal statement: %w", err)
@@ -136,12 +154,12 @@ func SignInTotoStatement(ctx context.Context, statement *intoto.Statement, signe
return env, nil
}
func UpdateIndexImage(
func updateImageIndex(
idx v1.ImageIndex,
manifest *AttestationManifest,
options ...func(*AttestationManifestImageOptions) error) (v1.ImageIndex, error) {
image, err := manifest.BuildAttestationImage(options...)
manifest *Manifest,
options ...func(*ManifestImageOptions) error,
) (v1.ImageIndex, error) {
image, err := manifest.BuildImage(options...)
if err != nil {
return nil, fmt.Errorf("failed to build image: %w", err)
}
@@ -163,10 +181,10 @@ func UpdateIndexImage(
return idx, nil
}
func UpdateIndexImages(idx v1.ImageIndex, manifest []*AttestationManifest, options ...func(*AttestationManifestImageOptions) error) (v1.ImageIndex, error) {
func UpdateIndexImages(idx v1.ImageIndex, manifest []*Manifest, options ...func(*ManifestImageOptions) error) (v1.ImageIndex, error) {
var err error
for _, m := range manifest {
idx, err = UpdateIndexImage(idx, m, options...)
idx, err = updateImageIndex(idx, m, options...)
if err != nil {
return nil, fmt.Errorf("failed to add image to index: %w", err)
}
@@ -174,8 +192,8 @@ func UpdateIndexImages(idx v1.ImageIndex, manifest []*AttestationManifest, optio
return idx, nil
}
func newOptions(options ...func(*AttestationManifestImageOptions) error) (*AttestationManifestImageOptions, error) {
opts := &AttestationManifestImageOptions{}
func newOptions(options ...func(*ManifestImageOptions) error) (*ManifestImageOptions, error) {
opts := &ManifestImageOptions{}
for _, opt := range options {
err := opt(opts)
if err != nil {
@@ -185,22 +203,22 @@ func newOptions(options ...func(*AttestationManifestImageOptions) error) (*Attes
return opts, nil
}
func WithoutSubject(skipSubject bool) func(*AttestationManifestImageOptions) error {
return func(r *AttestationManifestImageOptions) error {
func WithoutSubject(skipSubject bool) func(*ManifestImageOptions) error {
return func(r *ManifestImageOptions) error {
r.skipSubject = skipSubject
return nil
}
}
func WithReplacedLayers(replaceLayers bool) func(*AttestationManifestImageOptions) error {
return func(r *AttestationManifestImageOptions) error {
func WithReplacedLayers(replaceLayers bool) func(*ManifestImageOptions) error {
return func(r *ManifestImageOptions) error {
r.replaceLayers = replaceLayers
return nil
}
}
// build an image with signed attestations, optionally replacing existing layers with signed layers
func (manifest *AttestationManifest) BuildAttestationImage(options ...func(*AttestationManifestImageOptions) error) (v1.Image, error) {
// build an image with signed attestations, optionally replacing existing layers with signed layers.
func (manifest *Manifest) BuildImage(options ...func(*ManifestImageOptions) error) (v1.Image, error) {
opts, err := newOptions(options...)
if err != nil {
return nil, fmt.Errorf("failed to create options: %w", err)
@@ -218,26 +236,26 @@ func (manifest *AttestationManifest) BuildAttestationImage(options ...func(*Atte
break
}
}
//add existing layers if they've not been signed or we're not replacing them
// add existing layers if they've not been signed or we're not replacing them
if !found || !opts.replaceLayers {
resultLayers = append(resultLayers, existingLayer)
}
}
// so taht 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
newImg, err := buildImage(resultLayers, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts)
newImg, err := buildImageFromLayers(resultLayers, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts)
if err != nil {
return nil, fmt.Errorf("failed to build image: %w", err)
}
return newImg, nil
}
// build an image per attestation (layer) suitable for use as Referrers
func (manifest *AttestationManifest) BuildReferringArtifacts() ([]v1.Image, error) {
// build an image per attestation (layer) suitable for use as Referrers.
func (manifest *Manifest) BuildReferringArtifacts() ([]v1.Image, error) {
var images []v1.Image
for _, layer := range manifest.SignedLayers {
opts := &AttestationManifestImageOptions{}
newImg, err := buildImage([]*AttestationLayer{layer}, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts)
opts := &ManifestImageOptions{}
newImg, err := buildImageFromLayers([]*Layer{layer}, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts)
if err != nil {
return nil, fmt.Errorf("failed to build image: %w", err)
}
@@ -246,15 +264,15 @@ func (manifest *AttestationManifest) BuildReferringArtifacts() ([]v1.Image, erro
return images, nil
}
// build and image containing only layers
func buildImage(layers []*AttestationLayer, manifest *v1.Descriptor, subject *v1.Descriptor, opts *AttestationManifestImageOptions) (v1.Image, error) {
// build an image containing only layers provided.
func buildImageFromLayers(layers []*Layer, manifest *v1.Descriptor, subject *v1.Descriptor, opts *ManifestImageOptions) (v1.Image, error) {
newImg := empty.Image
var err error
if len(layers) == 0 {
return nil, fmt.Errorf("no layers supplied to build image")
}
// NB: if we add the subject before the layers, it does not end up being computed/serialised in the output for some reason
//TODO - recreate this bug and push upstream
// NB: if we add the subject before the layers, it does not end up being computed/serialized in the output for some reason
// TODO - recreate this bug and push upstream
for _, layer := range layers {
add := mutate.Addendum{
Layer: layer.Layer,
@@ -284,41 +302,143 @@ func buildImage(layers []*AttestationLayer, manifest *v1.Descriptor, subject *v1
// see note above - must be added after the layers!
if !opts.skipSubject {
subject.Platform = nil
newImg = mutate.Subject(newImg, *subject).(v1.Image)
ok := false
newImg, ok = mutate.Subject(newImg, *subject).(v1.Image)
if !ok {
return nil, fmt.Errorf("failed to set subject: %w", err)
}
}
if !opts.laxReferrers {
// 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
}
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()
func ExtractEnvelopes(manifest *Manifest, predicateType string) ([]*Envelope, error) {
var envs []*Envelope
dsseMediaType, err := DSSEMediaType(predicateType)
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{
MediaType: "application/vnd.oci.empty.v1+json",
Size: 2,
Digest: v1.Hash{Algorithm: "sha256", Hex: "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"},
Data: []byte("{}"),
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(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) {
mf, err := i.Manifest()
func ExtractStatementsFromIndex(idx v1.ImageIndex, mediaType string) ([]*AnnotatedStatement, error) {
mfs2, err := idx.IndexManifest()
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 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)
}

View File

@@ -1,23 +1,18 @@
package test
package attestation_test
import (
"path/filepath"
"testing"
"github.com/docker/attest/attestation"
"github.com/docker/attest/internal/test"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"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) {
statements, err := ExtractAnnotatedStatements(UnsignedTestImage, intoto.PayloadType)
statements, err := attestation.ExtractAnnotatedStatements(test.UnsignedTestImage(".."), intoto.PayloadType)
assert.NoError(t, err)
assert.Equalf(t, len(statements), ExpectedStatements, "expected %d statement, got %d", ExpectedStatements, len(statements))
}

View File

@@ -4,17 +4,15 @@ import (
"context"
"time"
"github.com/docker/attest/pkg/attest"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/mirror"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/signerverifier"
"github.com/docker/attest/attestation"
"github.com/docker/attest/oci"
"github.com/docker/attest/signerverifier"
v1 "github.com/google/go-containerregistry/pkg/v1"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
)
func ExampleAttestationManifest() {
func ExampleManifest() {
// configure signerverifier
// local signer (unsafe for production)
signer, err := signerverifier.GenKeyPair()
@@ -55,7 +53,7 @@ func ExampleAttestationManifest() {
ID: "test-verifier",
},
TimeVerified: time.Now().UTC().Format(time.RFC3339),
ResourceUri: "some-uri",
ResourceURI: "some-uri",
Policy: attestation.VSAPolicy{URI: "some-uri"},
VerificationResult: "PASSED",
VerifiedLevels: []string{"SLSA_BUILD_LEVEL_1"},
@@ -63,13 +61,13 @@ func ExampleAttestationManifest() {
}
// create a new manifest to hold the attestation
manifest, err := attest.NewAttestationManifest(desc)
manifest, err := attestation.NewManifest(desc)
if err != nil {
panic(err)
}
// 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 {
panic(err)
}
@@ -80,7 +78,11 @@ func ExampleAttestationManifest() {
}
// save the manifest to the registry as a referrers artifact
err = mirror.SaveReferrers(manifest, output)
artifacts, err := manifest.BuildReferringArtifacts()
if err != nil {
panic(err)
}
err = oci.SaveImagesNoTag(artifacts, output)
if err != nil {
panic(err)
}

View File

@@ -1,52 +1,51 @@
package oci
package attestation
import (
"context"
"encoding/json"
"fmt"
"github.com/docker/attest/pkg/attestation"
att "github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/oci"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/layout"
)
// implementation of AttestationResolver that closes over attestations from an oci layout
type OCILayoutResolver struct {
*attestation.AttestationManifest
*ImageSpec
// implementation of Resolver that closes over attestations from an oci layout.
type LayoutResolver struct {
*Manifest
*oci.ImageSpec
}
func NewOCILayoutAttestationResolver(src *ImageSpec) (*OCILayoutResolver, error) {
r := &OCILayoutResolver{
func NewOCILayoutResolver(src *oci.ImageSpec) (*LayoutResolver, error) {
r := &LayoutResolver{
ImageSpec: src,
}
_, err := r.fetchAttestationManifest()
_, err := r.fetchManifest()
if err != nil {
return nil, err
}
return r, nil
}
func (r *OCILayoutResolver) fetchAttestationManifest() (*attestation.AttestationManifest, error) {
if r.AttestationManifest == nil {
m, err := attestationManifestFromOCILayout(r.Identifier, r.ImageSpec.Platform)
func (r *LayoutResolver) fetchManifest() (*Manifest, error) {
if r.Manifest == nil {
m, err := manifestFromOCILayout(r.Identifier, r.ImageSpec.Platform)
if err != nil {
return nil, err
}
r.AttestationManifest = m
r.Manifest = m
}
return r.AttestationManifest, nil
return r.Manifest, nil
}
func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
var envs []*att.Envelope
dsseMediaType, err := attestation.DSSEMediaType(predicateType)
func (r *LayoutResolver) Attestations(_ context.Context, predicateType string) ([]*Envelope, error) {
var envs []*Envelope
dsseMediaType, err := DSSEMediaType(predicateType)
if err != nil {
return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err)
}
for _, attestationLayer := range r.AttestationManifest.OriginalLayers {
for _, attestationLayer := range r.Manifest.OriginalLayers {
mt, err := attestationLayer.Layer.MediaType()
if err != nil {
return nil, fmt.Errorf("failed to get layer media type: %w", err)
@@ -55,10 +54,9 @@ func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType stri
if mts != dsseMediaType {
continue
}
var env = new(att.Envelope)
env := new(Envelope)
// parse layer blob as json
r, err := attestationLayer.Layer.Uncompressed()
if err != nil {
return nil, fmt.Errorf("failed to get layer contents: %w", err)
}
@@ -72,19 +70,19 @@ func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType stri
return envs, nil
}
func (r *OCILayoutResolver) ImageName(ctx context.Context) (string, error) {
func (r *LayoutResolver) ImageName(_ context.Context) (string, error) {
return r.SubjectName, nil
}
func (r *OCILayoutResolver) ImageDescriptor(ctx context.Context) (*v1.Descriptor, error) {
func (r *LayoutResolver) ImageDescriptor(_ context.Context) (*v1.Descriptor, error) {
return r.SubjectDescriptor, nil
}
func (r *OCILayoutResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
func (r *LayoutResolver) ImagePlatform(_ context.Context) (*v1.Platform, error) {
return r.ImageSpec.Platform, nil
}
func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*attestation.AttestationManifest, error) {
func manifestFromOCILayout(path string, platform *v1.Platform) (*Manifest, error) {
idx, err := layout.ImageIndexFromPath(path)
if err != nil {
return nil, err
@@ -96,7 +94,6 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*atte
}
idxDescriptor := idxm.Manifests[0]
name := idxDescriptor.Annotations["org.opencontainers.image.ref.name"]
idxDigest := idxDescriptor.Digest
mfs, err := idx.ImageIndex(idxDigest)
@@ -108,18 +105,25 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*atte
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
}
var subjectDescriptor *v1.Descriptor
for _, mf := range mfs2.Manifests {
if mf.Platform.Equals(*platform) {
subjectDescriptor = &mf
break
for i := range mfs2.Manifests {
manifest := &mfs2.Manifests[i]
if manifest.Platform != nil {
if manifest.Platform.Equals(*platform) {
subjectDescriptor = manifest
break
}
}
}
for _, mf := range mfs2.Manifests {
if mf.Annotations[att.DockerReferenceType] != attestation.AttestationManifestType {
if subjectDescriptor == nil {
return nil, fmt.Errorf("platform not found in index")
}
for i := range mfs2.Manifests {
mf := &mfs2.Manifests[i]
if mf.Annotations[DockerReferenceType] != AttestationManifestType {
continue
}
if mf.Annotations[att.DockerReferenceDigest] != subjectDescriptor.Digest.String() {
if mf.Annotations[DockerReferenceDigest] != subjectDescriptor.Digest.String() {
continue
}
@@ -127,14 +131,14 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*atte
if err != nil {
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 {
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
}
attest := &attestation.AttestationManifest{
attest := &Manifest{
OriginalLayers: layers,
OriginalDescriptor: &mf,
SubjectName: name,
OriginalDescriptor: mf,
SubjectName: idxDescriptor.Annotations["org.opencontainers.image.ref.name"],
SubjectDescriptor: subjectDescriptor,
}
return attest, nil

View File

@@ -0,0 +1,68 @@
package attestation_test
import (
"strings"
"testing"
"github.com/docker/attest"
"github.com/docker/attest/attestation"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/oci"
"github.com/docker/attest/policy"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAttestationFromOCILayout(t *testing.T) {
ctx, signer := test.Setup(t)
outputLayout := test.CreateTempDir(t, "", "attest-oci-layout")
invalidPlatform := &v1.Platform{
Architecture: "invalid",
OS: "invalid",
}
opts := &attestation.SigningOptions{}
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage(".."))
require.NoError(t, err)
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
signedIndex := attIdx.Index
signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests)
require.NoError(t, err)
spec, err := oci.ParseImageSpec(oci.LocalPrefix + outputLayout)
require.NoError(t, err)
err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, outputLayout)
require.NoError(t, err)
testCases := []struct {
name string
platform *v1.Platform
errorStr string
}{
{name: "nominal", platform: spec.Platform},
{name: "invalid platform", platform: invalidPlatform, errorStr: "platform not found in index"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
spec := &oci.ImageSpec{
Type: oci.OCI,
Identifier: outputLayout,
Platform: tc.platform,
}
resolver, err := policy.CreateImageDetailsResolver(spec)
if tc.errorStr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errorStr)
return
}
require.NoError(t, err)
desc, err := resolver.ImageDescriptor(ctx)
require.NoError(t, err)
digest := desc.Digest.String()
assert.True(t, strings.Contains(digest, "sha256:"))
})
}
}

69
attestation/mock.go Normal file
View File

@@ -0,0 +1,69 @@
package attestation
import (
"context"
"github.com/docker/attest/oci"
v1 "github.com/google/go-containerregistry/pkg/v1"
)
// ensure MockResolver implements Resolver.
var _ oci.ImageDetailsResolver = MockResolver{}
type MockResolver struct {
Envs []*Envelope
Image string
PlatformFn func() (*v1.Platform, error)
DescriptorFn func() (*v1.Descriptor, error)
ImangeNameFn func() (string, error)
}
func (r MockResolver) Attestations(_ context.Context, _ string) ([]*Envelope, error) {
return r.Envs, nil
}
func (r MockResolver) ImageName(_ context.Context) (string, error) {
if r.Image != "" {
return r.Image, nil
}
if r.ImangeNameFn != nil {
return r.ImangeNameFn()
}
return "library/alpine:latest", nil
}
func (r MockResolver) ImageDescriptor(_ context.Context) (*v1.Descriptor, error) {
if r.DescriptorFn != nil {
return r.DescriptorFn()
}
digest, err := v1.NewHash("sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620")
if err != nil {
return nil, err
}
return &v1.Descriptor{
Digest: digest,
Size: 1234,
MediaType: "application/vnd.oci.image.manifest.v1+json",
}, nil
}
func (r MockResolver) ImagePlatform(_ context.Context) (*v1.Platform, error) {
if r.PlatformFn != nil {
return r.PlatformFn()
}
return oci.ParsePlatform("linux/amd64")
}
type MockRegistryResolver struct {
Subject *v1.Descriptor
ImageNameStr string
*MockResolver
}
func (r *MockRegistryResolver) ImageDescriptor(_ context.Context) (*v1.Descriptor, error) {
return r.Subject, nil
}
func (r *MockRegistryResolver) ImageName(_ context.Context) (string, error) {
return r.ImageNameStr, nil
}

View File

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

View File

@@ -7,13 +7,12 @@ import (
"path/filepath"
"testing"
"github.com/docker/attest"
"github.com/docker/attest/attestation"
"github.com/docker/attest/config"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/attest"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/mirror"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
"github.com/docker/attest/oci"
"github.com/docker/attest/policy"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/registry"
"github.com/google/go-containerregistry/pkg/v1/remote"
@@ -22,19 +21,17 @@ import (
)
var (
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
LocalPolicy = filepath.Join("..", "..", "test", "testdata", "local-policy")
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")
NoProvenanceImage = filepath.Join("..", "test", "testdata", "no-provenance-image")
PassPolicyDir = filepath.Join("..", "test", "testdata", "local-policy-pass")
LocalPolicy = filepath.Join("..", "test", "testdata", "local-policy")
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")
TestTempDir = "attest-sign-test"
)
func TestAttestationReferenceTypes(t *testing.T) {
ctx, signer := test.Setup(t)
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
platforms := []string{"linux/amd64", "linux/arm64"}
for _, tc := range []struct {
name string
@@ -57,7 +54,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
{
name: "attached attestations, referrers repo (mismatched args)",
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
expectFailure: true, //mismatched args
expectFailure: true, // mismatched args
attestationSource: config.AttestationStyleAttached,
referrersRepo: "referrers",
},
@@ -93,7 +90,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
opts := &attestation.SigningOptions{
SkipTL: true,
}
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage(".."))
require.NoError(t, err)
indexName := fmt.Sprintf("%s/repo:root", u.Host)
@@ -112,14 +109,16 @@ func TestAttestationReferenceTypes(t *testing.T) {
// push subject image so that it can be resolved
require.NoError(t, err)
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
err = oci.PushIndexToRegistry(attIdx.Index, indexName)
require.NoError(t, err)
// upload referrers
output, err := oci.ParseImageSpec(outputRepo)
require.NoError(t, err)
for _, attIdx := range signedManifests {
err = mirror.SaveReferrers(attIdx, []*oci.ImageSpec{output})
images, err := attIdx.BuildReferringArtifacts()
require.NoError(t, err)
err = oci.SaveImagesNoTag(images, []*oci.ImageSpec{output})
require.NoError(t, err)
}
@@ -137,8 +136,9 @@ func TestAttestationReferenceTypes(t *testing.T) {
ref = fmt.Sprintf("%s/repo@%s", u.Host, idxDigest.String())
}
policyOpts := &policy.PolicyOptions{
policyOpts := &policy.Options{
LocalPolicyDir: LocalPolicy,
DisableTUF: true,
}
if tc.referrersRepo != "" {
@@ -201,22 +201,22 @@ func TestReferencesInDifferentRepo(t *testing.T) {
} {
server := tc.server
defer server.Close()
serverUrl, err := url.Parse(server.URL)
serverURL, err := url.Parse(server.URL)
require.NoError(t, err)
refServer := tc.refServer
defer refServer.Close()
refServerUrl, err := url.Parse(refServer.URL)
refServerURL, err := url.Parse(refServer.URL)
require.NoError(t, err)
opts := &attestation.SigningOptions{
SkipTL: true,
}
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage(".."))
require.NoError(t, err)
indexName := fmt.Sprintf("%s/%s:latest", serverUrl.Host, repoName)
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName)
err = oci.PushIndexToRegistry(attIdx.Index, indexName)
require.NoError(t, err)
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
@@ -225,24 +225,24 @@ func TestReferencesInDifferentRepo(t *testing.T) {
// push signed attestation image to the ref server
for _, signedManifest := range signedManifests {
// push references using subject-digest.att convention
image, err := signedManifest.BuildAttestationImage()
image, err := signedManifest.BuildImage()
require.NoError(t, err)
err = mirror.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)
refServer := tc.refServer
defer refServer.Close()
refServerUrl, err := url.Parse(refServer.URL)
refServerURL, err := url.Parse(refServer.URL)
require.NoError(t, err)
opts := &attestation.SigningOptions{
SkipTL: true,
}
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage(".."))
require.NoError(t, err)
indexName := fmt.Sprintf("%s/%s:latest", serverUrl.Host, repoName)
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName)
err = oci.PushIndexToRegistry(attIdx.Index, indexName)
require.NoError(t, err)
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
@@ -254,21 +254,22 @@ func TestReferencesInDifferentRepo(t *testing.T) {
imgs, err := mf.BuildReferringArtifacts()
require.NoError(t, err)
for _, img := range imgs {
err = mirror.PushImageToRegistry(img, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerUrl.Host, repoName))
err = oci.PushImageToRegistry(img, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerURL.Host, repoName))
require.NoError(t, err)
}
}
mfs2, err := attIdx.Index.IndexManifest()
require.NoError(t, err)
for _, mf := range mfs2.Manifests {
//skip signed/unsigned attestations
// skip signed/unsigned attestations
if mf.Annotations[attestation.DockerReferenceType] == attestation.AttestationManifestType {
continue
}
// can evaluate policy using referrers in a different repo
referencedImage := fmt.Sprintf("%s@%s", indexName, mf.Digest.String())
policyOpts := &policy.PolicyOptions{
policyOpts := &policy.Options{
LocalPolicyDir: PassPolicyDir,
DisableTUF: true,
}
src, err := oci.ParseImageSpec(referencedImage)
require.NoError(t, err)
@@ -285,7 +286,7 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) {
server := httptest.NewServer(registry.New())
defer server.Close()
serverUrl, err := url.Parse(server.URL)
serverURL, err := url.Parse(server.URL)
require.NoError(t, err)
repoName := "repo"
@@ -293,11 +294,11 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) {
opts := &attestation.SigningOptions{
SkipTL: true,
}
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage(".."))
require.NoError(t, err)
indexName := fmt.Sprintf("%s/%s:latest", serverUrl.Host, repoName)
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName)
err = oci.PushIndexToRegistry(attIdx.Index, indexName)
require.NoError(t, err)
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
@@ -308,12 +309,12 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) {
imgs, err := mf.BuildReferringArtifacts()
require.NoError(t, err)
for _, img := range imgs {
err = mirror.PushImageToRegistry(img, fmt.Sprintf("%s/%s:tag-does-not-matter", serverUrl.Host, repoName))
err = oci.PushImageToRegistry(img, fmt.Sprintf("%s/%s:tag-does-not-matter", serverURL.Host, repoName))
require.NoError(t, err)
mf, err := img.Manifest()
require.NoError(t, err)
subject := mf.Subject
subjectRef, err := name.ParseReference(fmt.Sprintf("%s/%s:sha256-%s", serverUrl.Host, repoName, subject.Digest.Hex))
subjectRef, err := name.ParseReference(fmt.Sprintf("%s/%s:sha256-%s", serverURL.Host, repoName, subject.Digest.Hex))
require.NoError(t, err)
idx, err := remote.Index(subjectRef)
require.NoError(t, err)

104
attestation/registry.go Normal file
View File

@@ -0,0 +1,104 @@
package attestation
import (
"context"
"fmt"
"github.com/docker/attest/oci"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
)
// ensure RegistryResolver implements Resolver.
var _ Resolver = &RegistryResolver{}
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 (
"fmt"
@@ -7,12 +7,11 @@ import (
"strings"
"testing"
"github.com/docker/attest"
"github.com/docker/attest/attestation"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/attest"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/mirror"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
"github.com/docker/attest/oci"
"github.com/docker/attest/policy"
"github.com/google/go-containerregistry/pkg/registry"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -26,7 +25,7 @@ func TestRegistry(t *testing.T) {
require.NoError(t, err)
opts := &attestation.SigningOptions{}
attIdx, err := oci.IndexFromPath(oci.UnsignedTestImage)
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage(".."))
require.NoError(t, err)
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
@@ -36,7 +35,7 @@ func TestRegistry(t *testing.T) {
indexName := fmt.Sprintf("%s/repo:root", u.Host)
require.NoError(t, err)
err = mirror.PushIndexToRegistry(signedIndex, indexName)
err = oci.PushIndexToRegistry(signedIndex, indexName)
require.NoError(t, err)
spec, err := oci.ParseImageSpec(indexName)

12
attestation/resolver.go Normal file
View File

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

View File

@@ -5,12 +5,12 @@ import (
"fmt"
"github.com/docker/attest/internal/util"
"github.com/docker/attest/pkg/tlog"
"github.com/docker/attest/tlog"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/secure-systems-lab/go-securesystemslib/dsse"
)
// SignDSSE signs a payload with a given signer and uploads the signature to the transparency log
// SignDSSE signs a payload with a given signer and uploads the signature to the transparency log.
func SignDSSE(ctx context.Context, payload []byte, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) {
payloadType := intoto.PayloadType
env := new(Envelope)
@@ -28,13 +28,13 @@ func SignDSSE(ctx context.Context, payload []byte, signer dsse.SignerVerifier, o
}
// get Key ID from signer
keyId, err := signer.KeyID()
keyID, err := signer.KeyID()
if err != nil {
return nil, fmt.Errorf("error getting public key ID: %w", err)
}
dsseSig := Signature{
KeyID: keyId,
dsseSig := &Signature{
KeyID: keyID,
Sig: base64Encoding.EncodeToString(sig),
}
if !opts.SkipTL {
@@ -42,22 +42,22 @@ func SignDSSE(ctx context.Context, payload []byte, signer dsse.SignerVerifier, o
if err != nil {
return nil, fmt.Errorf("failed to log to rekor: %w", err)
}
dsseSig.Extension = *ext
dsseSig.Extension = ext
}
// add signature to dsse envelope
env.Signatures = []Signature{dsseSig}
env.Signatures = []*Signature{dsseSig}
return env, nil
}
// returns a new envelope with the transparency log entry added to the signature extension
// returns a new envelope with the transparency log entry added to the signature extension.
func logSignature(ctx context.Context, t tlog.TL, sig *[]byte, encPayload *[]byte, signer dsse.SignerVerifier) (*Extension, error) {
// get Key ID from signer
keyId, err := signer.KeyID()
keyID, err := signer.KeyID()
if err != nil {
return nil, fmt.Errorf("error getting public key ID: %w", err)
}
entry, err := t.UploadLogEntry(ctx, keyId, *encPayload, *sig, signer)
entry, err := t.UploadLogEntry(ctx, keyID, *encPayload, *sig, signer)
if err != nil {
return nil, fmt.Errorf("error uploading TL entry: %w", err)
}
@@ -66,10 +66,10 @@ func logSignature(ctx context.Context, t tlog.TL, sig *[]byte, encPayload *[]byt
return nil, fmt.Errorf("error unmarshaling tl entry: %w", err)
}
return &Extension{
Kind: DockerDsseExtKind,
Ext: DockerDsseExtension{
Tl: DockerTlExtension{
Kind: RekorTlExtKind,
Kind: DockerDSSEExtKind,
Ext: &DockerDSSEExtension{
TL: &DockerTLExtension{
Kind: RekorTLExtKind,
Data: entryObj, // transparency log entry metadata
},
},

301
attestation/sign_test.go Normal file
View File

@@ -0,0 +1,301 @@
package attestation_test
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"fmt"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/docker/attest/attestation"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/oci"
"github.com/docker/attest/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"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSignVerifyAttestation(t *testing.T) {
ctx, signer := test.Setup(t)
stmt := &intoto.Statement{
StatementHeader: intoto.StatementHeader{
Type: intoto.StatementInTotoV01,
PredicateType: intoto.PredicateSPDX,
},
Predicate: "test",
}
payload, err := json.Marshal(stmt)
require.NoError(t, err)
opts := &attestation.SigningOptions{}
env, err := attestation.SignDSSE(ctx, payload, signer, opts)
require.NoError(t, err)
// marshal envelope to json to test for bugs when marshaling envelope data
serializedEnv, err := json.Marshal(env)
require.NoError(t, err)
deserializedEnv := new(attestation.Envelope)
err = json.Unmarshal(serializedEnv, deserializedEnv)
require.NoError(t, err)
// signer.Public() calls AWS API when using AWS signer, use attestation.GetPublicVerificationKey() to get key from TUF repo
// signer.Public() used here for test purposes
ecPub, ok := signer.Public().(*ecdsa.PublicKey)
assert.True(t, ok)
pem, err := signerverifier.ConvertToPEM(ecPub)
assert.NoError(t, err)
keyID, err := signerverifier.KeyID(ecPub)
assert.NoError(t, err)
badKeyPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
badKey := &badKeyPriv.PublicKey
badPEM, err := signerverifier.ConvertToPEM(badKey)
require.NoError(t, err)
testCases := []struct {
name string
keyID string
pem []byte
distrust bool
from time.Time
to *time.Time
status string
expectedError string
}{
{
name: "all OK",
keyID: keyID,
pem: pem,
distrust: false,
from: time.Time{},
to: nil,
status: "active",
expectedError: "",
},
{
name: "key not found",
keyID: "someotherkey",
pem: pem,
distrust: false,
from: time.Time{},
to: nil,
status: "active",
expectedError: fmt.Sprintf("key not found: %s", keyID),
},
{
name: "key distrusted",
keyID: keyID,
pem: pem,
distrust: true,
from: time.Time{},
to: nil,
status: "active",
expectedError: "distrusted",
},
{
name: "key not yet valid",
keyID: keyID,
pem: pem,
distrust: false,
from: time.Now().Add(time.Hour),
to: nil,
status: "active",
expectedError: "not yet valid",
},
{
name: "key already revoked",
keyID: keyID,
pem: pem,
distrust: false,
from: time.Time{},
to: new(time.Time),
status: "revoked",
expectedError: "already revoked",
},
{
name: "bad key",
keyID: keyID,
pem: badPEM,
distrust: false,
from: time.Time{},
to: nil,
status: "active",
expectedError: "signature is not valid",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
keyMeta := &attestation.KeyMetadata{
ID: tc.keyID,
PEM: string(tc.pem),
Distrust: tc.distrust,
From: tc.from,
To: tc.to,
Status: tc.status,
}
opts := &attestation.VerifyOptions{
Keys: attestation.Keys{keyMeta},
}
_, err = attestation.VerifyDSSE(ctx, deserializedEnv, opts)
if tc.expectedError != "" {
assert.Contains(t, err.Error(), tc.expectedError)
} else {
assert.NoError(t, err)
}
})
}
}
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

@@ -7,6 +7,7 @@ import (
v1 "github.com/google/go-containerregistry/pkg/v1"
intoto "github.com/in-toto/in-toto-golang/in_toto"
v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
slsav1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
)
@@ -15,8 +16,8 @@ const (
AttestationManifestType = "attestation-manifest"
InTotoPredicateType = "in-toto.io/predicate-type"
DockerReferenceDigest = "vnd.docker.reference.digest"
DockerDsseExtKind = "application/vnd.docker.attestation-verification.v1+json"
RekorTlExtKind = "Rekor"
DockerDSSEExtKind = "application/vnd.docker.attestation-verification.v1+json"
RekorTLExtKind = "Rekor"
OCIDescriptorDSSEMediaType = ociv1.MediaTypeDescriptor + "+dsse"
InTotoReferenceLifecycleStage = "vnd.docker.lifecycle-stage"
LifecycleStageExperimental = "experimental"
@@ -24,58 +25,64 @@ const (
var base64Encoding = base64.StdEncoding.Strict()
type AttestationLayer struct {
type Layer struct {
Statement *intoto.Statement
Layer v1.Layer
Annotations map[string]string
}
type AttestationManifest struct {
type Manifest struct {
OriginalDescriptor *v1.Descriptor
OriginalLayers []*AttestationLayer
OriginalLayers []*Layer
// accumulated during signing
SignedLayers []*AttestationLayer
// details of subect image
SignedLayers []*Layer
// details of subject image
SubjectName string
SubjectDescriptor *v1.Descriptor
}
type AttestationManifestImageOptions struct {
type ManifestImageOptions struct {
// how to output the image
skipSubject bool
replaceLayers bool
laxReferrers bool
}
// the following types are needed until https://github.com/secure-systems-lab/dsse/pull/61 is merged
// the following types are needed until https://github.com/secure-systems-lab/dsse/pull/61 is merged.
type Envelope struct {
PayloadType string `json:"payloadType"`
Payload string `json:"payload"`
Signatures []Signature `json:"signatures"`
PayloadType string `json:"payloadType"`
Payload string `json:"payload"`
Signatures []*Signature `json:"signatures"`
}
type Signature struct {
KeyID string `json:"keyid"`
Sig string `json:"sig"`
Extension Extension `json:"extension,omitempty"`
KeyID string `json:"keyid"`
Sig string `json:"sig"`
Extension *Extension `json:"extension,omitempty"`
}
type Extension struct {
Kind string `json:"kind"`
Ext DockerDsseExtension `json:"ext"`
Kind string `json:"kind"`
Ext *DockerDSSEExtension `json:"ext"`
}
type DockerDsseExtension struct {
Tl DockerTlExtension `json:"tl"`
type AnnotatedStatement struct {
OCIDescriptor *v1.Descriptor
InTotoStatement *intoto.Statement
Annotations map[string]string
}
type DockerTlExtension struct {
type DockerDSSEExtension struct {
TL *DockerTLExtension `json:"tl"`
}
type DockerTLExtension struct {
Kind string `json:"kind"`
Data any `json:"data"`
}
type VerifyOptions struct {
Keys []KeyMetadata `json:"keys"`
SkipTL bool `json:"skip_tl"`
Keys []*KeyMetadata `json:"keys"`
SkipTL bool `json:"skip_tl"`
}
type SigningOptions struct {
@@ -83,9 +90,17 @@ type SigningOptions struct {
SkipTL bool
}
type Options struct {
NoReferrers bool
Attach bool
ReferrersRepo string
}
func DSSEMediaType(predicateType string) (string, error) {
var predicateName string
switch predicateType {
case slsav1.PredicateSLSAProvenance:
predicateName = "provenance"
case v02.PredicateSLSAProvenance:
predicateName = "provenance"
case intoto.PredicateSPDX:

44
attestation/types_test.go Normal file
View File

@@ -0,0 +1,44 @@
package attestation
import (
"fmt"
"testing"
intoto "github.com/in-toto/in-toto-golang/in_toto"
v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
slsav1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDSSEMediaType(t *testing.T) {
testcases := []struct {
name string
expected string
}{
{
name: slsav1.PredicateSLSAProvenance,
expected: "provenance",
},
{
name: v02.PredicateSLSAProvenance,
expected: "provenance",
},
{
name: intoto.PredicateSPDX,
expected: "spdx",
},
{
name: VSAPredicateType,
expected: "verification_summary",
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
mt, err := DSSEMediaType(tc.name)
require.NoError(t, err)
assert.Equal(t, fmt.Sprintf("application/vnd.in-toto.%s+dsse", tc.expected), mt)
})
}
}

View File

@@ -10,8 +10,8 @@ import (
"time"
"github.com/docker/attest/internal/util"
"github.com/docker/attest/pkg/signerverifier"
"github.com/docker/attest/pkg/tlog"
"github.com/docker/attest/signerverifier"
"github.com/docker/attest/tlog"
intoto "github.com/in-toto/in-toto-golang/in_toto"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/secure-systems-lab/go-securesystemslib/dsse"
@@ -27,8 +27,10 @@ type KeyMetadata struct {
Distrust bool `json:"distrust,omitempty"`
}
type Keys []KeyMetadata
type KeysMap map[string]KeyMetadata
type (
Keys []*KeyMetadata
KeysMap map[string]*KeyMetadata
)
func VerifyDSSE(ctx context.Context, env *Envelope, opts *VerifyOptions) ([]byte, error) {
// enforce payload type
@@ -58,8 +60,8 @@ func VerifyDSSE(ctx context.Context, env *Envelope, opts *VerifyOptions) ([]byte
return payload, nil
}
func verifySignature(ctx context.Context, sig Signature, payload []byte, opts *VerifyOptions) error {
keys := make(map[string]KeyMetadata, len(opts.Keys))
func verifySignature(ctx context.Context, sig *Signature, payload []byte, opts *VerifyOptions) error {
keys := make(map[string]*KeyMetadata, len(opts.Keys))
for _, key := range opts.Keys {
keys[key.ID] = key
}
@@ -72,26 +74,25 @@ func verifySignature(ctx context.Context, sig Signature, payload []byte, opts *V
return fmt.Errorf("key %s is distrusted", keyMeta.ID)
}
// TODO: this is unmarshalling with MarshalPKIXPublicKey only for us to marshal it again
publicKey, err := signerverifier.Parse([]byte(keyMeta.PEM))
publicKey, err := signerverifier.ParsePublicKey([]byte(keyMeta.PEM))
if err != nil {
return fmt.Errorf("failed to parse public key: %w", err)
}
if !opts.SkipTL {
t := tlog.GetTL(ctx)
if sig.Extension.Kind == "" {
return fmt.Errorf("error missing signature extension kind")
if sig.Extension == nil || sig.Extension.Kind == "" {
return fmt.Errorf("error missing signature extension")
}
if sig.Extension.Kind != DockerDsseExtKind {
if sig.Extension.Kind != DockerDSSEExtKind {
return fmt.Errorf("error unsupported signature extension kind: %s", sig.Extension.Kind)
}
// verify TL entry
if sig.Extension.Ext.Tl.Kind != RekorTlExtKind {
return fmt.Errorf("error unsupported TL extension kind: %s", sig.Extension.Ext.Tl.Kind)
if sig.Extension.Ext.TL.Kind != RekorTLExtKind {
return fmt.Errorf("error unsupported TL extension kind: %s", sig.Extension.Ext.TL.Kind)
}
entry := sig.Extension.Ext.Tl.Data
entry := sig.Extension.Ext.TL.Data
entryBytes, err := json.Marshal(entry)
if err != nil {
return fmt.Errorf("failed to marshal TL entry: %w", err)

View File

@@ -4,8 +4,8 @@ import (
"encoding/base64"
"testing"
"github.com/docker/attest/attestation"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/attestation"
intoto "github.com/in-toto/in-toto-golang/in_toto"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/assert"
@@ -35,7 +35,7 @@ func TestVerifyUnsignedAttestation(t *testing.T) {
payload := []byte("payload")
env := &attestation.Envelope{
// no signatures
Signatures: []attestation.Signature{},
Signatures: []*attestation.Signature{},
Payload: base64.StdEncoding.EncodeToString(payload),
PayloadType: intoto.PayloadType,
}

View File

@@ -14,9 +14,9 @@ const (
type VSAPredicate struct {
Verifier VSAVerifier `json:"verifier"`
TimeVerified string `json:"timeVerified"`
ResourceUri string `json:"resourceUri"`
ResourceURI string `json:"resourceUri"`
Policy VSAPolicy `json:"policy"`
InputAttestations []VSAInputAttestation `json:"inputAttestations"`
InputAttestations []VSAInputAttestation `json:"inputAttestations,omitempty"`
VerificationResult string `json:"verificationResult"`
VerifiedLevels []string `json:"verifiedLevels"`
}
@@ -26,7 +26,9 @@ type VSAVerifier struct {
}
type VSAPolicy struct {
URI string `json:"uri"`
URI string `json:"uri,omitempty"`
Digest map[string]string `json:"digest"`
DownloadLocation string `json:"downloadLocation,omitempty"`
}
type VSAInputAttestation struct {
@@ -35,7 +37,7 @@ type VSAInputAttestation struct {
}
func ToVSAResourceURI(sub intoto.Subject) (string, error) {
//parse purl
// parse purl
purl, err := packageurl.FromString(sub.Name)
if err != nil {
return "", fmt.Errorf("failed to parse package url: %w", err)

View File

@@ -1,2 +1,5 @@
ignore:
- "internal/test"
coverage:
status:
patch: false

120
config/config.go Normal file
View File

@@ -0,0 +1,120 @@
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"github.com/docker/attest/tuf"
"sigs.k8s.io/yaml"
)
const (
MappingFilename = "mapping.yaml"
)
func validateMappingsFile(mappings *policyMappingsFile) error {
var validationErrors []error
if mappings.Kind != "policy-mapping" {
validationErrors = append(validationErrors, fmt.Errorf("file is not of kind policy-mapping: %s", mappings.Kind))
}
if mappings.Version != "v1" {
validationErrors = append(validationErrors, fmt.Errorf("unsupported policy mapping file version: %s", mappings.Version))
}
for _, rule := range mappings.Rules {
if rule.Pattern == "" {
validationErrors = append(validationErrors, fmt.Errorf("rule missing pattern: %s", rule))
}
if rule.PolicyID == "" && rule.Replacement == "" {
validationErrors = append(validationErrors, fmt.Errorf("rule must have policy-id or replacement: %s", rule))
}
if rule.PolicyID != "" && rule.Replacement != "" {
validationErrors = append(validationErrors, fmt.Errorf("rule cannot have both policy-id and replacement: %s", rule))
}
}
for _, policy := range mappings.Policies {
if policy.ID == "" {
validationErrors = append(validationErrors, fmt.Errorf("policy missing id: %s", policy.ID))
}
if len(policy.Files) == 0 {
validationErrors = append(validationErrors, fmt.Errorf("policy missing files: %v", policy))
}
for _, file := range policy.Files {
if file.Path == "" {
validationErrors = append(validationErrors, fmt.Errorf("file missing path: %s", file))
}
}
}
if len(validationErrors) > 0 {
return errors.Join(validationErrors...)
}
return nil
}
func parsePolicyMappingsFile(data []byte) (*PolicyMappings, error) {
mappings := &policyMappingsFile{}
err := yaml.Unmarshal(data, mappings)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal policy mapping file: %w", err)
}
err = validateMappingsFile(mappings)
if err != nil {
return nil, fmt.Errorf("invalid policy mapping file: %w", err)
}
return expandMappingFile(mappings)
}
func LoadLocalMappings(configDir string) (*PolicyMappings, error) {
if configDir == "" {
return nil, nil
}
path := filepath.Join(configDir, MappingFilename)
mappingFile, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read local policy mapping file %s: %w", path, err)
}
return parsePolicyMappingsFile(mappingFile)
}
func LoadTUFMappings(tufClient tuf.Downloader, localTargetsDir string) (*PolicyMappings, error) {
if tufClient == nil {
return nil, fmt.Errorf("tuf client not set")
}
filename := MappingFilename
file, err := tufClient.DownloadTarget(filename, filepath.Join(localTargetsDir, filename))
if err != nil {
return nil, fmt.Errorf("failed to download policy mapping file %s: %w", filename, err)
}
return parsePolicyMappingsFile(file.Data)
}
func expandMappingFile(mappingFile *policyMappingsFile) (*PolicyMappings, error) {
policies := make(map[string]*PolicyMapping)
for _, policy := range mappingFile.Policies {
policies[policy.ID] = policy
}
var rules []*PolicyRule
for _, rule := range mappingFile.Rules {
r, err := regexp.Compile(rule.Pattern)
if err != nil {
return nil, err
}
rules = append(rules, &PolicyRule{
Pattern: r,
PolicyID: rule.PolicyID,
Replacement: rule.Replacement,
})
}
return &PolicyMappings{
Version: mappingFile.Version,
Kind: mappingFile.Kind,
Policies: policies,
Rules: rules,
}, nil
}

82
config/config_test.go Normal file
View File

@@ -0,0 +1,82 @@
package config
import (
"testing"
"github.com/stretchr/testify/require"
)
func newMapping() *policyMappingsFile {
return &policyMappingsFile{
Version: "v1",
Kind: "policy-mapping",
Policies: []*PolicyMapping{
{
ID: "docker-official-images",
Files: []PolicyMappingFile{
{
Path: "docker.io/library/alpine",
},
},
},
},
Rules: []*policyRuleFile{
{
Pattern: "docker.io/library/alpine",
PolicyID: "docker-official-images",
},
},
}
}
func TestMappingsFileValidation(t *testing.T) {
mappings := newMapping()
err := validateMappingsFile(mappings)
require.NoError(t, err)
mappings = newMapping()
mappings.Kind = "not-policy-mapping"
err = validateMappingsFile(mappings)
require.ErrorContains(t, err, "file is not of kind policy-mapping: not-policy-mapping")
mappings = newMapping()
mappings.Version = "v2"
err = validateMappingsFile(mappings)
require.ErrorContains(t, err, "unsupported policy mapping file version: v2")
mappings = newMapping()
mappings.Rules[0].Pattern = ""
err = validateMappingsFile(mappings)
require.ErrorContains(t, err, "rule missing pattern")
mappings = newMapping()
mappings.Rules[0].PolicyID = ""
err = validateMappingsFile(mappings)
require.ErrorContains(t, err, "rule must have policy-id or replacement")
mappings = newMapping()
mappings.Rules[0].PolicyID = "docker-official-images"
mappings.Rules[0].Replacement = "docker.io/library/alpine"
err = validateMappingsFile(mappings)
require.ErrorContains(t, err, "rule cannot have both policy-id and replacement")
mappings = newMapping()
mappings.Policies[0].ID = ""
err = validateMappingsFile(mappings)
require.ErrorContains(t, err, "policy missing id")
mappings = newMapping()
mappings.Policies[0].Files = nil
err = validateMappingsFile(mappings)
require.ErrorContains(t, err, "policy missing files")
mappings = newMapping()
mappings.Policies[0].Files[0].Path = ""
err = validateMappingsFile(mappings)
require.ErrorContains(t, err, "file missing path")
// multiple errors
mappings.Policies[0].ID = ""
err = validateMappingsFile(mappings)
require.ErrorContains(t, err, "policy missing id: \nfile missing path: {}")
}

View File

@@ -13,7 +13,7 @@ type policyMappingsFile struct {
type policyRuleFile struct {
Pattern string `json:"pattern"`
PolicyId string `json:"policy-id"`
PolicyID string `json:"policy-id"`
Replacement string `json:"rewrite"`
}
@@ -32,7 +32,7 @@ const (
)
type PolicyMapping struct {
Id string `json:"id"`
ID string `json:"id"`
Description string `json:"description"`
Files []PolicyMappingFile `json:"files"`
Attestations *AttestationConfig `json:"attestations"`
@@ -49,6 +49,6 @@ type PolicyMappingFile struct {
type PolicyRule struct {
Pattern *regexp.Regexp
PolicyId string
PolicyID string
Replacement string
}

View File

@@ -3,11 +3,10 @@ package attest_test
import (
"context"
"github.com/docker/attest/pkg/attest"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/mirror"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/signerverifier"
"github.com/docker/attest"
"github.com/docker/attest/attestation"
"github.com/docker/attest/oci"
"github.com/docker/attest/signerverifier"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
@@ -55,7 +54,7 @@ func ExampleSignStatements_remote() {
}
// push image index with signed attestation-manifests
err = mirror.PushIndexToRegistry(signedIndex, ref)
err = oci.PushIndexToRegistry(signedIndex, ref)
if err != nil {
panic(err)
}
@@ -66,11 +65,11 @@ func ExampleSignStatements_remote() {
Add: signedIndex,
Descriptor: v1.Descriptor{
Annotations: map[string]string{
oci.OciReferenceTarget: attIdx.Name,
oci.OCIReferenceTarget: attIdx.Name,
},
},
})
err = mirror.SaveIndexAsOCILayout(idx, path)
err = oci.SaveIndexAsOCILayout(idx, path)
if err != nil {
panic(err)
}

54
example_verify_test.go Normal file
View File

@@ -0,0 +1,54 @@
package attest_test
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/docker/attest"
"github.com/docker/attest/oci"
"github.com/docker/attest/policy"
"github.com/docker/attest/tuf"
)
func ExampleVerify_remote() {
// create a tuf client
home, err := os.UserHomeDir()
if err != nil {
panic(err)
}
tufOutputPath := filepath.Join(home, ".docker", "tuf")
tufClientOpts := tuf.NewDockerDefaultClientOptions(tufOutputPath)
// create a resolver for remote attestations
image := "registry-1.docker.io/library/notary:server"
platform := "linux/amd64"
// configure policy options
opts := &policy.Options{
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
DisableTUF: false, // set to disable TUF and rely on local policy files
}
src, err := oci.ParseImageSpec(image, oci.WithPlatform(platform))
if err != nil {
panic(err)
}
// verify attestations
result, err := attest.Verify(context.Background(), src, opts)
if err != nil {
panic(err)
}
switch result.Outcome {
case attest.OutcomeSuccess:
fmt.Println("policy passed")
case attest.OutcomeNoPolicy:
fmt.Println("no policy for image")
case attest.OutcomeFailure:
fmt.Println("policy failed")
}
}

124
go.mod
View File

@@ -3,71 +3,70 @@ module github.com/docker/attest
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/Masterminds/semver/v3 v3.3.0
github.com/aws/aws-sdk-go-v2/config v1.27.33
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
github.com/go-openapi/runtime v0.28.0
github.com/go-openapi/strfmt v0.23.0
github.com/google/go-containerregistry v0.20.1
github.com/google/go-containerregistry v0.20.2
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/in-toto/in-toto-golang v0.9.0
github.com/open-policy-agent/opa v0.67.0
github.com/open-policy-agent/opa v0.68.0
github.com/opencontainers/image-spec v1.1.0
github.com/package-url/packageurl-go v0.1.3
github.com/secure-systems-lab/go-securesystemslib v0.8.0
github.com/sigstore/cosign/v2 v2.3.0
github.com/sigstore/cosign/v2 v2.4.0
github.com/sigstore/rekor v1.3.6
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.7
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.7
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.9
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.9
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.33.0
github.com/theupdateframework/go-tuf/v2 v2.0.0
google.golang.org/api v0.189.0
google.golang.org/api v0.196.0
sigs.k8s.io/yaml v1.4.0
)
// fork of a fork (in case it goes away) with changes to support ArtifactType (https://github.com/google/go-containerregistry/pull/1931)
replace github.com/google/go-containerregistry => github.com/kipz/go-containerregistry v0.0.0-20240722163910-ebe90246535d
// fork with changes to support ArtifactType (https://github.com/google/go-containerregistry/pull/1931)
replace github.com/google/go-containerregistry => github.com/docker/go-containerregistry v0.0.0-20240808132857-c8bfc44af7c8
require (
cloud.google.com/go v0.115.0 // indirect
cloud.google.com/go/auth v0.7.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
cloud.google.com/go v0.115.1 // indirect
cloud.google.com/go/auth v0.9.3 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
cloud.google.com/go/compute/metadata v0.5.0 // indirect
cloud.google.com/go/iam v1.1.10 // indirect
cloud.google.com/go/kms v1.18.2 // indirect
cloud.google.com/go/longrunning v0.5.9 // indirect
cloud.google.com/go/iam v1.2.0 // indirect
cloud.google.com/go/kms v1.19.0 // indirect
cloud.google.com/go/longrunning v0.6.0 // indirect
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.12.3 // indirect
github.com/OneOfOne/xxhash v1.2.8 // indirect
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.5 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // 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/kms v1.35.1 // 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/internal/accept-encoding v1.11.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.35.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // 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
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.3.8 // indirect
github.com/containerd/containerd v1.7.20 // indirect
github.com/containerd/containerd v1.7.21 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
github.com/cpuguy83/dockercfg v0.3.1 // indirect
@@ -75,9 +74,9 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect
github.com/docker/cli v26.1.3+incompatible // indirect
github.com/docker/cli v27.1.1+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v27.0.3+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
@@ -101,22 +100,21 @@ require (
github.com/gobwas/glob v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/certificate-transparency-go v1.2.1 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.3 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/hcl v1.0.1-vault-5 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 // indirect
github.com/jellydator/ttlcache/v3 v3.2.0 // indirect
github.com/jellydator/ttlcache/v3 v3.3.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
github.com/magiconair/properties v1.8.7 // indirect
@@ -126,7 +124,8 @@ require (
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/sys/user v0.3.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
@@ -138,7 +137,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_golang v1.20.2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
@@ -149,7 +148,8 @@ require (
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/shirou/gopsutil/v3 v3.24.4 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sigstore/sigstore v1.8.7 // indirect
github.com/sigstore/protobuf-specs v0.3.2 // indirect
github.com/sigstore/sigstore v1.8.8 // indirect
github.com/sigstore/timestamp-authority v1.2.2 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
@@ -161,7 +161,7 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
github.com/testcontainers/testcontainers-go v0.32.0 // indirect
github.com/testcontainers/testcontainers-go v0.33.0 // indirect
github.com/theupdateframework/go-tuf v0.7.0 // indirect
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
@@ -174,28 +174,28 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver v1.15.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/term v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto v0.0.0-20240722135656-d784300faade // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect
google.golang.org/grpc v1.65.0 // indirect
golang.org/x/mod v0.19.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/oauth2 v0.22.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/term v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/time v0.6.0 // indirect
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/grpc v1.66.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

276
go.sum
View File

@@ -1,18 +1,18 @@
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.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE=
cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
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 v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=
cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U=
cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk=
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/iam v1.1.10 h1:ZSAr64oEhQSClwBL670MsJAW5/RLiC6kfw3Bqmd5ZDI=
cloud.google.com/go/iam v1.1.10/go.mod h1:iEgMq62sg8zx446GCaijmA2Miwg5o3UbO+nI47WHJps=
cloud.google.com/go/kms v1.18.2 h1:EGgD0B9k9tOOkbPhYW1PHo2W0teamAUYMOUIcDRMfPk=
cloud.google.com/go/kms v1.18.2/go.mod h1:YFz1LYrnGsXARuRePL729oINmN5J/5e7nYijgvfiIeY=
cloud.google.com/go/longrunning v0.5.9 h1:haH9pAuXdPAMqHvzX0zlWQigXT7B0+CL4/2nXXdBo5k=
cloud.google.com/go/longrunning v0.5.9/go.mod h1:HD+0l9/OOW0za6UWdKJtXoFAX/BGg/3Wj8p10NeWF7c=
cloud.google.com/go/iam v1.2.0 h1:kZKMKVNk/IsSSc/udOb83K0hL/Yh/Gcqpz+oAkoIFN8=
cloud.google.com/go/iam v1.2.0/go.mod h1:zITGuWgsLZxd8OwAlX+eMFgZDXzBm7icj1PVTYG766Q=
cloud.google.com/go/kms v1.19.0 h1:x0OVJDl6UH1BSX4THKlMfdcFWoE4ruh90ZHuilZekrU=
cloud.google.com/go/kms v1.19.0/go.mod h1:e4imokuPJUc17Trz2s6lEXFDt8bgDmvpVynH39bdrHM=
cloud.google.com/go/longrunning v0.6.0 h1:mM1ZmaNsQsnb+5n1DNPeL0KwQd9jQRqSqSDEkBZr+aI=
cloud.google.com/go/longrunning v0.6.0/go.mod h1:uHzSZqW89h7/pasCWNYdUpwGz3PcVWhrWupreVPYLts=
cuelabs.dev/go/oci/ociregistry v0.0.0-20240404174027-a39bec0462d2 h1:BnG6pr9TTr6CYlrJznYUDj6V7xldD1W+1iXPum0wT/w=
cuelabs.dev/go/oci/ociregistry v0.0.0-20240404174027-a39bec0462d2/go.mod h1:pK23AUVXuNzzTpfMCA06sxZGeVQ/75FdVtW249de9Uo=
cuelang.org/go v0.9.2 h1:pfNiry2PdRBr02G/aKm5k2vhzmqbAOoaB4WurmEbWvs=
@@ -29,12 +29,12 @@ github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo
github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo/helper v0.2.0/go.mod h1:GgeIE+1be8Ivm7Sh4RgwI42aTtC9qrcj+Y9Y6CjJhJs=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 h1:1nGuui+4POelzDwI7RG56yfQJHCnKvwfMoU7VsEp+Zg=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0/go.mod h1:99EvauvlcJ1U06amZiksfYz/3aFGyIhWGHVyiZXtBAI=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0 h1:GJHeeA2N7xrG3q30L2UXDyuWRzDM900/65j70wcM4Ww=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 h1:H+U3Gk9zY56G3u872L82bk4thcsy2Gghb9ExT4Zvm1o=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0/go.mod h1:mgrmMSgaLp9hmax62XQTd0N4aAqSE5E0DulSpVYK7vc=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 h1:DRiANoJTiW6obBQe3SqZizkuV1PEgfiiGivmVocDy64=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0/go.mod h1:qLIye2hwb/ZouqhpSD9Zn3SJipvpEnz1Ywl3VUk9Y0s=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
@@ -60,12 +60,10 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.12.3 h1:LS9NXqXhMoqNCplK1ApmVSfB4UnVLRDWRapB6EIlxE0=
github.com/Microsoft/hcsshim v0.12.3/go.mod h1:Iyl1WVpZzr+UkzjekHZbV8o5Z9ZkxNGx6CtY2Qg/JVQ=
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
@@ -102,48 +100,48 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.54.19 h1:tyWV+07jagrNiCcGRzRhdtVjQs7Vy41NwsuOcl0IbVI=
github.com/aws/aws-sdk-go v1.54.19/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 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.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g=
github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
github.com/aws/aws-sdk-go-v2/config v1.27.33 h1:Nof9o/MsmH4oa0s2q9a0k7tMz5x/Yj5k06lDODWz3BU=
github.com/aws/aws-sdk-go-v2/config v1.27.33/go.mod h1:kEqdYzRb8dd8Sy2pOdEbExTTF5v7ozEXX0McgPE7xks=
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 h1:7Cxhp/BnT2RcGy4VisJ9miUPecY+lyE9I8JvcZofn9I=
github.com/aws/aws-sdk-go-v2/credentials v1.17.32/go.mod h1:P5/QMF3/DCHbXGEGkdbilXHsyTBX5D3HSwcrSc9p20I=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU=
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/kms v1.35.1 h1:0gP2OJJT6HM2BYltZ9x+A87OE8LJL96DXeAAdLv3t1M=
github.com/aws/aws-sdk-go-v2/service/kms v1.35.1/go.mod h1:hGONorZkQCfR5DW6l2xdy7zC8vfO0r9pJlwyg6gmGeo=
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/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.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE=
github.com/aws/aws-sdk-go-v2/service/kms v1.35.5 h1:XUomV7SiclZl1QuXORdGcfFqHxEHET7rmNGtxTfNB+M=
github.com/aws/aws-sdk-go-v2/service/kms v1.35.5/go.mod h1:A5CS0VRmxxj2YKYLCY08l/Zzbd01m6JZn0WzxgT1OCA=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 h1:pIaGg+08llrP7Q5aiz9ICWbY8cqhTkyy+0SHvfzQpTc=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 h1:/Cfdu0XV3mONYKaOt1Gr0k1KvQzkzPyiKUdlWJqy+J4=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 h1:NKTa1eqZYw8tiHSRGpP0VtTdub/8KNk8sDkNPFaOKDE=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o=
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=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/buildkite/agent/v3 v3.75.1 h1:FJg1gss9nUESExTsfx6yWqs/g20Vyd4pHSEB9iuT4pI=
github.com/buildkite/agent/v3 v3.75.1/go.mod h1:UXCAEaqJnw5UsoZjJ9NxMvdSGc4oMHBnQFQqzGPDM0Y=
github.com/buildkite/agent/v3 v3.76.2 h1:SweFq3e0N20RikWsVeOXzTjfr0AoOskxm9c0bcNyI0E=
github.com/buildkite/agent/v3 v3.76.2/go.mod h1:9ffbmJD7d7C/nOcElj6Qm+uIj1QoYh3NNvka4rkKkss=
github.com/buildkite/go-pipeline v0.10.0 h1:EDffu+LfMY2k5u+iEdo6Jn3obGKsrL5wicc1O/yFeRs=
github.com/buildkite/go-pipeline v0.10.0/go.mod h1:eMH1kiav5VeiTiu0Mk2/M7nZhKyFeL4iGj7Y7rj4f3w=
github.com/buildkite/interpolate v0.1.3 h1:OFEhqji1rNTRg0u9DsSodg63sjJQEb1uWbENq9fUOBM=
@@ -180,8 +178,8 @@ github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUo
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ=
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w=
github.com/containerd/containerd v1.7.20 h1:Sl6jQYk3TRavaU83h66QMbI2Nqg9Jm6qzwX57Vsn1SQ=
github.com/containerd/containerd v1.7.20/go.mod h1:52GsS5CwquuqPuLncsXwG0t2CiUce+KsNHJZQJvAgR0=
github.com/containerd/containerd v1.7.21 h1:USGXRK1eOC/SX0L195YgxTHb0a00anxajOzgfN0qrCA=
github.com/containerd/containerd v1.7.21/go.mod h1:e3Jz1rYRUZ2Lt51YrH9Rz0zPyJBOlSvB3ghr2jbVD8g=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
@@ -218,16 +216,18 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v26.1.3+incompatible h1:bUpXT/N0kDE3VUHI2r5VMsYQgi38kYuoC0oL9yt3lqc=
github.com/docker/cli v26.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE=
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.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE=
github.com/docker/docker v27.0.3+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=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-containerregistry v0.0.0-20240808132857-c8bfc44af7c8 h1:T/wutVfQ1Oj4H5tbP5IZL5l6PZqzvapVJ5cB4Wy4Ucc=
github.com/docker/go-containerregistry v0.0.0-20240808132857-c8bfc44af7c8/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@@ -353,8 +353,8 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w=
github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM=
github.com/google/trillian v1.6.0 h1:jMBeDBIkINFvS2n6oV5maDqfRlxREAc6CW9QYWQ0qT4=
@@ -362,10 +362,10 @@ github.com/google/trillian v1.6.0/go.mod h1:Yu3nIMITzNhhMJEHjAtp6xKiu+H/iHu2Oq5F
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA=
github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
github.com/googleapis/enterprise-certificate-proxy v0.3.3 h1:QRje2j5GZimBzlbhGA2V2QlGNgL8G6e+wGo/+/2bWI0=
github.com/googleapis/enterprise-certificate-proxy v0.3.3/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
@@ -405,8 +405,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 h1:TMtDYDHKYY15rFihtRfck/bfFqNfvcabqvXAFQfAUpY=
github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267/go.mod h1:h1nSAbGFqGVzn6Jyl1R/iCcBUHN4g+gW1u9CoBTrb9E=
github.com/jellydator/ttlcache/v3 v3.2.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++uW8a3LE=
github.com/jellydator/ttlcache/v3 v3.2.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc=
github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
@@ -417,12 +417,10 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kipz/go-containerregistry v0.0.0-20240722163910-ebe90246535d h1:5QaWAwKhslfqxEyMZY0ofvsbMJkMLcx5E30JFufMVj8=
github.com/kipz/go-containerregistry v0.0.0-20240722163910-ebe90246535d/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -458,8 +456,10 @@ github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkV
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
@@ -493,8 +493,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/open-policy-agent/opa v0.67.0 h1:FOdsO9yNhfmrh+72oVK7ImWmzruG+VSpfbr5IBqEWVs=
github.com/open-policy-agent/opa v0.67.0/go.mod h1:aqKlHc8E2VAAylYE9x09zJYr/fYzGX+JKne89UGqFzk=
github.com/open-policy-agent/opa v0.68.0 h1:Jl3U2vXRjwk7JrHmS19U3HZO5qxQRinQbJ2eCJYSqJQ=
github.com/open-policy-agent/opa v0.68.0/go.mod h1:5E5SvaPwTpwt2WM177I9Z3eT7qUpmOGjk1ZdHs+TZ4w=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -517,8 +517,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg=
github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
@@ -555,22 +555,26 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sigstore/cosign/v2 v2.3.0 h1:rBLVdKMYuER0blmaKMfMNkvawBdK1lTMz2L5PtTPrI8=
github.com/sigstore/cosign/v2 v2.3.0/go.mod h1:tjACBZS6LoH3bap5hU8MxyGC4DIJiatqhZxuJWFcIJ0=
github.com/sigstore/cosign/v2 v2.4.0 h1:2NdidNgClg+oXr/fDIr37E/BE6j00gqgUhSiBK2kjSQ=
github.com/sigstore/cosign/v2 v2.4.0/go.mod h1:j+fH1DCUkcn92qp6ezDj4JbGMri6eG1nLJC+hs64rvc=
github.com/sigstore/fulcio v1.5.1 h1:Iasy1zfNjaq8BV4S8o6pXspLDU28PQC2z07GmOu9zpM=
github.com/sigstore/fulcio v1.5.1/go.mod h1:W1A/UHrTopy1IBZPMtHmxg7GPYAu+vt5dRXM3W6yjPo=
github.com/sigstore/protobuf-specs v0.3.2 h1:nCVARCN+fHjlNCk3ThNXwrZRqIommIeNKWwQvORuRQo=
github.com/sigstore/protobuf-specs v0.3.2/go.mod h1:RZ0uOdJR4OB3tLQeAyWoJFbNCBFrPQdcokntde4zRBA=
github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8=
github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc=
github.com/sigstore/sigstore v1.8.7 h1:L7/zKauHTg0d0Hukx7qlR4nifh6T6O6UIt9JBwAmTIg=
github.com/sigstore/sigstore v1.8.7/go.mod h1:MPiQ/NIV034Fc3Kk2IX9/XmBQdK60wfmpvgK9Z1UjRA=
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.7 h1:SoahswHQm2JhO8h3KTAeX8IZeE7mSR2Lc53ay5choes=
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.7/go.mod h1:TOVOPOqldrrz4dP7x4/0DFQTs9QSXZUoHu21+JHmixA=
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.7 h1:jcdKxc5bvwfL7+ZbeCszaN/qsBd6180fGAHxeX5Ckm0=
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.7/go.mod h1:gakVcpRiN+aFdLhPXXP8ubOCWiedM1YJ/gR74ez/tT0=
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.7 h1:zYg1XlbKpQkmE7FpWTkLuUn7RttLAq4FcZ1G9JcqhoY=
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.7/go.mod h1:VmUsO1R4OHuyHBEgI4bbSUn0z2nojszrDMvlDxyX/dE=
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.7 h1:dbcB9VEddYrvK+y4rHeES5OZ/pMQuucfJ0qCNWQmnp0=
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.7/go.mod h1:96MrPJBkHiAvqFyqviuYbwPAdbPCj8CR3V0RJ9bKjrE=
github.com/sigstore/sigstore v1.8.8 h1:B6ZQPBKK7Z7tO3bjLNnlCMG+H66tO4E/+qAphX8T/hg=
github.com/sigstore/sigstore v1.8.8/go.mod h1:GW0GgJSCTBJY3fUOuGDHeFWcD++c4G8Y9K015pwcpDI=
github.com/sigstore/sigstore-go v0.5.1 h1:5IhKvtjlQBeLnjKkzMELNG4tIBf+xXQkDzhLV77+/8Y=
github.com/sigstore/sigstore-go v0.5.1/go.mod h1:TuOfV7THHqiDaUHuJ5+QN23RP/YoKmsbwJpY+aaYPN0=
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.9 h1:tgpdvjyoEgYFeTBFe4MHvBKsG+J4E7NVtstChIExVT8=
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.9/go.mod h1:wCz6cAZKL/wFumDHX9l8VkVITS2GntrOfs2j/kwH4wo=
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.8 h1:RKk4Z+qMaLORUdT7zntwMqKiYAej1VQlCswg0S7xNSY=
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.8/go.mod h1:dMJdlBWKHMu2xf0wIKpbo7+QfG+RzVkBB3nHP8EMM5o=
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.9 h1:liWcl12dfFeQXU0JemQVgdVQx02Fls9UPdrFzVrCWhs=
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.9/go.mod h1:Ckx62auqPQvNJWRBAboY+/kHs77gy6L33b6UtB/FB5U=
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.8 h1:Zte3Oogkd8m+nu2oK3yHtGmN++TZWh2Lm6q2iSprT1M=
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.8/go.mod h1:j00crVw6ki4/WViXflw0zWgNALrAzZT+GbIK8v7Xlz4=
github.com/sigstore/timestamp-authority v1.2.2 h1:X4qyutnCQqJ0apMewFyx+3t7Tws00JQ/JonBiu3QvLE=
github.com/sigstore/timestamp-authority v1.2.2/go.mod h1:nEah4Eq4wpliDjlY342rXclGSO7Kb9hoRrl9tqLW13A=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
@@ -611,10 +615,10 @@ github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDd
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes=
github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/testcontainers/testcontainers-go v0.32.0 h1:ug1aK08L3gCHdhknlTTwWjPHPS+/alvLJU/DRxTD/ME=
github.com/testcontainers/testcontainers-go v0.32.0/go.mod h1:CRHrzHLQhlXUsa5gXjTOfqIEJcrK5+xMDmBr/WMI88E=
github.com/testcontainers/testcontainers-go/modules/registry v0.32.0 h1:b4JSSEhbGXGtQA1WXJ3BlbkVjjdXoFTtBPvLRe+9Y9Y=
github.com/testcontainers/testcontainers-go/modules/registry v0.32.0/go.mod h1:bX3JF8vQkv3D2frmrDyQd0GCQIQGl5nPG91xUvl7UhA=
github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw=
github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8=
github.com/testcontainers/testcontainers-go/modules/registry v0.33.0 h1:rpQS5KcFpyRPM3xVKERuXDqUcE5xjwE8MQUgmKVkL0o=
github.com/testcontainers/testcontainers-go/modules/registry v0.33.0/go.mod h1:qr3nJgBZ2ovQva6vadXchwi786/mBBDzhBPbrmWkYIE=
github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg=
github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU=
github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
@@ -656,28 +660,28 @@ go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUS
go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 h1:vS1Ao/R55RNV4O7TA2Qopok8yN+X0LIP6RVWLFkprck=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0/go.mod h1:BMsdeOxN04K0L5FNUBfjFdvwWGNe/rkmSwH4Aelu/X0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.step.sm/crypto v0.50.0 h1:BqI9sEgocoHDLLHiZnFqdqXl5FjdMvOWKMm/fKL/lrw=
go.step.sm/crypto v0.50.0/go.mod h1:NCFMhLS6FJXQ9sD9PP282oHtsBWLrI6wXZY0eOkq7t8=
go.step.sm/crypto v0.51.1 h1:ktUg/2hetEMiBAqgz502ktZDGoDoGrcHFg3XpkmkvvA=
go.step.sm/crypto v0.51.1/go.mod h1:PdrhttNU/tG9/YsVd4fdlysBN+UV503p0o2irFZQlAw=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -690,8 +694,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
@@ -702,8 +706,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -723,11 +727,11 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -735,8 +739,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -767,15 +771,15 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -783,10 +787,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -798,33 +802,33 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/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-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI=
google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8=
google.golang.org/api v0.196.0 h1:k/RafYqebaIJBO3+SMnfEGtFVlvp5vSgqTUF54UN/zg=
google.golang.org/api v0.196.0/go.mod h1:g9IL21uGkYgvQ5BZg6BAtoGJQIm8r6EgaAbpNey5wBE=
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=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240722135656-d784300faade h1:lKFsS7wpngDgSCeFn7MoLy+wBDQZ1UQIJD4UNM1Qvkg=
google.golang.org/genproto v0.0.0-20240722135656-d784300faade/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY=
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade h1:oCRSWfwGXQsqlVdErcyTt4A93Y8fo0/9D4b1gnI++qo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU=
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4=
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0=
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c=
google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -875,8 +879,8 @@ k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCI
k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/release-utils v0.8.3 h1:KtOtA4qDmzJyeQ2zkDsFVI25+NViwms/o5eL2NftFdA=
sigs.k8s.io/release-utils v0.8.3/go.mod h1:fp82Fma06OXBhEJ+GUJKqvcplDBomruK1R/1fWJnsrQ=
sigs.k8s.io/release-utils v0.8.4 h1:4QVr3UgbyY/d9p74LBhg0njSVQofUsAZqYOzVZBhdBw=
sigs.k8s.io/release-utils v0.8.4/go.mod h1:m1bHfscTemQp+z+pLCZnkXih9n0+WukIUU70n6nFnU0=
sigs.k8s.io/structured-merge-diff/v4 v4.3.0 h1:UZbZAZfX0wV2zr7YZorDz6GXROfDFj6LvqCRm4VUVKk=
sigs.k8s.io/structured-merge-diff/v4 v4.3.0/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=

View File

@@ -16,11 +16,13 @@ var prodRoot []byte
var defaultRoot = prodRoot
type RootName string
type EmbeddedRoot struct {
Data []byte
Name RootName
}
type (
RootName string
EmbeddedRoot struct {
Data []byte
Name RootName
}
)
var (
RootDev = EmbeddedRoot{Data: devRoot, Name: "dev"}

View File

@@ -1,64 +0,0 @@
package test
import (
"context"
"os"
"path/filepath"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/signerverifier"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/secure-systems-lab/go-securesystemslib/dsse"
)
type MockResolver struct {
Envs []*attestation.Envelope
}
func (r MockResolver) Attestations(ctx context.Context, mediaType string) ([]*attestation.Envelope, error) {
return r.Envs, nil
}
func (r MockResolver) ImageName(ctx context.Context) (string, error) {
return "library/alpine:latest", nil
}
func (r MockResolver) ImageDescriptor(ctx context.Context) (*v1.Descriptor, error) {
digest, err := v1.NewHash("sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620")
if err != nil {
return nil, err
}
return &v1.Descriptor{
Digest: digest,
Size: 1234,
MediaType: "application/vnd.oci.image.manifest.v1+json",
}, nil
}
func (r MockResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
return oci.ParsePlatform("linux/amd64")
}
type MockRegistryResolver struct {
Subject *v1.Descriptor
ImageNameStr string
*MockResolver
}
func (r *MockRegistryResolver) ImageDescriptor(ctx context.Context) (*v1.Descriptor, error) {
return r.Subject, nil
}
func (r *MockRegistryResolver) ImageName(ctx context.Context) (string, error) {
return r.ImageNameStr, nil
}
func GetMockSigner(ctx context.Context) (dsse.SignerVerifier, error) {
priv, err := os.ReadFile(filepath.Join("..", "..", "test", "testdata", "test-signing-key.pem"))
if err != nil {
return nil, err
}
return signerverifier.LoadKeyPair(priv)
}

View File

@@ -2,33 +2,29 @@ package test
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
_ "embed"
"os"
"strings"
"path/filepath"
"testing"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/policy"
"github.com/docker/attest/pkg/signerverifier"
"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/docker/attest/signerverifier"
"github.com/docker/attest/tlog"
"github.com/secure-systems-lab/go-securesystemslib/dsse"
)
const (
USE_MOCK_TL = true
USE_MOCK_KMS = true
USE_MOCK_POLICY = true
UseMockTL = true
UseMockKMS = true
AwsRegion = "us-east-1"
AwsKmsKeyArn = "arn:aws:kms:us-east-1:175142243308:alias/doi-signing" // sandbox
AWSRegion = "us-east-1"
AWSKMSKeyARN = "arn:aws:kms:us-east-1:175142243308:alias/doi-signing" // sandbox
)
func UnsignedTestImage(rel ...string) string {
rel = append(rel, "test", "testdata", "unsigned-test-image")
return filepath.Join(rel...)
}
func CreateTempDir(t *testing.T, dir, pattern string) string {
// Create a temporary directory for output oci layout
tempDir, err := os.MkdirTemp(dir, pattern)
@@ -45,9 +41,16 @@ func CreateTempDir(t *testing.T, dir, pattern string) string {
return tempDir
}
//go:embed test-signing-key.pem
var signingKey []byte
func GetMockSigner(_ context.Context) (dsse.SignerVerifier, error) {
return signerverifier.LoadKeyPair(signingKey)
}
func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) {
var tl tlog.TL
if USE_MOCK_TL {
if UseMockTL {
tl = tlog.GetMockTL()
} else {
tl = &tlog.RekorTL{}
@@ -55,24 +58,15 @@ func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) {
ctx := tlog.WithTL(context.Background(), tl)
var policyEvaluator policy.PolicyEvaluator
if USE_MOCK_POLICY {
policyEvaluator = policy.GetMockPolicy()
} else {
policyEvaluator = policy.NewRegoEvaluator(true)
}
ctx = policy.WithPolicyEvaluator(ctx, policyEvaluator)
var signer dsse.SignerVerifier
var err error
if USE_MOCK_KMS {
if UseMockKMS {
signer, err = GetMockSigner(ctx)
if err != nil {
t.Fatal(err)
}
} else {
signer, err = signerverifier.GetAWSSigner(ctx, AwsKmsKeyArn, AwsRegion)
signer, err = signerverifier.GetAWSSigner(ctx, AWSKMSKeyARN, AWSRegion)
if err != nil {
t.Fatal(err)
}
@@ -80,107 +74,3 @@ func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) {
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 _, mf := range mfs2.Manifests {
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()
var intotoStatement = new(intoto.Statement)
var desc *v1.Descriptor
if strings.HasSuffix(string(mt), "+dsse") {
var 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
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,20 +6,20 @@ import (
"path/filepath"
"strings"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/pkg/mirror"
"github.com/docker/attest/pkg/tuf"
"github.com/docker/attest/mirror"
"github.com/docker/attest/oci"
"github.com/docker/attest/tuf"
v1 "github.com/google/go-containerregistry/pkg/v1"
)
type TufMirrorOutput struct {
metadata v1.Image
delegatedMetadata []*mirror.MirrorImage
targets []*mirror.MirrorImage
delegatedTargets []*mirror.MirrorIndex
delegatedMetadata []*mirror.Image
targets []*mirror.Image
delegatedTargets []*mirror.Index
}
func ExampleNewTufMirror() {
func ExampleNewTUFMirror() {
home, err := os.UserHomeDir()
if err != nil {
panic(err)
@@ -29,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)
}
@@ -46,7 +46,7 @@ func ExampleNewTufMirror() {
}
// create targets manifest
targets, err := m.GetTufTargetMirrors()
targets, err := m.GetTUFTargetMirrors()
if err != nil {
panic(err)
}
@@ -80,7 +80,7 @@ func ExampleNewTufMirror() {
func mirrorToRegistry(o *TufMirrorOutput) error {
// push metadata to registry
metadataRepo := "registry-1.docker.io/docker/tuf-metadata:latest"
err := mirror.PushImageToRegistry(o.metadata, metadataRepo)
err := oci.PushImageToRegistry(o.metadata, metadataRepo)
if err != nil {
return err
}
@@ -91,7 +91,7 @@ func mirrorToRegistry(o *TufMirrorOutput) error {
return fmt.Errorf("failed to get repo without tag: %s", metadataRepo)
}
imageName := fmt.Sprintf("%s:%s", repo, metadata.Tag)
err = mirror.PushImageToRegistry(metadata.Image, imageName)
err = oci.PushImageToRegistry(metadata.Image, imageName)
if err != nil {
return err
}
@@ -101,7 +101,7 @@ func mirrorToRegistry(o *TufMirrorOutput) error {
targetsRepo := "registry-1.docker.io/docker/tuf-targets"
for _, target := range o.targets {
imageName := fmt.Sprintf("%s:%s", targetsRepo, target.Tag)
err = mirror.PushImageToRegistry(target.Image, imageName)
err = oci.PushImageToRegistry(target.Image, imageName)
if err != nil {
return err
}
@@ -109,7 +109,7 @@ func mirrorToRegistry(o *TufMirrorOutput) error {
// push delegated targets to registry
for _, target := range o.delegatedTargets {
imageName := fmt.Sprintf("%s:%s", targetsRepo, target.Tag)
err = mirror.PushIndexToRegistry(target.Index, imageName)
err = oci.PushIndexToRegistry(target.Index, imageName)
if err != nil {
return err
}
@@ -119,14 +119,14 @@ func mirrorToRegistry(o *TufMirrorOutput) error {
func mirrorToLocal(o *TufMirrorOutput, outputPath string) error {
// output metadata to local directory
err := mirror.SaveImageAsOCILayout(o.metadata, outputPath)
err := oci.SaveImageAsOCILayout(o.metadata, outputPath)
if err != nil {
return err
}
// output delegated metadata to local directory
for _, metadata := range o.delegatedMetadata {
path := filepath.Join(outputPath, metadata.Tag)
err = mirror.SaveImageAsOCILayout(metadata.Image, path)
err = oci.SaveImageAsOCILayout(metadata.Image, path)
if err != nil {
return err
}
@@ -135,7 +135,7 @@ func mirrorToLocal(o *TufMirrorOutput, outputPath string) error {
// output top-level targets to local directory
for _, target := range o.targets {
path := filepath.Join(outputPath, target.Tag)
err = mirror.SaveImageAsOCILayout(target.Image, path)
err = oci.SaveImageAsOCILayout(target.Image, path)
if err != nil {
return err
}
@@ -143,7 +143,7 @@ func mirrorToLocal(o *TufMirrorOutput, outputPath string) error {
// output delegated targets to local directory
for _, target := range o.delegatedTargets {
path := filepath.Join(outputPath, target.Tag)
err = mirror.SaveIndexAsOCILayout(target.Index, path)
err = oci.SaveIndexAsOCILayout(target.Index, path)
if err != nil {
return err
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"strconv"
"github.com/docker/attest/oci"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
@@ -16,9 +17,9 @@ import (
// TUF root metadata
// -----------------
// GetMetadataManifest returns an image with TUF root metadata as layers
func (m *TufMirror) GetMetadataManifest(metadataURL string) (v1.Image, error) {
metadata, err := m.getTufMetadataMirror(metadataURL)
// GetMetadataManifest returns an image with TUF root metadata as layers.
func (m *TUFMirror) GetMetadataManifest(metadataURL string) (*oci.EmptyConfigImage, error) {
metadata, err := m.getMetadataMirror(metadataURL)
if err != nil {
return nil, fmt.Errorf("failed to get metadata: %w", err)
}
@@ -26,19 +27,19 @@ func (m *TufMirror) GetMetadataManifest(metadataURL string) (v1.Image, error) {
if err != nil {
return nil, fmt.Errorf("failed to build metadata manifest: %w", err)
}
return manifest, nil
return &oci.EmptyConfigImage{Image: manifest}, nil
}
// getTufMetadataMirror returns a TufMetadata struct with TUF metadata as map of file names to bytes
func (m *TufMirror) getTufMetadataMirror(metadataURL string) (*TufMetadata, error) {
trustedMetadata := m.TufClient.GetMetadata()
// getMetadataMirror returns a TufMetadata struct with TUF metadata as map of file names to bytes.
func (m *TUFMirror) getMetadataMirror(metadataURL string) (*TUFMetadata, error) {
trustedMetadata := m.TUFClient.GetMetadata()
rootMetadata := map[string][]byte{}
rootVersion := trustedMetadata.Root.Signed.Version
// get the previous versions of root metadata if any
if rootVersion != 1 {
var err error
rootMetadata, err = m.TufClient.GetPriorRoots(metadataURL)
rootMetadata, err = m.TUFClient.GetPriorRoots(metadataURL)
if err != nil {
return nil, fmt.Errorf("failed to get prior root metadata: %w", err)
}
@@ -69,7 +70,7 @@ func (m *TufMirror) getTufMetadataMirror(metadataURL string) (*TufMetadata, erro
snapshotVersion = strconv.FormatInt(trustedMetadata.Snapshot.Signed.Version, 10)
targetsVersion = strconv.FormatInt(trustedMetadata.Targets[metadata.TARGETS].Signed.Version, 10)
}
return &TufMetadata{
return &TUFMetadata{
Root: rootMetadata,
Snapshot: map[string][]byte{nameFromRole(metadata.SNAPSHOT, snapshotVersion): snapshotBytes},
Targets: map[string][]byte{nameFromRole(metadata.TARGETS, targetsVersion): targetsBytes},
@@ -77,12 +78,12 @@ func (m *TufMirror) getTufMetadataMirror(metadataURL string) (*TufMetadata, erro
}, nil
}
// buildMetadataManifest returns an OCI image with TUF metadata as layers with annotations
func (m *TufMirror) buildMetadataManifest(metadata *TufMetadata) (v1.Image, error) {
// buildMetadataManifest returns an OCI image with TUF metadata as layers with annotations.
func (m *TUFMirror) buildMetadataManifest(metadata *TUFMetadata) (v1.Image, error) {
img := empty.Image
img = mutate.MediaType(img, types.OCIManifestSchema1)
img = mutate.ConfigMediaType(img, types.OCIConfigJSON)
for _, role := range TufRoles {
for _, role := range TUFRoles {
layers, err := m.makeRoleLayers(role, metadata)
if err != nil {
return nil, fmt.Errorf("failed to make role layer: %w", err)
@@ -95,8 +96,8 @@ func (m *TufMirror) buildMetadataManifest(metadata *TufMetadata) (v1.Image, erro
return img, nil
}
// makeRoleLayers returns a list of layers for a given TUF role
func (m *TufMirror) makeRoleLayers(role TufRole, tufMetadata *TufMetadata) ([]mutate.Addendum, error) {
// makeRoleLayers returns a list of layers for a given TUF role.
func (m *TUFMirror) makeRoleLayers(role TUFRole, tufMetadata *TUFMetadata) ([]mutate.Addendum, error) {
var layers []mutate.Addendum
ann := map[string]string{tufFileAnnotation: ""}
switch role {
@@ -115,8 +116,8 @@ func (m *TufMirror) makeRoleLayers(role TufRole, tufMetadata *TufMetadata) ([]mu
return layers, nil
}
// annotatedMetaLayers returns a list of layers with annotations for each TUF metadata file
func (m *TufMirror) annotatedMetaLayers(meta map[string][]byte) []mutate.Addendum {
// annotatedMetaLayers returns a list of layers with annotations for each TUF metadata file.
func (m *TUFMirror) annotatedMetaLayers(meta map[string][]byte) []mutate.Addendum {
var layers []mutate.Addendum
for name, data := range meta {
ann := map[string]string{tufFileAnnotation: name}
@@ -129,8 +130,8 @@ func (m *TufMirror) annotatedMetaLayers(meta map[string][]byte) []mutate.Addendu
// TUF delegated targets metadata
// ------------------------------
// GetDelegatedMetadataMirrors returns a list of mirrors (image/tag pairs) for each delegated targets role metadata
func (m *TufMirror) GetDelegatedMetadataMirrors() ([]*MirrorImage, error) {
// GetDelegatedMetadataMirrors returns a list of mirrors (image/tag pairs) for each delegated targets role metadata.
func (m *TUFMirror) GetDelegatedMetadataMirrors() ([]*Image, error) {
// get current delegated targets metadata
delegatedTargets, err := m.getDelegatedTargetsMetadata()
if err != nil {
@@ -143,12 +144,12 @@ func (m *TufMirror) GetDelegatedMetadataMirrors() ([]*MirrorImage, error) {
return mirror, nil
}
// getDelegatedTargetsMetadata returns delegated targets metadata as a list of DelegatedTargetMetadata (role name and data)
func (m *TufMirror) getDelegatedTargetsMetadata() ([]DelegatedTargetMetadata, error) {
// getDelegatedTargetsMetadata returns delegated targets metadata as a list of DelegatedTargetMetadata (role name and data).
func (m *TUFMirror) getDelegatedTargetsMetadata() ([]DelegatedTargetMetadata, error) {
var delegatedTargets []DelegatedTargetMetadata
md := m.TufClient.GetMetadata()
md := m.TUFClient.GetMetadata()
for _, role := range md.Targets[metadata.TARGETS].Signed.Delegations.Roles {
roleMetadata, err := m.TufClient.LoadDelegatedTargets(role.Name, metadata.TARGETS)
roleMetadata, err := m.TUFClient.LoadDelegatedTargets(role.Name, metadata.TARGETS)
if err != nil {
return nil, fmt.Errorf("failed to get delegated role metadata: %w", err)
}
@@ -170,9 +171,9 @@ func (m *TufMirror) getDelegatedTargetsMetadata() ([]DelegatedTargetMetadata, er
return delegatedTargets, nil
}
// buildDelegatedMetadataManifests returns a list of mirrors (image/tag pairs) for each delegated target role metadata
func (m *TufMirror) buildDelegatedMetadataManifests(delegated []DelegatedTargetMetadata) ([]*MirrorImage, error) {
manifests := []*MirrorImage{}
// buildDelegatedMetadataManifests returns a list of mirrors (image/tag pairs) for each delegated target role metadata.
func (m *TUFMirror) buildDelegatedMetadataManifests(delegated []DelegatedTargetMetadata) ([]*Image, error) {
manifests := []*Image{}
for _, role := range delegated {
img := empty.Image
img = mutate.MediaType(img, types.OCIManifestSchema1)
@@ -183,7 +184,7 @@ func (m *TufMirror) buildDelegatedMetadataManifests(delegated []DelegatedTargetM
if err != nil {
return nil, fmt.Errorf("failed to append delegated targets layer to image: %w", err)
}
manifests = append(manifests, &MirrorImage{Image: img, Tag: role.Name})
manifests = append(manifests, &Image{Image: &oci.EmptyConfigImage{Image: img}, Tag: role.Name})
}
return manifests, nil
}

View File

@@ -9,22 +9,26 @@ import (
"strings"
"testing"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/tuf"
"github.com/docker/attest/tuf"
"github.com/stretchr/testify/assert"
"github.com/theupdateframework/go-tuf/v2/metadata"
)
const (
metadataPath = "/metadata"
targetsPath = "/targets"
)
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()
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)
tufMetadata, err := m.getTufMetadataMirror(server.URL + "/metadata")
tufMetadata, err := m.getMetadataMirror(server.URL + metadataPath)
assert.NoError(t, err)
// check that all roles are not empty
@@ -35,14 +39,14 @@ func TestGetTufMetadataMirror(t *testing.T) {
}
func TestGetMetadataManifest(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()
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)
img, err := m.GetMetadataManifest(server.URL + "/metadata")
img, err := m.GetMetadataManifest(server.URL + metadataPath)
assert.NoError(t, err)
assert.NotNil(t, img)
@@ -74,11 +78,11 @@ func TestGetMetadataManifest(t *testing.T) {
}
func TestGetDelegatedMetadataMirrors(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()
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)
delegations, err := m.GetDelegatedMetadataMirrors()

18
mirror/mirror.go Normal file
View File

@@ -0,0 +1,18 @@
package mirror
import (
"fmt"
"github.com/docker/attest/tuf"
)
func NewTUFMirror(root []byte, tufPath, metadataURL, targetsURL string, versionChecker tuf.VersionChecker) (*TUFMirror, error) {
if root == nil {
root = tuf.DockerTUFRootDefault.Data
}
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)
}
return &TUFMirror{TUFClient: tufClient, tufPath: tufPath, metadataURL: metadataURL, targetsURL: targetsURL}, nil
}

View File

@@ -5,6 +5,7 @@ import (
"path/filepath"
"strings"
"github.com/docker/attest/oci"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
@@ -13,16 +14,16 @@ import (
"github.com/theupdateframework/go-tuf/v2/metadata"
)
// GetTufTargetMirrors returns a list of top-level target files as MirrorImages (image with tag)
func (m *TufMirror) GetTufTargetMirrors() ([]*MirrorImage, error) {
targetMirrors := []*MirrorImage{}
md := m.TufClient.GetMetadata()
// GetTUFTargetMirrors returns a list of top-level target files as MirrorImages (image with tag).
func (m *TUFMirror) GetTUFTargetMirrors() ([]*Image, error) {
targetMirrors := []*Image{}
md := m.TUFClient.GetMetadata()
// for each top-level target file, create an image with the target file as a layer
targets := md.Targets[metadata.TARGETS].Signed.Targets
for _, t := range targets {
// download target file
_, data, err := m.TufClient.DownloadTarget(t.Path, filepath.Join(m.tufPath, "download"))
file, err := m.TUFClient.DownloadTarget(t.Path, filepath.Join(m.tufPath, "download"))
if err != nil {
return nil, fmt.Errorf("failed to download target %s: %w", t.Path, err)
}
@@ -37,21 +38,21 @@ func (m *TufMirror) GetTufTargetMirrors() ([]*MirrorImage, error) {
}
name := hash.String() + "." + t.Path
ann := map[string]string{tufFileAnnotation: name}
layer := mutate.Addendum{Layer: static.NewLayer(data, tufTargetMediaType), Annotations: ann}
layer := mutate.Addendum{Layer: static.NewLayer(file.Data, tufTargetMediaType), Annotations: ann}
img, err = mutate.Append(img, layer)
if err != nil {
return nil, fmt.Errorf("failed to append role layer to image: %w", err)
}
targetMirrors = append(targetMirrors, &MirrorImage{Image: img, Tag: name})
targetMirrors = append(targetMirrors, &Image{Image: &oci.EmptyConfigImage{Image: img}, Tag: name})
}
return targetMirrors, nil
}
// GetDelegatedTargetMirrors returns a list of delegated target files as MirrorIndexes (image index with tag)
// each image in the index contains a delegated target file
func (m *TufMirror) GetDelegatedTargetMirrors() ([]*MirrorIndex, error) {
mirror := []*MirrorIndex{}
md := m.TufClient.GetMetadata()
// each image in the index contains a delegated target file.
func (m *TUFMirror) GetDelegatedTargetMirrors() ([]*Index, error) {
mirror := []*Index{}
md := m.TUFClient.GetMetadata()
// for each delegated role, create an image index with target files as images
roles := md.Targets[metadata.TARGETS].Signed.Delegations.Roles
@@ -60,7 +61,7 @@ func (m *TufMirror) GetDelegatedTargetMirrors() ([]*MirrorIndex, error) {
index := v1.ImageIndex(empty.Index)
// get delegated targets metadata for role
roleMeta, err := m.TufClient.LoadDelegatedTargets(role.Name, metadata.TARGETS)
roleMeta, err := m.TUFClient.LoadDelegatedTargets(role.Name, metadata.TARGETS)
if err != nil {
return nil, fmt.Errorf("failed to load delegated targets metadata: %w", err)
}
@@ -68,7 +69,7 @@ func (m *TufMirror) GetDelegatedTargetMirrors() ([]*MirrorIndex, error) {
// for each target file, create an image with the target file as a layer
for _, target := range roleMeta.Signed.Targets {
// download target file
_, data, err := m.TufClient.DownloadTarget(target.Path, filepath.Join(m.tufPath, "download"))
file, err := m.TUFClient.DownloadTarget(target.Path, filepath.Join(m.tufPath, "download"))
if err != nil {
return nil, fmt.Errorf("failed to download target %s: %w", target.Path, err)
}
@@ -88,14 +89,15 @@ func (m *TufMirror) GetDelegatedTargetMirrors() ([]*MirrorIndex, error) {
}
name := hash.String() + "." + filename
ann := map[string]string{tufFileAnnotation: name}
layer := mutate.Addendum{Layer: static.NewLayer(data, tufTargetMediaType), Annotations: ann}
layer := mutate.Addendum{Layer: static.NewLayer(file.Data, tufTargetMediaType), Annotations: ann}
img, err = mutate.Append(img, layer)
if err != nil {
return nil, fmt.Errorf("failed to append role layer to image: %w", err)
}
emptyConfigImage := &oci.EmptyConfigImage{Image: img}
// append image to index with annotation
index = mutate.AppendManifests(index, mutate.IndexAddendum{
Add: img,
Add: emptyConfigImage,
Descriptor: v1.Descriptor{
Annotations: map[string]string{
tufFileAnnotation: fmt.Sprintf("%s/%s", subdir, name),
@@ -103,7 +105,7 @@ func (m *TufMirror) GetDelegatedTargetMirrors() ([]*MirrorIndex, error) {
},
})
}
mirror = append(mirror, &MirrorIndex{Index: index, Tag: role.Name})
mirror = append(mirror, &Index{Index: index, Tag: role.Name})
}
return mirror, nil
}

View File

@@ -8,9 +8,8 @@ import (
"strings"
"testing"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/tuf"
"github.com/docker/attest/tuf"
"github.com/stretchr/testify/assert"
)
@@ -23,14 +22,14 @@ type Layers struct {
}
func TestGetTufTargetsMirror(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()
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)
targets, err := m.GetTufTargetMirrors()
targets, err := m.GetTUFTargetMirrors()
assert.NoError(t, err)
assert.Greater(t, len(targets), 0)
@@ -57,24 +56,24 @@ func TestGetTufTargetsMirror(t *testing.T) {
}
func TestTargetDelegationMetadata(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()
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)
targets, err := tm.TufClient.LoadDelegatedTargets("test-role", "targets")
targets, err := tm.TUFClient.LoadDelegatedTargets("test-role", "targets")
assert.NoError(t, err)
assert.Greater(t, len(targets.Signed.Targets), 0)
}
func TestGetDelegatedTargetMirrors(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()
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)
mirrors, err := m.GetDelegatedTargetMirrors()

View File

@@ -1,7 +1,8 @@
package mirror
import (
"github.com/docker/attest/pkg/tuf"
"github.com/docker/attest/oci"
"github.com/docker/attest/tuf"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/theupdateframework/go-tuf/v2/metadata"
)
@@ -14,11 +15,11 @@ const (
tufFileAnnotation = "tuf.io/filename"
)
type TufRole string
type TUFRole string
var TufRoles = []TufRole{metadata.ROOT, metadata.SNAPSHOT, metadata.TARGETS, metadata.TIMESTAMP}
var TUFRoles = []TUFRole{metadata.ROOT, metadata.SNAPSHOT, metadata.TARGETS, metadata.TIMESTAMP}
type TufMetadata struct {
type TUFMetadata struct {
Root map[string][]byte
Snapshot map[string][]byte
Targets map[string][]byte
@@ -31,18 +32,18 @@ type DelegatedTargetMetadata struct {
Data []byte
}
type MirrorImage struct {
Image v1.Image
type Image struct {
Image *oci.EmptyConfigImage
Tag string
}
type MirrorIndex struct {
type Index struct {
Index v1.ImageIndex
Tag string
}
type TufMirror struct {
TufClient *tuf.TufClient
type TUFMirror struct {
TUFClient *tuf.Client
tufPath string
metadataURL string
targetsURL string

2
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,20 +1,17 @@
//go:build e2e
package mirror_test
package oci_test
import (
"path/filepath"
"testing"
"github.com/docker/attest/pkg/mirror"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/oci"
"github.com/stretchr/testify/require"
)
func TestRegistryAuth(t *testing.T) {
UnsignedTestImage := filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage(".."))
require.NoError(t, err)
// test cases for ecr, gcr and dockerhub
testCases := []struct {
@@ -25,7 +22,7 @@ func TestRegistryAuth(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.Image, func(t *testing.T) {
err := mirror.PushIndexToRegistry(attIdx.Index, tc.Image)
err := oci.PushIndexToRegistry(attIdx.Index, tc.Image)
require.NoError(t, err)
_, err = oci.IndexFromRemote(tc.Image)
require.NoError(t, err)

View File

@@ -7,21 +7,21 @@ import (
)
type userAgentTransporter struct {
ua string
rt http.RoundTripper
userAgent string
roundTripper http.RoundTripper
}
type Option = func(*http.Client)
func (u *userAgentTransporter) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", u.ua)
req.Header.Set("User-Agent", u.userAgent)
return u.rt.RoundTrip(req)
return u.roundTripper.RoundTrip(req)
}
func HttpTransport() http.RoundTripper {
func HTTPTransport() http.RoundTripper {
return &userAgentTransporter{
ua: "Docker-Client",
rt: cleanhttp.DefaultTransport(),
userAgent: "Docker-Client",
roundTripper: cleanhttp.DefaultTransport(),
}
}

View File

@@ -2,14 +2,11 @@ package oci
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/containerd/platforms"
"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"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
@@ -18,7 +15,7 @@ import (
)
// ParsePlatform parses the provided platform string or attempts to obtain
// the platform of the current host system
// the platform of the current host system.
func ParsePlatform(platformStr string) (*v1.Platform, error) {
if platformStr == "" {
cdp := platforms.Normalize(platforms.DefaultSpec())
@@ -30,14 +27,13 @@ func ParsePlatform(platformStr string) (*v1.Platform, error) {
Architecture: cdp.Architecture,
Variant: cdp.Variant,
}, nil
} else {
return v1.ParsePlatform(platformStr)
}
return v1.ParsePlatform(platformStr)
}
func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
// prepare options
options := []remote.Option{MultiKeychainOption(), remote.WithTransport(HttpTransport()), remote.WithContext(ctx)}
options := []remote.Option{MultiKeychainOption(), remote.WithTransport(HTTPTransport()), remote.WithContext(ctx)}
// add in platform into remote Get operation; this might conflict with an explicit digest, but we are trying anyway
if platform != nil {
@@ -46,61 +42,18 @@ func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
return options
}
func ExtractEnvelopes(manifest *attestation.AttestationManifest, predicateType string) ([]*att.Envelope, 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()
var 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 _, m := range ix.Manifests {
func ImageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descriptor, error) {
for i := range ix.Manifests {
m := &ix.Manifests[i]
if (m.MediaType == ocispec.MediaTypeImageManifest || m.MediaType == "application/vnd.docker.distribution.manifest.v2+json") && m.Platform.Equals(*platform) {
return &m, nil
return m, nil
}
}
return nil, fmt.Errorf("no image found for platform %v", platform)
}
func attestationDigestForDigest(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) {
for _, m := range ix.Manifests {
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) {
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 {
@@ -149,7 +102,7 @@ func SplitDigest(digest string) (common.DigestSet, 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 {
return nil, fmt.Errorf("failed to parse repo name: %w", err)
}
@@ -160,8 +113,8 @@ func ReplaceTagInSpec(src *ImageSpec, digest v1.Hash) (*ImageSpec, error) {
}, nil
}
// 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) {
// 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) {
if strings.HasPrefix(image, LocalPrefix) {
return image, nil
}
@@ -171,3 +124,26 @@ func replaceTag(image string, digest v1.Hash) (string, error) {
}
return fmt.Sprintf("%s:%s-%s.att", notag, digest.Algorithm, digest.Hex), nil
}
func ReplaceDigestInSpec(src *ImageSpec, digest v1.Hash) (*ImageSpec, error) {
newName, err := replaceDigest(src.Identifier, digest)
if err != nil {
return nil, fmt.Errorf("failed to parse repo name: %w", err)
}
return &ImageSpec{
Identifier: newName,
Type: src.Type,
Platform: src.Platform,
}, nil
}
func replaceDigest(image string, digest v1.Hash) (string, error) {
if strings.HasPrefix(image, LocalPrefix) {
return image, nil
}
notag, err := WithoutTag(image)
if err != nil {
return "", nil
}
return fmt.Sprintf("%s@%s:%s", notag, digest.Algorithm, digest.Hex), nil
}

View File

@@ -1,9 +1,11 @@
package oci
package oci_test
import (
"path/filepath"
"testing"
"github.com/distribution/reference"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/oci"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/stretchr/testify/assert"
@@ -11,56 +13,61 @@ import (
)
func TestRefToPurl(t *testing.T) {
arm, err := ParsePlatform("arm64/linux")
arm, err := oci.ParsePlatform("arm64/linux")
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.Equal(t, "pkg:docker/alpine@latest?platform=arm64%2Flinux", purl)
assert.False(t, canonical)
purl, canonical, err = 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 = 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 = 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 = 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 = 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 = 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 = 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)
}
var (
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
)
// Test fix for https://github.com/docker/secure-artifacts-team-issues/issues/202
func TestImageDigestForPlatform(t *testing.T) {
idx, err := layout.ImageIndexFromPath(UnsignedTestImage)
idx, err := layout.ImageIndexFromPath(test.UnsignedTestImage(".."))
assert.NoError(t, err)
idxm, err := idx.IndexManifest()
@@ -74,16 +81,16 @@ func TestImageDigestForPlatform(t *testing.T) {
mfs2, err := mfs.IndexManifest()
assert.NoError(t, err)
p, err := ParsePlatform("linux/amd64")
p, err := oci.ParsePlatform("linux/amd64")
assert.NoError(t, err)
desc, err := imageDescriptor(mfs2, p)
desc, err := oci.ImageDescriptor(mfs2, p)
assert.NoError(t, err)
digest := desc.Digest.String()
assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", digest)
p, err = ParsePlatform("linux/arm64")
p, err = oci.ParsePlatform("linux/arm64")
assert.NoError(t, err)
desc, err = imageDescriptor(mfs2, p)
desc, err = oci.ImageDescriptor(mfs2, p)
assert.NoError(t, err)
digest = desc.Digest.String()
assert.Equal(t, "sha256:7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e", digest)
@@ -97,14 +104,14 @@ func TestWithoutTag(t *testing.T) {
{name: "image:tag", 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: "docker://image:tag", expected: "docker://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: "docker://image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "docker://index.docker.io/library/image"},
{name: "docker://127.0.0.1:36555/repo:latest", expected: "docker://127.0.0.1:36555/repo"},
{name: oci.RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: oci.RegistryPrefix + "index.docker.io/library/image"},
{name: oci.RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: oci.RegistryPrefix + "127.0.0.1:36555/repo"},
}
for _, c := range tc {
t.Run(c.name, func(t *testing.T) {
notag, _ := WithoutTag(c.name)
notag, _ := oci.WithoutTag(c.name)
assert.Equal(t, c.expected, notag)
})
}
@@ -118,11 +125,11 @@ func TestReplaceTag(t *testing.T) {
{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:sha256-digest.att", expected: "index.docker.io/library/image:sha256-digest.att"},
{name: "docker://image:tag", expected: "docker://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: "oci://foobar", expected: "oci://foobar"},
{name: "docker://image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "docker://index.docker.io/library/image:sha256-digest.att"},
{name: "docker://127.0.0.1:36555/repo:latest", expected: "docker://127.0.0.1:36555/repo:sha256-digest.att"},
{name: oci.LocalPrefix + "foobar", expected: oci.LocalPrefix + "foobar"},
{name: oci.RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: oci.RegistryPrefix + "index.docker.io/library/image: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{
@@ -131,7 +138,7 @@ func TestReplaceTag(t *testing.T) {
}
for _, c := range tc {
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)
assert.Equal(t, c.expected, replaced)
})

View File

@@ -1,13 +1,9 @@
package mirror
package oci
import (
"fmt"
"os"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/tuf"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
@@ -16,17 +12,7 @@ import (
"github.com/google/go-containerregistry/pkg/v1/remote"
)
func NewTufMirror(root []byte, tufPath, metadataURL, targetsURL string, versionChecker tuf.VersionChecker) (*TufMirror, error) {
if root == nil {
root = embed.RootDefault.Data
}
tufClient, err := tuf.NewTufClient(root, tufPath, metadataURL, targetsURL, versionChecker)
if err != nil {
return nil, fmt.Errorf("failed to create TUF client: %w", err)
}
return &TufMirror{TufClient: tufClient, tufPath: tufPath, metadataURL: metadataURL, targetsURL: targetsURL}, nil
}
// PushImageToRegistry pushes an image to the registry with the specified name.
func PushImageToRegistry(image v1.Image, imageName string) error {
ref, err := name.ParseReference(imageName)
if err != nil {
@@ -34,9 +20,10 @@ func PushImageToRegistry(image v1.Image, imageName string) error {
}
// Push the image to the registry
return remote.Write(ref, image, oci.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 {
// Parse the index name
ref, err := name.ParseReference(imageName)
@@ -45,9 +32,10 @@ func PushIndexToRegistry(index v1.ImageIndex, imageName string) error {
}
// Push the index to the registry
return remote.WriteIndex(ref, index, oci.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 {
// Save the image to the local filesystem
err := os.MkdirAll(path, os.ModePerm)
@@ -62,6 +50,7 @@ func SaveImageAsOCILayout(image v1.Image, path string) error {
return l.AppendImage(image)
}
// SaveIndexAsOCILayout saves an index as an OCI layout to the specified path.
func SaveIndexAsOCILayout(image v1.ImageIndex, path string) error {
// Save the index to the local filesystem
err := os.MkdirAll(path, os.ModePerm)
@@ -76,16 +65,17 @@ func SaveIndexAsOCILayout(image v1.ImageIndex, path string) error {
return nil
}
func SaveIndex(outputs []*oci.ImageSpec, index v1.ImageIndex, indexName string) error {
// SaveIndex saves an index to the specified outputs.
func SaveIndex(outputs []*ImageSpec, index v1.ImageIndex, indexName string) error {
// split output by comma and write or push each one
for _, output := range outputs {
if output.Type == oci.OCI {
if output.Type == OCI {
idx := v1.ImageIndex(empty.Index)
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
Add: index,
Descriptor: v1.Descriptor{
Annotations: map[string]string{
oci.OciReferenceTarget: indexName,
OCIReferenceTarget: indexName,
},
},
})
@@ -103,14 +93,15 @@ func SaveIndex(outputs []*oci.ImageSpec, index v1.ImageIndex, indexName string)
return nil
}
func SaveImage(output *oci.ImageSpec, image v1.Image, imageName string) error {
if output.Type == oci.OCI {
// SaveImage saves an image to the specified output.
func SaveImage(output *ImageSpec, image v1.Image, imageName string) error {
if output.Type == OCI {
idx := v1.ImageIndex(empty.Index)
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
Add: image,
Descriptor: v1.Descriptor{
Annotations: map[string]string{
oci.OciReferenceTarget: imageName,
OCIReferenceTarget: imageName,
},
},
})
@@ -127,30 +118,26 @@ func SaveImage(output *oci.ImageSpec, image v1.Image, imageName string) error {
return nil
}
func SaveReferrers(manifest *attestation.AttestationManifest, outputs []*oci.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 {
if output.Type == oci.OCI {
// OCI layout output not supported
if output.Type == OCI {
continue
}
// so that we use the same tag each time to reduce number of tags (tags aren't needed for referrers but we must push one)
attOut, err := oci.ReplaceTagInSpec(output, manifest.SubjectDescriptor.Digest)
if err != nil {
return err
}
//otherwise we end up with the detected platform, though I'm not sure it matters
attOut.Platform = &v1.Platform{
OS: "unknown",
Architecture: "unknown",
}
images, err := manifest.BuildReferringArtifacts()
if err != nil {
return fmt.Errorf("failed to build image: %w", err)
}
for _, image := range images {
err = SaveImage(attOut, image, "")
}
if err != nil {
return fmt.Errorf("failed to push image: %w", err)
digest, err := image.Digest()
if err != nil {
return fmt.Errorf("failed to get image digest: %w", err)
}
spec, err := ReplaceDigestInSpec(output, digest)
if err != nil {
return fmt.Errorf("failed to create image spec: %w", err)
}
err = PushImageToRegistry(image, spec.Identifier)
if err != nil {
return fmt.Errorf("failed to push image: %w", err)
}
}
}
return nil

View File

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

64
oci/registry.go Normal file
View File

@@ -0,0 +1,64 @@
package oci
import (
"context"
"fmt"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
)
// ensure RegistryImageDetailsResolver implements ImageDetailsResolver.
var _ ImageDetailsResolver = &RegistryImageDetailsResolver{}
type RegistryImageDetailsResolver struct {
*ImageSpec
descriptor *v1.Descriptor
}
func NewRegistryImageDetailsResolver(src *ImageSpec) (*RegistryImageDetailsResolver, error) {
return &RegistryImageDetailsResolver{
ImageSpec: src,
}, nil
}
func (r *RegistryImageDetailsResolver) ImageName(_ context.Context) (string, error) {
return r.Identifier, nil
}
func (r *RegistryImageDetailsResolver) ImagePlatform(_ context.Context) (*v1.Platform, error) {
return r.Platform, nil
}
func (r *RegistryImageDetailsResolver) ImageDescriptor(ctx context.Context) (*v1.Descriptor, error) {
if r.descriptor == nil {
subjectRef, err := name.ParseReference(r.Identifier)
if err != nil {
return nil, fmt.Errorf("failed to parse reference: %w", err)
}
options := WithOptions(ctx, r.Platform)
image, err := remote.Image(subjectRef, options...)
if err != nil {
return nil, fmt.Errorf("failed to get image manifest: %w", err)
}
digest, err := image.Digest()
if err != nil {
return nil, fmt.Errorf("failed to get image digest: %w", err)
}
size, err := image.Size()
if err != nil {
return nil, fmt.Errorf("failed to get image size: %w", err)
}
mediaType, err := image.MediaType()
if err != nil {
return nil, fmt.Errorf("failed to get image media type: %w", err)
}
r.descriptor = &v1.Descriptor{
Digest: digest,
Size: size,
MediaType: mediaType,
}
}
return r.descriptor, nil
}

View File

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

View File

@@ -1,6 +1,8 @@
package oci
import (
"bytes"
"encoding/json"
"fmt"
"strings"
@@ -11,24 +13,20 @@ import (
)
const (
OciReferenceTarget = "org.opencontainers.image.ref.name"
OCIReferenceTarget = "org.opencontainers.image.ref.name"
LocalPrefix = "oci://"
RegistryPrefix = "docker://"
OCI SourceType = "OCI"
Docker SourceType = "Docker"
)
type SourceType string
type NamedIndex struct {
Index v1.ImageIndex
Name string
}
type AttestationOptions struct {
NoReferrers bool
Attach bool
ReferrersRepo string
}
type (
SourceType string
NamedIndex struct {
Index v1.ImageIndex
Name string
}
)
type ImageSpecOption func(*ImageSpec) error
@@ -50,7 +48,7 @@ func IndexFromPath(path string) (*NamedIndex, error) {
if err != nil {
return nil, fmt.Errorf("failed to get digest: %w", err)
}
imageName := idxm.Manifests[0].Annotations[OciReferenceTarget]
imageName := idxm.Manifests[0].Annotations[OCIReferenceTarget]
idxDigest := idxm.Manifests[0].Digest
idx, err := wrapperIdx.ImageIndex(idxDigest)
@@ -83,9 +81,8 @@ func IndexFromRemote(image string) (*NamedIndex, error) {
func LoadIndex(input *ImageSpec) (*NamedIndex, error) {
if input.Type == OCI {
return IndexFromPath(input.Identifier)
} else {
return IndexFromRemote(input.Identifier)
}
return IndexFromRemote(input.Identifier)
}
func (i *ImageSpec) ForPlatforms(platform string) ([]*ImageSpec, error) {
@@ -179,3 +176,42 @@ func WithoutTag(image string) (string, error) {
repo := ref.Context().Name()
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
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)
}

View File

@@ -1,68 +0,0 @@
package attest_test
import (
"context"
"fmt"
"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.TufClient, 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.NewTufClient(embed.RootStaging.Data, outputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker())
}
func ExampleVerify_remote() {
// create a tuf client
home, err := os.UserHomeDir()
if err != nil {
panic(err)
}
tufOutputPath := filepath.Join(home, ".docker", "tuf")
tufClient, err := createTufClient(tufOutputPath)
if err != nil {
panic(err)
}
// create a resolver for remote attestations
image := "registry-1.docker.io/library/notary:server"
platform := "linux/amd64"
// configure policy options
opts := &policy.PolicyOptions{
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
}
src, err := oci.ParseImageSpec(image, oci.WithPlatform(platform))
if err != nil {
panic(err)
}
// verify attestations
result, err := attest.Verify(context.Background(), src, opts)
if err != nil {
panic(err)
}
switch result.Outcome {
case attest.OutcomeSuccess:
fmt.Println("policy passed")
case attest.OutcomeNoPolicy:
fmt.Println("no policy for image")
case attest.OutcomeFailure:
fmt.Println("policy failed")
}
}

View File

@@ -1,246 +0,0 @@
package attest
import (
"encoding/json"
"fmt"
"net/http/httptest"
"net/url"
"path/filepath"
"testing"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/mirror"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
"github.com/google/go-containerregistry/pkg/registry"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"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"
v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
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")
TestTempDir = "attest-sign-test"
)
func TestSignVerifyOCILayout(t *testing.T) {
ctx, signer := test.Setup(t)
testCases := []struct {
name string
TestImage string
expectedStatements int
expectedAttestations int
replace bool
}{
{"signed replaced", UnsignedTestImage, 0, 4, true},
{"without replace", UnsignedTestImage, 4, 4, false},
// image without provenance doesn't fail
{"no provenance (replace)", NoProvenanceImage, 0, 2, true},
{"no provenance (no replace)", NoProvenanceImage, 2, 2, false},
}
policyOpts := &policy.PolicyOptions{
LocalPolicyDir: PassPolicyDir,
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
outputLayout := test.CreateTempDir(t, "", TestTempDir)
opts := &attestation.SigningOptions{}
attIdx, err := oci.IndexFromPath(tc.TestImage)
require.NoError(t, err)
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
signedIndex := attIdx.Index
signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests, attestation.WithReplacedLayers(tc.replace))
require.NoError(t, err)
// output signed attestations
idx := v1.ImageIndex(empty.Index)
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
Add: signedIndex,
Descriptor: v1.Descriptor{
Annotations: map[string]string{
oci.OciReferenceTarget: attIdx.Name,
},
},
})
_, err = layout.Write(outputLayout, idx)
require.NoError(t, err)
src, err := oci.ParseImageSpec("oci://" + outputLayout)
require.NoError(t, err)
policy, err := Verify(ctx, src, policyOpts)
require.NoError(t, err)
assert.Equalf(t, OutcomeSuccess, policy.Outcome, "Policy should have been found")
var allEnvelopes []*test.AnnotatedStatement
for _, predicate := range []string{intoto.PredicateSPDX, v02.PredicateSLSAProvenance, attestation.VSAPredicateType} {
mt, _ := attestation.DSSEMediaType(predicate)
statements, err := test.ExtractAnnotatedStatements(outputLayout, mt)
require.NoError(t, err)
allEnvelopes = append(allEnvelopes, statements...)
for _, stmt := range statements {
assert.Equalf(t, predicate, stmt.Annotations[attestation.InTotoPredicateType], "expected predicate-type annotation to be set to %s, got %s", predicate, stmt.Annotations[attestation.InTotoPredicateType])
assert.Equalf(t, attestation.LifecycleStageExperimental, stmt.Annotations[attestation.InTotoReferenceLifecycleStage], "expected reference lifecycle stage annotation to be set to %s, got %s", attestation.LifecycleStageExperimental, stmt.Annotations[attestation.InTotoReferenceLifecycleStage])
}
}
assert.Equalf(t, tc.expectedAttestations, len(allEnvelopes), "expected %d attestations, got %d", tc.expectedAttestations, len(allEnvelopes))
statements, err := test.ExtractAnnotatedStatements(outputLayout, intoto.PayloadType)
require.NoError(t, err)
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.AttestationLayer{
Layer: testLayer,
Statement: &intoto.Statement{
StatementHeader: intoto.StatementHeader{
PredicateType: attestation.VSAPredicateType,
},
},
Annotations: map[string]string{"test": "test"},
}
manifest := &attestation.AttestationManifest{
OriginalDescriptor: &v1.Descriptor{
MediaType: mediaType,
},
OriginalLayers: []*attestation.AttestationLayer{
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 = mirror.SaveReferrers(manifest, output)
require.NoError(t, err)
})
}
}

View File

@@ -1,176 +0,0 @@
package attest
import (
"context"
"fmt"
"strings"
"time"
"github.com/distribution/reference"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
v1 "github.com/google/go-containerregistry/pkg/v1"
intoto "github.com/in-toto/in-toto-golang/in_toto"
)
func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.PolicyOptions) (result *VerificationResult, err error) {
// so that we can resolve mapping from the image name earlier
detailsResolver, err := policy.CreateImageDetailsResolver(src)
if err != nil {
return nil, fmt.Errorf("failed to create image details resolver: %w", err)
}
if opts.AttestationStyle == "" {
opts.AttestationStyle = config.AttestationStyleReferrers
}
if opts.ReferrersRepo != "" && opts.AttestationStyle != config.AttestationStyleReferrers {
return nil, fmt.Errorf("referrers repo specified but attestation source not set to referrers")
}
pctx, err := policy.ResolvePolicy(ctx, detailsResolver, opts)
if err != nil {
return nil, fmt.Errorf("failed to resolve policy: %w", err)
}
if pctx == nil {
return &VerificationResult{
Outcome: OutcomeNoPolicy,
}, nil
}
// this is overriding the mapping with a referrers config. Useful for testing if nothing else
if opts.ReferrersRepo != "" {
pctx.Mapping.Attestations = &config.AttestationConfig{
Repo: opts.ReferrersRepo,
Style: config.AttestationStyleReferrers,
}
} else if opts.AttestationStyle == config.AttestationStyleAttached {
pctx.Mapping.Attestations = &config.AttestationConfig{
Repo: opts.ReferrersRepo,
Style: config.AttestationStyleAttached,
}
}
// because we have a mapping now, we can select a resolver based on its contents (ie. referrers or attached)
resolver, err := policy.CreateAttestationResolver(detailsResolver, pctx.Mapping)
if err != nil {
return nil, fmt.Errorf("failed to create attestation resolver: %w", err)
}
result, err = VerifyAttestations(ctx, resolver, pctx)
if err != nil {
return nil, fmt.Errorf("failed to evaluate policy: %w", err)
}
return result, nil
}
func toVerificationResult(p *policy.Policy, input *policy.PolicyInput, result *policy.Result) (*VerificationResult, error) {
dgst, err := oci.SplitDigest(input.Digest)
if err != nil {
return nil, fmt.Errorf("failed to split digest: %w", err)
}
subject := intoto.Subject{
Name: input.Purl,
Digest: dgst,
}
resourceUri, err := attestation.ToVSAResourceURI(subject)
if err != nil {
return nil, fmt.Errorf("failed to create resource uri: %w", err)
}
var outcome Outcome
if result.Success {
outcome = OutcomeSuccess
} else {
outcome = OutcomeFailure
}
outcomeStr, err := outcome.StringForVSA()
if err != nil {
return nil, err
}
return &VerificationResult{
Policy: p,
Outcome: outcome,
Violations: result.Violations,
Input: input,
VSA: &intoto.Statement{
StatementHeader: intoto.StatementHeader{
PredicateType: attestation.VSAPredicateType,
Type: intoto.StatementInTotoV01,
Subject: result.Summary.Subjects,
},
Predicate: attestation.VSAPredicate{
Verifier: attestation.VSAVerifier{
ID: result.Summary.Verifier,
},
TimeVerified: time.Now().UTC().Format(time.RFC3339),
ResourceUri: resourceUri,
Policy: attestation.VSAPolicy{URI: result.Summary.PolicyURI},
VerificationResult: outcomeStr,
VerifiedLevels: result.Summary.SLSALevels,
},
},
}, nil
}
func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, pctx *policy.Policy) (*VerificationResult, error) {
desc, err := resolver.ImageDescriptor(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get image descriptor: %w", err)
}
digest := desc.Digest.String()
name, err := resolver.ImageName(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get image name: %w", err)
}
platform, err := resolver.ImagePlatform(ctx)
if err != nil {
return nil, err
}
if pctx.ResolvedName != "" {
// this means the name we have is not the one we want to use for policy evaluation
// so we need to replace it with the one we resolved during policy resolution.
// this can happen if the name is an alias for another image, e.g. if it is a mirror
ref, err := reference.ParseNormalizedNamed(name)
if err != nil {
return nil, fmt.Errorf("failed to parse image name: %w", err)
}
oldName := ref.Name()
name = strings.Replace(name, oldName, pctx.ResolvedName, 1)
}
purl, canonical, err := oci.RefToPURL(name, platform)
if err != nil {
return nil, fmt.Errorf("failed to convert ref to purl: %w", err)
}
input := &policy.PolicyInput{
Digest: digest,
Purl: purl,
IsCanonical: canonical,
}
evaluator, err := policy.GetPolicyEvaluator(ctx)
if err != nil {
return nil, err
}
result, err := evaluator.Evaluate(ctx, resolver, pctx, input)
if err != nil {
return nil, fmt.Errorf("policy evaluation failed: %w", err)
}
verificationResult, err := toVerificationResult(pctx, input, result)
if err != nil {
return nil, fmt.Errorf("failed to convert to policy result: %w", err)
}
verificationResult.SubjectDescriptor = desc
return verificationResult, nil
}
func NewAttestationManifest(subject *v1.Descriptor) (*attestation.AttestationManifest, error) {
return &attestation.AttestationManifest{
OriginalDescriptor: &v1.Descriptor{
MediaType: "application/vnd.oci.image.manifest.v1+json",
},
OriginalLayers: []*attestation.AttestationLayer{},
SubjectDescriptor: subject,
}, nil
}

View File

@@ -1,151 +0,0 @@
package attestation_test
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/signerverifier"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSignVerifyAttestation(t *testing.T) {
ctx, signer := test.Setup(t)
stmt := &intoto.Statement{
StatementHeader: intoto.StatementHeader{
Type: intoto.StatementInTotoV01,
PredicateType: intoto.PredicateSPDX,
},
Predicate: "test",
}
payload, err := json.Marshal(stmt)
require.NoError(t, err)
opts := &attestation.SigningOptions{}
env, err := attestation.SignDSSE(ctx, payload, signer, opts)
require.NoError(t, err)
// marshal envelope to json to test for bugs when marshaling envelope data
serializedEnv, err := json.Marshal(env)
require.NoError(t, err)
deserializedEnv := new(attestation.Envelope)
err = json.Unmarshal(serializedEnv, deserializedEnv)
require.NoError(t, err)
// signer.Public() calls AWS API when using AWS signer, use attestation.GetPublicVerificationKey() to get key from TUF repo
// signer.Public() used here for test purposes
ecPub, ok := signer.Public().(*ecdsa.PublicKey)
assert.True(t, ok)
pem, err := signerverifier.ToPEM(ecPub)
assert.NoError(t, err)
keyId, err := signerverifier.KeyID(ecPub)
assert.NoError(t, err)
badKeyPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
badKey := &badKeyPriv.PublicKey
badPEM, err := signerverifier.ToPEM(badKey)
require.NoError(t, err)
testCases := []struct {
name string
keyId string
pem []byte
distrust bool
from time.Time
to *time.Time
status string
expectedError string
}{
{
name: "all OK",
keyId: keyId,
pem: pem,
distrust: false,
from: time.Time{},
to: nil,
status: "active",
expectedError: "",
},
{
name: "key not found",
keyId: "someotherkey",
pem: pem,
distrust: false,
from: time.Time{},
to: nil,
status: "active",
expectedError: fmt.Sprintf("key not found: %s", keyId),
},
{
name: "key distrusted",
keyId: keyId,
pem: pem,
distrust: true,
from: time.Time{},
to: nil,
status: "active",
expectedError: "distrusted",
},
{
name: "key not yet valid",
keyId: keyId,
pem: pem,
distrust: false,
from: time.Now().Add(time.Hour),
to: nil,
status: "active",
expectedError: "not yet valid",
},
{
name: "key already revoked",
keyId: keyId,
pem: pem,
distrust: false,
from: time.Time{},
to: new(time.Time),
status: "revoked",
expectedError: "already revoked",
},
{
name: "bad key",
keyId: keyId,
pem: badPEM,
distrust: false,
from: time.Time{},
to: nil,
status: "active",
expectedError: "signature is not valid",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
keyMeta := attestation.KeyMetadata{
ID: tc.keyId,
PEM: string(tc.pem),
Distrust: tc.distrust,
From: tc.from,
To: tc.to,
Status: tc.status,
}
opts := &attestation.VerifyOptions{
Keys: attestation.Keys{keyMeta},
}
_, err = attestation.VerifyDSSE(ctx, deserializedEnv, opts)
if tc.expectedError != "" {
assert.Contains(t, err.Error(), tc.expectedError)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@@ -1,77 +0,0 @@
package config
import (
"fmt"
"os"
"path/filepath"
"regexp"
"github.com/docker/attest/pkg/tuf"
"sigs.k8s.io/yaml"
)
const (
MappingFilename = "mapping.yaml"
)
func LoadLocalMappings(configDir string) (*PolicyMappings, error) {
if configDir == "" {
return nil, nil
}
mappings := &policyMappingsFile{}
path := filepath.Join(configDir, MappingFilename)
mappingFile, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read local policy mapping file %s: %w", path, err)
}
err = yaml.Unmarshal(mappingFile, mappings)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", path, err)
}
return expandMappingFile(mappings)
}
func LoadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMappings, error) {
if tufClient == nil {
return nil, fmt.Errorf("tuf client not set")
}
filename := MappingFilename
_, fileContents, err := tufClient.DownloadTarget(filename, filepath.Join(localTargetsDir, filename))
if err != nil {
return nil, fmt.Errorf("failed to download policy mapping file %s: %w", filename, err)
}
mappings := &policyMappingsFile{}
err = yaml.Unmarshal(fileContents, mappings)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", filename, err)
}
return expandMappingFile(mappings)
}
func expandMappingFile(mappingFile *policyMappingsFile) (*PolicyMappings, error) {
policies := make(map[string]*PolicyMapping)
for _, policy := range mappingFile.Policies {
policies[policy.Id] = policy
}
var rules []*PolicyRule
for _, rule := range mappingFile.Rules {
r, err := regexp.Compile(rule.Pattern)
if err != nil {
return nil, err
}
rules = append(rules, &PolicyRule{
Pattern: r,
PolicyId: rule.PolicyId,
Replacement: rule.Replacement,
})
}
return &PolicyMappings{
Version: mappingFile.Version,
Kind: mappingFile.Kind,
Policies: policies,
Rules: rules,
}, nil
}

View File

@@ -1,141 +0,0 @@
package oci
import (
"context"
"fmt"
"github.com/docker/attest/pkg/attestation"
att "github.com/docker/attest/pkg/attestation"
"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 {
*RegistryImageDetailsResolver
*attestation.AttestationManifest
}
type RegistryImageDetailsResolver struct {
*ImageSpec
descriptor *v1.Descriptor
}
func NewRegistryImageDetailsResolver(src *ImageSpec) (*RegistryImageDetailsResolver, error) {
return &RegistryImageDetailsResolver{
ImageSpec: src,
}, nil
}
func NewRegistryAttestationResolver(src *RegistryImageDetailsResolver) (*RegistryResolver, error) {
return &RegistryResolver{
RegistryImageDetailsResolver: src,
}, nil
}
func (r *RegistryImageDetailsResolver) ImageName(ctx context.Context) (string, error) {
return r.Identifier, nil
}
func (r *RegistryImageDetailsResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
return r.Platform, nil
}
func (r *RegistryImageDetailsResolver) ImageDescriptor(ctx context.Context) (*v1.Descriptor, error) {
if r.descriptor == nil {
subjectRef, err := name.ParseReference(r.Identifier)
if err != nil {
return nil, fmt.Errorf("failed to parse reference: %w", err)
}
options := WithOptions(ctx, r.Platform)
image, err := remote.Image(subjectRef, options...)
if err != nil {
return nil, fmt.Errorf("failed to get image manifest: %w", err)
}
digest, err := image.Digest()
if err != nil {
return nil, fmt.Errorf("failed to get image digest: %w", err)
}
size, err := image.Size()
if err != nil {
return nil, fmt.Errorf("failed to get image size: %w", err)
}
mediaType, err := image.MediaType()
if err != nil {
return nil, fmt.Errorf("failed to get image media type: %w", err)
}
r.descriptor = &v1.Descriptor{
Digest: digest,
Size: size,
MediaType: mediaType,
}
}
return r.descriptor, nil
}
func (r *RegistryResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
if r.AttestationManifest == nil {
attest, err := FetchAttestationManifest(ctx, r.Identifier, r.ImageSpec.Platform)
if err != nil {
return nil, err
}
r.AttestationManifest = attest
}
return ExtractEnvelopes(r.AttestationManifest, predicateType)
}
func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Platform) (*attestation.AttestationManifest, 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.AttestationManifest{
OriginalLayers: layers,
OriginalDescriptor: &remoteDescriptor.Descriptor,
SubjectName: image,
SubjectDescriptor: subjectDescriptor,
}
return attest, nil
}

View File

@@ -1,30 +0,0 @@
package policy
import (
"context"
"fmt"
"github.com/docker/attest/pkg/oci"
)
type policyEvaluatorCtxKeyType struct{}
var PolicyEvaluatorCtxKey policyEvaluatorCtxKeyType
// sets PolicyEvaluator in context
func WithPolicyEvaluator(ctx context.Context, pe PolicyEvaluator) context.Context {
return context.WithValue(ctx, PolicyEvaluatorCtxKey, pe)
}
// gets PolicyEvaluator from context, defaults to Rego PolicyEvaluator if not set
func GetPolicyEvaluator(ctx context.Context) (PolicyEvaluator, error) {
t, ok := ctx.Value(PolicyEvaluatorCtxKey).(PolicyEvaluator)
if !ok {
return nil, fmt.Errorf("no policy evaluator client set on context (set one with policy.WithPolicyEvaluator)")
}
return t, nil
}
type PolicyEvaluator interface {
Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *PolicyInput) (*Result, error)
}

View File

@@ -1,32 +0,0 @@
package policy
import (
"context"
"github.com/docker/attest/pkg/oci"
)
type MockPolicyEvaluator struct {
EvaluateFunc func(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *PolicyInput) (*Result, error)
}
func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *PolicyInput) (*Result, error) {
if pe.EvaluateFunc != nil {
return pe.EvaluateFunc(ctx, resolver, pctx, input)
}
return AllowedResult(), nil
}
func GetMockPolicy() PolicyEvaluator {
return &MockPolicyEvaluator{
EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *PolicyInput) (*Result, error) {
return AllowedResult(), nil
},
}
}
func AllowedResult() *Result {
return &Result{
Success: true,
}
}

View File

@@ -1,238 +0,0 @@
package policy
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"github.com/distribution/reference"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
)
func resolveLocalPolicy(opts *PolicyOptions, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
if opts.LocalPolicyDir == "" {
return nil, fmt.Errorf("local policy dir not set")
}
files := make([]*PolicyFile, 0, len(mapping.Files))
for _, f := range mapping.Files {
filename := f.Path
filePath := path.Join(opts.LocalPolicyDir, filename)
fileContents, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read policy file %s: %w", filename, err)
}
files = append(files, &PolicyFile{
Path: filename,
Content: fileContents,
})
}
policy := &Policy{
InputFiles: files,
Mapping: mapping,
}
if imageName != matchedName {
policy.ResolvedName = matchedName
}
return policy, nil
}
func resolveTufPolicy(opts *PolicyOptions, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
files := make([]*PolicyFile, 0, len(mapping.Files))
for _, f := range mapping.Files {
filename := f.Path
_, fileContents, err := opts.TufClient.DownloadTarget(filename, filepath.Join(opts.LocalTargetsDir, filename))
if err != nil {
return nil, fmt.Errorf("failed to download policy file %s: %w", filename, err)
}
files = append(files, &PolicyFile{
Path: filename,
Content: fileContents,
})
}
policy := &Policy{
InputFiles: files,
Mapping: mapping,
}
if imageName != matchedName {
policy.ResolvedName = matchedName
}
return policy, nil
}
type matchType string
const (
matchTypePolicy matchType = "policy"
matchTypeMatchNoPolicy matchType = "match_no_policy"
matchTypeNoMatch matchType = "no_match"
)
type policyMatch struct {
matchType matchType
policy *config.PolicyMapping
rule *config.PolicyRule
matchedName string
}
func findPolicyMatch(imageName string, mappings *config.PolicyMappings) (*policyMatch, error) {
if mappings == nil {
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
}
return findPolicyMatchImpl(imageName, mappings, make(map[*config.PolicyRule]bool))
}
func findPolicyMatchImpl(imageName string, mappings *config.PolicyMappings, matched map[*config.PolicyRule]bool) (*policyMatch, error) {
for _, rule := range mappings.Rules {
if rule.Pattern.MatchString(imageName) {
switch {
case rule.PolicyId == "" && rule.Replacement == "":
return nil, fmt.Errorf("rule %s has neither policy-id nor rewrite", rule.Pattern)
case rule.PolicyId != "" && rule.Replacement != "":
return nil, fmt.Errorf("rule %s has both policy-id and rewrite", rule.Pattern)
case rule.PolicyId != "":
policy := mappings.Policies[rule.PolicyId]
if policy != nil {
return &policyMatch{
matchType: matchTypePolicy,
policy: policy,
rule: rule,
matchedName: imageName,
}, nil
}
return &policyMatch{
matchType: matchTypeMatchNoPolicy,
rule: rule,
matchedName: imageName,
}, nil
case rule.Replacement != "":
if matched[rule] {
return nil, fmt.Errorf("rewrite loop detected")
}
matched[rule] = true
imageName = rule.Pattern.ReplaceAllString(imageName, rule.Replacement)
return findPolicyMatchImpl(imageName, mappings, matched)
}
}
}
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
}
func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
if opts.PolicyId != "" {
localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir)
if err != nil {
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
}
if localMappings != nil {
policy := localMappings.Policies[opts.PolicyId]
if policy != nil {
return resolveLocalPolicy(opts, policy, "", "")
}
}
// must check tuf
tufMappings, err := config.LoadTufMappings(opts.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 nil, fmt.Errorf("policy with id %s not found", opts.PolicyId)
}
return nil, nil
}
func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver, opts *PolicyOptions) (*Policy, error) {
p, err := resolvePolicyById(opts)
if err != nil {
return nil, fmt.Errorf("failed to resolve policy by id: %w", err)
}
if p != nil {
return p, nil
}
imageName, err := detailsResolver.ImageName(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get image name: %w", err)
}
imageName, err = normalizeImageName(imageName)
if err != nil {
return nil, fmt.Errorf("failed to parse image name: %w", err)
}
localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir)
if err != nil {
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
}
match, err := findPolicyMatch(imageName, localMappings)
if err != nil {
return nil, err
}
if match.matchType == matchTypePolicy {
return resolveLocalPolicy(opts, match.policy, imageName, match.matchedName)
}
// must check tuf
tufMappings, err := config.LoadTufMappings(opts.TufClient, opts.LocalTargetsDir)
if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err)
}
// it's a mirror of a tuf policy
if match.matchType == matchTypeMatchNoPolicy {
for _, mapping := range tufMappings.Policies {
if mapping.Id == match.rule.PolicyId {
return resolveTufPolicy(opts, mapping, imageName, match.matchedName)
}
}
}
// try to resolve a tuf policy directly
match, err = findPolicyMatch(imageName, tufMappings)
if err != nil {
return nil, err
}
if match.matchType == matchTypePolicy {
return resolveTufPolicy(opts, match.policy, imageName, match.matchedName)
}
return nil, nil
}
func normalizeImageName(imageName string) (string, error) {
named, err := reference.ParseNormalizedNamed(imageName)
if err != nil {
return "", fmt.Errorf("failed to parse image name: %w", err)
}
return named.Name(), nil
}
func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsResolver, error) {
switch imageSource.Type {
case oci.OCI:
return oci.NewOCILayoutAttestationResolver(imageSource)
case oci.Docker:
return oci.NewRegistryImageDetailsResolver(imageSource)
}
return nil, fmt.Errorf("unsupported image source type: %s", imageSource.Type)
}
func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *config.PolicyMapping) (oci.AttestationResolver, error) {
switch resolver := resolver.(type) {
case *oci.RegistryImageDetailsResolver:
if mapping.Attestations != nil && mapping.Attestations.Style == config.AttestationStyleAttached {
return oci.NewRegistryAttestationResolver(resolver)
} else {
if mapping.Attestations != nil && mapping.Attestations.Repo != "" {
return oci.NewReferrersAttestationResolver(resolver, oci.WithReferrersRepo(mapping.Attestations.Repo))
} else {
return oci.NewReferrersAttestationResolver(resolver)
}
}
case *oci.OCILayoutResolver:
return resolver, nil
default:
return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver)
}
}

View File

@@ -1,118 +0,0 @@
package policy_test
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"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"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func loadAttestation(t *testing.T, path string) *attestation.Envelope {
ex, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var env = new(attestation.Envelope)
err = json.Unmarshal(ex, env)
if err != nil {
t.Fatal(err)
}
return env
}
func TestRegoEvaluator_Evaluate(t *testing.T) {
ctx, _ := test.Setup(t)
errorStr := "failed to resolve policy by id: policy with id non-existent-policy-id not found"
TestDataPath := filepath.Join("..", "..", "test", "testdata")
ExampleAttestation := filepath.Join(TestDataPath, "example_attestation.json")
re := policy.NewRegoEvaluator(true)
defaultResolver := test.MockResolver{
Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)},
}
testCases := []struct {
repo string
expectSuccess bool
isCanonical bool
resolver oci.AttestationResolver
policy *policy.PolicyOptions
policyId string
errorStr string
}{
{repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver},
{repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver, policyId: "docker-official-images"},
{repo: "testdata/mock-tuf-allow", expectSuccess: false, isCanonical: false, resolver: defaultResolver, policyId: "non-existent-policy-id", errorStr: errorStr},
{repo: "testdata/mock-tuf-deny", expectSuccess: false, isCanonical: false, resolver: defaultResolver},
{repo: "testdata/mock-tuf-verify-sig", expectSuccess: true, isCanonical: false, resolver: defaultResolver},
{repo: "testdata/mock-tuf-wrong-key", expectSuccess: false, isCanonical: false, resolver: defaultResolver},
{repo: "testdata/mock-tuf-allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver},
{repo: "testdata/mock-tuf-allow-canonical", expectSuccess: false, isCanonical: false, resolver: defaultResolver},
}
for _, tc := range testCases {
t.Run(tc.repo, func(t *testing.T) {
input := &policy.PolicyInput{
Digest: "sha256:test-digest",
Purl: "test-purl",
IsCanonical: tc.isCanonical,
}
tufClient := tuf.NewMockTufClient(tc.repo, test.CreateTempDir(t, "", "tuf-dest"))
if tc.policy == nil {
tc.policy = &policy.PolicyOptions{
TufClient: tufClient,
LocalTargetsDir: test.CreateTempDir(t, "", "tuf-targets"),
PolicyId: tc.policyId,
}
}
imageName, err := tc.resolver.ImageName(ctx)
require.NoError(t, err)
platform, err := tc.resolver.ImagePlatform(ctx)
require.NoError(t, err)
src, err := oci.ParseImageSpec(imageName, oci.WithPlatform(platform.String()))
require.NoError(t, err)
resolver, err := policy.CreateImageDetailsResolver(src)
require.NoError(t, err)
policy, err := policy.ResolvePolicy(ctx, resolver, tc.policy)
if tc.errorStr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errorStr)
return
}
require.NoErrorf(t, err, "failed to resolve policy")
require.NotNil(t, policy, "policy should not be nil")
result, err := re.Evaluate(ctx, tc.resolver, policy, input)
require.NoErrorf(t, err, "Evaluate failed")
if tc.expectSuccess {
assert.True(t, result.Success, "Evaluate should have succeeded")
} else {
assert.False(t, result.Success, "Evaluate should have failed")
}
})
}
}
func TestLoadingMappings(t *testing.T) {
policyMappings, err := config.LoadLocalMappings(filepath.Join("testdata", "mock-tuf-allow"))
require.NoError(t, err)
assert.Equal(t, len(policyMappings.Rules), 3)
for _, mirror := range policyMappings.Rules {
if mirror.PolicyId != "" {
assert.Equal(t, "docker-official-images", mirror.PolicyId)
}
}
}

View File

@@ -1,7 +0,0 @@
package attest
import rego.v1
result := {
"success": input.isCanonical,
}

View File

@@ -1,72 +0,0 @@
package tuf
import (
"io"
"os"
"path/filepath"
)
type mockTufClient struct {
srcPath string
dstPath string
}
func NewMockTufClient(srcPath string, dstPath string) *mockTufClient {
if srcPath == "" {
panic("srcPath must be set")
}
if dstPath == "" {
panic("dstPath must be set")
}
return &mockTufClient{
srcPath: srcPath,
dstPath: dstPath,
}
}
func (dc *mockTufClient) DownloadTarget(target string, filePath string) (actualFilePath string, data []byte, err error) {
src, err := os.Open(filepath.Join(dc.srcPath, target))
if err != nil {
return "", nil, err
}
defer src.Close()
var dstFilePath string
if filePath == "" {
dstFilePath = filepath.Join(dc.dstPath, filepath.FromSlash(target))
} else {
dstFilePath = filePath
}
err = os.MkdirAll(filepath.Dir(dstFilePath), 0755)
if err != nil {
return "", nil, err
}
dst, err := os.Create(dstFilePath)
if err != nil {
return "", nil, err
}
defer dst.Close()
// reading from tee will read from src and write to dst at the same time
tee := io.TeeReader(src, dst)
b, err := io.ReadAll(tee)
if err != nil {
return "", nil, err
}
return dstFilePath, b, nil
}
type mockVersionChecker struct {
err error
}
func NewMockVersionChecker() *mockVersionChecker {
return &mockVersionChecker{}
}
func (vc *mockVersionChecker) CheckVersion(client TUFClient) error {
return vc.err
}

View File

@@ -1,242 +0,0 @@
package tuf
import (
"errors"
"fmt"
"io/fs"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/internal/util"
"github.com/theupdateframework/go-tuf/v2/metadata"
"github.com/theupdateframework/go-tuf/v2/metadata/config"
"github.com/theupdateframework/go-tuf/v2/metadata/fetcher"
"github.com/theupdateframework/go-tuf/v2/metadata/trustedmetadata"
"github.com/theupdateframework/go-tuf/v2/metadata/updater"
)
type TufSource string
const (
HttpSource TufSource = "http"
OciSource TufSource = "oci"
)
var (
DockerTufRootProd = embed.RootProd
DockerTufRootStaging = embed.RootStaging
DockerTufRootDev = embed.RootDev
DockerTufRootDefault = embed.RootDefault
)
type TUFClient interface {
DownloadTarget(target, filePath string) (actualFilePath string, data []byte, err error)
}
type TufClient struct {
updater *updater.Updater
cfg *config.UpdaterConfig
}
// NewTufClient creates a new TUF client
func NewTufClient(initialRoot []byte, tufPath, metadataSource, targetsSource string, versionChecker VersionChecker) (*TufClient, error) {
var tufSource TufSource
if strings.HasPrefix(metadataSource, "https://") || strings.HasPrefix(metadataSource, "http://") {
tufSource = HttpSource
} else {
tufSource = OciSource
}
tufRootDigest := util.SHA256Hex(initialRoot)
// create a directory for each initial root.json
metadataPath := filepath.Join(tufPath, tufRootDigest)
err := os.MkdirAll(metadataPath, 0755)
if err != nil {
return nil, fmt.Errorf("failed to create directory '%s': %w", metadataPath, err)
}
rootFile := filepath.Join(metadataPath, "root.json")
var rootBytes []byte
rootBytes, err = os.ReadFile(rootFile)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
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, 0644)
if err != nil {
return nil, fmt.Errorf("Failed to write root.json %w", err)
}
rootBytes = initialRoot
}
// create updater configuration
cfg, err := config.New(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
if tufSource == OciSource {
metadataRepo, metadataTag, found := strings.Cut(metadataSource, ":")
if !found {
fmt.Printf("metadata tag not found in URL, using latest\n")
metadataTag = "latest"
}
cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataTag, targetsSource)
}
// create a new Updater instance
up, err := updater.New(cfg)
if err != nil {
return nil, fmt.Errorf("failed to create TUF updater instance: %w", err)
}
// try to build the top-level metadata
err = up.Refresh()
if err != nil {
return nil, fmt.Errorf("failed to refresh trusted metadata: %w", err)
}
client := &TufClient{
updater: up,
cfg: cfg,
}
err = versionChecker.CheckVersion(client)
if err != nil {
return nil, err
}
return client, nil
}
// DownloadTarget downloads the target file using Updater. The Updater gets the target
// information, verifies if the target is already cached, and if it is not cached,
// downloads the target file.
func (t *TufClient) DownloadTarget(target string, filePath string) (actualFilePath string, data []byte, err error) {
// search if the desired target is available
targetInfo, err := t.updater.GetTargetInfo(target)
if err != nil {
return "", nil, err
}
// check if filePath exists and create the directory if it doesn't
if _, err := os.Stat(filepath.Dir(filePath)); os.IsNotExist(err) {
err = os.MkdirAll(filepath.Dir(filePath), 0755)
if err != nil {
return "", nil, fmt.Errorf("failed to create target download directory '%s': %w", filepath.Dir(filePath), err)
}
}
// target is available, so let's see if the target is already present locally
actualFilePath, data, err = t.updater.FindCachedTarget(targetInfo, filePath)
if err != nil {
return "", nil, fmt.Errorf("failed while finding a cached target: %w", err)
}
if data != nil {
return actualFilePath, data, err
}
// target is not present locally, so let's try to download it
actualFilePath, data, err = t.updater.DownloadTarget(targetInfo, filePath, "")
if err != nil {
return "", nil, fmt.Errorf("failed to download target file %s - %w", target, err)
}
return actualFilePath, data, err
}
func (t *TufClient) GetMetadata() trustedmetadata.TrustedMetadata {
return t.updater.GetTrustedMetadataSet()
}
func (t *TufClient) MaxRootLength() int64 {
return t.cfg.RootMaxLength
}
func (t *TufClient) GetPriorRoots(metadataURL string) (map[string][]byte, error) {
rootMetadata := map[string][]byte{}
trustedMetadata := t.GetMetadata()
client := fetcher.DefaultFetcher{}
for i := 1; i < int(trustedMetadata.Root.Signed.Version); i++ {
meta, err := client.DownloadFile(metadataURL+fmt.Sprintf("/%d.root.json", i), t.MaxRootLength(), time.Second*15)
if err != nil {
return nil, fmt.Errorf("failed to download root metadata: %w", err)
}
rootMetadata[fmt.Sprintf("%d.root.json", i)] = meta
}
return rootMetadata, nil
}
func (t *TufClient) SetRemoteTargetsURL(url string) {
t.cfg.RemoteTargetsURL = url
}
// Derived from updater.loadTargets() in theupdateframework/go-tuf
func (t *TufClient) LoadDelegatedTargets(roleName, parentName string) (*metadata.Metadata[metadata.TargetsType], error) {
// extract the targets meta from the trusted snapshot metadata
meta := t.updater.GetTrustedMetadataSet()
metaInfo := meta.Snapshot.Signed.Meta[fmt.Sprintf("%s.json", roleName)]
// extract the length of the target metadata to be downloaded
length := metaInfo.Length
if length == 0 {
length = t.cfg.TargetsMaxLength
}
// extract which target metadata version should be downloaded in case of consistent snapshots
version := ""
if meta.Root.Signed.ConsistentSnapshot {
version = strconv.FormatInt(metaInfo.Version, 10)
}
// download targets metadata
data, err := t.downloadMetadata(roleName, length, version)
if err != nil {
return nil, err
}
// verify and load the new target metadata
delegatedTargets, err := meta.UpdateDelegatedTargets(data, roleName, parentName)
if err != nil {
return nil, err
}
return delegatedTargets, nil
}
// downloadMetadata download a metadata file and return it as bytes
func (t *TufClient) downloadMetadata(roleName string, length int64, version string) ([]byte, error) {
urlPath := ensureTrailingSlash(t.cfg.RemoteMetadataURL)
// build urlPath
if version == "" {
urlPath = fmt.Sprintf("%s%s.json", urlPath, url.QueryEscape(roleName))
} else {
urlPath = fmt.Sprintf("%s%s.%s.json", urlPath, version, url.QueryEscape(roleName))
}
return t.cfg.Fetcher.DownloadFile(urlPath, length, time.Second*15)
}
// ensureTrailingSlash ensures url ends with a slash
func ensureTrailingSlash(url string) string {
if updater.IsWindowsPath(url) {
slash := string(filepath.Separator)
if strings.HasSuffix(url, slash) {
return url
}
return url + slash
}
if strings.HasSuffix(url, "/") {
return url
}
return url + "/"
}
// GetEmbeddedTufRoot returns the embedded TUF root based on the given root name
func GetEmbeddedTufRoot(root string) (*embed.EmbeddedRoot, error) {
return embed.GetRootFromName(root)
}

2
policy/README.md Normal file
View File

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

11
policy/evaluator.go Normal file
View File

@@ -0,0 +1,11 @@
package policy
import (
"context"
"github.com/docker/attest/attestation"
)
type Evaluator interface {
Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error)
}

65
policy/match.go Normal file
View File

@@ -0,0 +1,65 @@
package policy
import (
"fmt"
"github.com/docker/attest/config"
)
type matchType string
const (
matchTypePolicy matchType = "policy"
matchTypeMatchNoPolicy matchType = "match_no_policy"
matchTypeNoMatch matchType = "no_match"
)
type policyMatch struct {
matchType matchType
policy *config.PolicyMapping
rule *config.PolicyRule
matchedName string
}
func findPolicyMatch(imageName string, mappings *config.PolicyMappings) (*policyMatch, error) {
if mappings == nil {
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
}
return findPolicyMatchImpl(imageName, mappings, make(map[*config.PolicyRule]bool))
}
func findPolicyMatchImpl(imageName string, mappings *config.PolicyMappings, matched map[*config.PolicyRule]bool) (*policyMatch, error) {
for _, rule := range mappings.Rules {
if rule.Pattern.MatchString(imageName) {
switch {
case rule.PolicyID == "" && rule.Replacement == "":
return nil, fmt.Errorf("rule %s has neither policy-id nor rewrite", rule.Pattern)
case rule.PolicyID != "" && rule.Replacement != "":
return nil, fmt.Errorf("rule %s has both policy-id and rewrite", rule.Pattern)
case rule.PolicyID != "":
policy := mappings.Policies[rule.PolicyID]
if policy != nil {
return &policyMatch{
matchType: matchTypePolicy,
policy: policy,
rule: rule,
matchedName: imageName,
}, nil
}
return &policyMatch{
matchType: matchTypeMatchNoPolicy,
rule: rule,
matchedName: imageName,
}, nil
case rule.Replacement != "":
if matched[rule] {
return nil, fmt.Errorf("rewrite loop detected")
}
matched[rule] = true
imageName = rule.Pattern.ReplaceAllString(imageName, rule.Replacement)
return findPolicyMatchImpl(imageName, mappings, matched)
}
}
}
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
}

View File

@@ -4,7 +4,7 @@ import (
"path/filepath"
"testing"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -15,10 +15,11 @@ func TestFindPolicyMatch(t *testing.T) {
imageName string
mappingDir string
expectError bool
expectedMatchType matchType
expectedPolicyID string
expectedImageName string
expectError bool
expectLoadingError bool
expectedMatchType matchType
expectedPolicyID string
expectedImageName string
}{
{
name: "alpine",
@@ -79,16 +80,6 @@ func TestFindPolicyMatch(t *testing.T) {
expectedPolicyID: "docker-official-images",
expectedImageName: "docker.io/library/alpine",
},
{
name: "invalid rewrites",
mappingDir: "rewrite-invalid",
imageName: "mycoolmirror.org/library/alpine",
expectError: true,
expectedMatchType: matchTypePolicy,
expectedPolicyID: "docker-official-images",
expectedImageName: "docker.io/library/alpine",
},
{
name: "rewrite loop",
mappingDir: "rewrite-loop",
@@ -112,7 +103,7 @@ func TestFindPolicyMatch(t *testing.T) {
assert.Equal(t, tc.expectedMatchType, match.matchType)
if match.matchType == matchTypePolicy {
if assert.NotNil(t, match.policy) {
assert.Equal(t, tc.expectedPolicyID, match.policy.Id)
assert.Equal(t, tc.expectedPolicyID, match.policy.ID)
}
}
assert.Equal(t, tc.expectedImageName, match.matchedName)

32
policy/mock.go Normal file
View File

@@ -0,0 +1,32 @@
package policy
import (
"context"
"github.com/docker/attest/attestation"
)
type MockPolicyEvaluator struct {
EvaluateFunc func(ctx context.Context, resolver attestation.Resolver, 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 {
return pe.EvaluateFunc(ctx, resolver, pctx, input)
}
return AllowedResult(), nil
}
func GetMockPolicy() Evaluator {
return &MockPolicyEvaluator{
EvaluateFunc: func(_ context.Context, _ attestation.Resolver, _ *Policy, _ *Input) (*Result, error) {
return AllowedResult(), nil
},
}
}
func AllowedResult() *Result {
return &Result{
Success: true,
}
}

92
policy/policy.go Normal file
View File

@@ -0,0 +1,92 @@
package policy
import (
"context"
"fmt"
"github.com/distribution/reference"
"github.com/docker/attest/attestation"
"github.com/docker/attest/config"
"github.com/docker/attest/oci"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/package-url/packageurl-go"
)
func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsResolver, error) {
switch imageSource.Type {
case oci.OCI:
return attestation.NewOCILayoutResolver(imageSource)
case oci.Docker:
return oci.NewRegistryImageDetailsResolver(imageSource)
}
return nil, fmt.Errorf("unsupported image source type: %s", imageSource.Type)
}
func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *config.PolicyMapping) (attestation.Resolver, error) {
if mapping.Attestations != nil {
if mapping.Attestations.Style == config.AttestationStyleAttached {
switch resolver := resolver.(type) {
case *oci.RegistryImageDetailsResolver:
return attestation.NewRegistryResolver(resolver)
case *attestation.LayoutResolver:
return resolver, nil
default:
return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver)
}
}
if mapping.Attestations.Repo != "" {
return attestation.NewReferrersResolver(resolver, attestation.WithReferrersRepo(mapping.Attestations.Repo))
}
}
return attestation.NewReferrersResolver(resolver)
}
// VerifySubject verifies if any of the given subject PURLs matches the image name and platform from resolver.
// Tags are not taken into account when attempting to match because sometimes the user may not have specified a tag, and maybe there
// isn't a purl subject with that particular tag (because of post build tagging?).
func VerifySubject(ctx context.Context, subject []intoto.Subject, resolver attestation.Resolver) error {
img, err := resolver.ImageName(ctx)
if err != nil {
return err
}
inputName, err := reference.ParseNormalizedNamed(img)
if err != nil {
return err
}
descriptor, err := resolver.ImageDescriptor(ctx)
if err != nil {
return err
}
platform, err := resolver.ImagePlatform(ctx)
if err != nil {
return err
}
for _, sub := range subject {
if sub.Digest[descriptor.Digest.Algorithm] != descriptor.Digest.Hex {
continue
}
purl, err := packageurl.FromString(sub.Name)
if err != nil {
continue
}
if purl.Type != "docker" {
continue
}
if purl.Qualifiers.Map()["platform"] != platform.String() {
continue
}
// ensure reference is normalized before comparing
subjectName, err := reference.ParseNormalizedNamed(purl.Name)
if err != nil {
continue
}
// this assumes that domain is part of the package URL (some say it should be a qualifier)
// buildkit puts the domain in the name, e.g. pkg:docker/ecr.io/foobar/alpine@latest?platform=linux%2Famd64
if inputName.Name() == subjectName.Name() {
// found a match
return nil
}
}
return fmt.Errorf("no matching subject found for image: %s", img)
}

384
policy/policy_test.go Normal file
View File

@@ -0,0 +1,384 @@
package policy_test
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/docker/attest/attestation"
"github.com/docker/attest/config"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/oci"
"github.com/docker/attest/policy"
v1 "github.com/google/go-containerregistry/pkg/v1"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func loadAttestation(t *testing.T, path string) *attestation.Envelope {
ex, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
env := new(attestation.Envelope)
err = json.Unmarshal(ex, env)
if err != nil {
t.Fatal(err)
}
return env
}
func TestRegoEvaluator_Evaluate(t *testing.T) {
ctx, _ := test.Setup(t)
resolveErrorStr := "failed to resolve policy by id: policy with id non-existent-policy-id not found"
TestDataPath := filepath.Join("..", "test", "testdata")
ExampleAttestation := filepath.Join(TestDataPath, "example_attestation.json")
re := policy.NewRegoEvaluator(true)
defaultResolver := attestation.MockResolver{
Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)},
}
testCases := []struct {
policyPath string
expectSuccess bool
isCanonical bool
resolver attestation.Resolver
opts *policy.Options
policyID string
resolveErrorStr string
}{
{policyPath: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver},
{policyPath: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver, policyID: "docker-official-images"},
{policyPath: "testdata/policies/allow", resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr},
{policyPath: "testdata/policies/deny", resolver: defaultResolver},
{policyPath: "testdata/policies/verify-sig", expectSuccess: true, resolver: defaultResolver},
{policyPath: "testdata/policies/wrong-key", resolver: defaultResolver},
{policyPath: "testdata/policies/allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver},
{policyPath: "testdata/policies/allow-canonical", resolver: defaultResolver},
{policyPath: "testdata/policies/no-rego", resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"},
}
for _, tc := range testCases {
t.Run(tc.policyPath, func(t *testing.T) {
input := &policy.Input{
Digest: "sha256:test-digest",
PURL: "test-purl",
}
if !tc.isCanonical {
input.Tag = "test"
}
if tc.opts == nil {
tc.opts = &policy.Options{
LocalTargetsDir: test.CreateTempDir(t, "", "tuf-targets"),
PolicyID: tc.policyID,
LocalPolicyDir: tc.policyPath,
DisableTUF: true,
}
}
imageName, err := tc.resolver.ImageName(ctx)
require.NoError(t, err)
resolver := policy.NewResolver(nil, tc.opts)
policy, err := resolver.ResolvePolicy(ctx, imageName)
if tc.resolveErrorStr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.resolveErrorStr)
return
}
require.NoErrorf(t, err, "failed to resolve policy")
require.NotNil(t, policy, "policy should not be nil")
result, err := re.Evaluate(ctx, tc.resolver, policy, input)
require.NoErrorf(t, err, "Evaluate failed")
if tc.expectSuccess {
assert.True(t, result.Success, "Evaluate should have succeeded")
} else {
assert.False(t, result.Success, "Evaluate should have failed")
}
})
}
}
func TestLoadingMappings(t *testing.T) {
policyMappings, err := config.LoadLocalMappings(filepath.Join("testdata", "policies", "allow"))
require.NoError(t, err)
assert.Equal(t, len(policyMappings.Rules), 3)
for _, mirror := range policyMappings.Rules {
if mirror.PolicyID != "" {
assert.Equal(t, "docker-official-images", mirror.PolicyID)
}
}
}
func TestCreateAttestationResolver(t *testing.T) {
mockResolver := attestation.MockResolver{
Envs: []*attestation.Envelope{},
}
layoutResolver := &attestation.LayoutResolver{}
registryResolver := &oci.RegistryImageDetailsResolver{}
nilRepoReferrers := &config.PolicyMapping{
Attestations: &config.AttestationConfig{
Style: config.AttestationStyleReferrers,
},
}
referrers := &config.PolicyMapping{
Attestations: &config.AttestationConfig{
Repo: "localhost:5000/repo",
Style: config.AttestationStyleReferrers,
},
}
attached := &config.PolicyMapping{
Attestations: &config.AttestationConfig{
Style: config.AttestationStyleAttached,
},
}
testCases := []struct {
name string
resolver oci.ImageDetailsResolver
mapping *config.PolicyMapping
errorStr string
}{
{name: "referrers", resolver: layoutResolver, mapping: referrers},
{name: "referrers (no mapped repo)", resolver: layoutResolver, mapping: nilRepoReferrers},
{name: "referrers (no mapping)", resolver: layoutResolver, mapping: &config.PolicyMapping{Attestations: nil}},
{name: "attached (registry)", resolver: registryResolver, mapping: attached},
{name: "attached (layout)", resolver: layoutResolver, mapping: attached},
{name: "attached (unsupported)", resolver: mockResolver, mapping: attached, errorStr: "unsupported image details resolver type"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
resolver, err := policy.CreateAttestationResolver(tc.resolver, tc.mapping)
if tc.errorStr == "" {
require.NoError(t, err)
} else {
assert.Contains(t, err.Error(), tc.errorStr)
}
if tc.mapping.Attestations == nil {
return
}
switch resolver.(type) {
case *attestation.ReferrersResolver:
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleReferrers)
case *attestation.RegistryResolver:
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached)
case *attestation.LayoutResolver:
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached)
}
})
}
}
func TestVerifySubject(t *testing.T) {
ctx, _ := test.Setup(t)
defaultResolver := attestation.MockResolver{}
testCases := []struct {
name string
subject []intoto.Subject
img string
expectError bool
digest string
}{
{
name: "library short",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "alpine",
},
{
name: "with domain and namespace",
subject: []intoto.Subject{
{
Name: "pkg:docker/docker.io/library/alpine@latest?platform=linux%2Famd64",
},
},
img: "alpine",
},
{
name: "with library",
subject: []intoto.Subject{
{
Name: "pkg:docker/library/alpine@latest?platform=linux%2Famd64",
},
},
img: "alpine",
},
{
name: "library short with tag",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "alpine:foo",
},
{
name: "library with namespace",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "library/alpine:foo",
},
{
name: "library with domain",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "docker.io/library/alpine:foo",
},
{
name: "domain mismatch",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "ecr.io/library/alpine:foo",
expectError: true,
},
{
name: "type mismatch",
subject: []intoto.Subject{
{
Name: "pkg:node/alpine@latest?platform=linux%2Famd64",
},
},
img: "alpine",
expectError: true,
},
{
name: "name mismatch",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "library/debian:latest",
expectError: true,
},
{
name: "namespace mismatch",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "unsupported/alpine:latest",
expectError: true,
},
{
name: "digest mismatch",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "alpine",
digest: "1234",
expectError: true,
},
{
name: "platform mismatch",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Farm64",
},
},
img: "alpine",
expectError: true,
},
{
name: "malformed purl",
subject: []intoto.Subject{
{
Name: "not-a-purl",
},
},
img: "alpine",
expectError: true,
},
{
name: "malformed image in valid purl",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine,bar@latest?platform=linux%2Famd64",
},
},
img: "alpine-broken",
expectError: true,
},
{
name: "malformed image name",
subject: []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
},
},
img: "foo bar",
expectError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
defaultResolver.Image = tc.img
// digest from mock resolver
tc.subject[0].Digest = map[string]string{"sha256": "da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620"}
if tc.digest != "" {
tc.subject[0].Digest = map[string]string{"sha256": tc.digest}
}
err := policy.VerifySubject(ctx, tc.subject, defaultResolver)
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
defaultResolver.Image = "alpine"
subject := []intoto.Subject{
{
Name: "pkg:docker/alpine@latest?platform=linux%2Famd64",
Digest: map[string]string{"sha256": "da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620"},
},
}
// error getting descriptor
defaultResolver.DescriptorFn = func() (*v1.Descriptor, error) {
return nil, fmt.Errorf("error")
}
err := policy.VerifySubject(ctx, subject, defaultResolver)
require.Error(t, err)
// error getting platform
defaultResolver.DescriptorFn = nil
defaultResolver.PlatformFn = func() (*v1.Platform, error) {
return nil, fmt.Errorf("error")
}
err = policy.VerifySubject(ctx, subject, defaultResolver)
require.Error(t, err)
// error getting image name
defaultResolver.PlatformFn = nil
defaultResolver.Image = ""
defaultResolver.ImangeNameFn = func() (string, error) {
return "", fmt.Errorf("error")
}
err = policy.VerifySubject(ctx, subject, defaultResolver)
require.Error(t, err)
}

View File

@@ -7,8 +7,7 @@ import (
"os"
"path/filepath"
att "github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/attestation"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/rego"
@@ -30,13 +29,13 @@ const (
resultBinding = "result"
)
func NewRegoEvaluator(debug bool) PolicyEvaluator {
func NewRegoEvaluator(debug bool) Evaluator {
return &regoEvaluator{
debug: debug,
}
}
func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *PolicyInput) (*Result, error) {
func (re *regoEvaluator) Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error) {
var regoOpts []func(*rego.Rego)
// Create a new in-memory store
@@ -113,7 +112,7 @@ func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationR
}
func jsonGenerator[T any]() func(t *ast.Term, ec *rego.EvalContext) (any, error) {
return func(t *ast.Term, ec *rego.EvalContext) (any, error) {
return func(t *ast.Term, _ *rego.EvalContext) (any, error) {
// TODO: this is horrible - we're converting the AST to JSON and then back to AST, then using ast.As to convert it to a struct
// We can't use ast.As directly because it fails if the AST contains a set
json, err := ast.JSON(t.Value)
@@ -140,6 +139,7 @@ var verifyDecl = &ast.Builtin{
Decl: types.NewFunction(types.Args(dynamicObj, dynamicObj), dynamicObj),
Nondeterministic: true,
}
var attestDecl = &ast.Builtin{
Name: "attest.fetch",
Decl: types.NewFunction(types.Args(types.S), dynamicObj),
@@ -169,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{
{
Decl: verifyDecl,
@@ -180,7 +180,7 @@ func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin {
Memoize: true,
Nondeterministic: verifyDecl.Nondeterministic,
},
handleErrors2(verifyIntotoEnvelope)),
handleErrors2(verifyInTotoEnvelope(resolver))),
},
{
Decl: attestDecl,
@@ -191,12 +191,12 @@ func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin {
Memoize: true,
Nondeterministic: attestDecl.Nondeterministic,
},
handleErrors1(fetchIntotoAttestations(resolver))),
handleErrors1(fetchInTotoAttestations(resolver))),
},
}
}
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) {
predicateTypeStr, ok := predicateTypeTerm.Value.(ast.String)
if !ok {
@@ -226,41 +226,48 @@ func fetchIntotoAttestations(resolver oci.AttestationResolver) rego.Builtin1 {
}
}
func verifyIntotoEnvelope(rCtx rego.BuiltinContext, envTerm, optsTerm *ast.Term) (*ast.Term, error) {
env := new(att.Envelope)
opts := new(att.VerifyOptions)
err := ast.As(envTerm.Value, env)
if err != nil {
return nil, fmt.Errorf("failed to cast envelope: %w", err)
}
err = ast.As(optsTerm.Value, &opts)
if err != nil {
return nil, fmt.Errorf("failed to cast verifier options: %w", err)
}
payload, err := att.VerifyDSSE(rCtx.Context, env, opts)
if err != nil {
return nil, err
}
statement := new(intoto.Statement)
switch env.PayloadType {
case intoto.PayloadType:
err = json.Unmarshal(payload, statement)
func verifyInTotoEnvelope(resolver attestation.Resolver) rego.Builtin2 {
return func(rCtx rego.BuiltinContext, envTerm, optsTerm *ast.Term) (*ast.Term, error) {
env := new(attestation.Envelope)
opts := new(attestation.VerifyOptions)
err := ast.As(envTerm.Value, env)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal statement: %w", err)
return nil, fmt.Errorf("failed to cast envelope: %w", err)
}
err = ast.As(optsTerm.Value, &opts)
if err != nil {
return nil, fmt.Errorf("failed to cast verifier options: %w", err)
}
// TODO: implement other types of envelope
default:
return nil, fmt.Errorf("unsupported payload type: %s", env.PayloadType)
}
value, err := ast.InterfaceToValue(statement)
if err != nil {
return nil, err
payload, err := attestation.VerifyDSSE(rCtx.Context, env, opts)
if err != nil {
return nil, fmt.Errorf("failed to verify envelope: %w", err)
}
statement := new(intoto.Statement)
switch env.PayloadType {
case intoto.PayloadType:
err = json.Unmarshal(payload, statement)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal statement: %w", err)
}
// TODO: implement other types of envelope
default:
return nil, fmt.Errorf("unsupported payload type: %s", env.PayloadType)
}
err = VerifySubject(rCtx.Context, statement.Subject, resolver)
if err != nil {
return nil, fmt.Errorf("failed to verify subject: %w", err)
}
value, err := ast.InterfaceToValue(statement)
if err != nil {
return nil, err
}
return ast.NewTerm(value), nil
}
return ast.NewTerm(value), nil
}
func loadYAML(path string, bs []byte) (interface{}, error) {

194
policy/resolver.go Normal file
View File

@@ -0,0 +1,194 @@
package policy
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"github.com/distribution/reference"
"github.com/docker/attest/config"
"github.com/docker/attest/internal/util"
"github.com/docker/attest/tuf"
)
type Resolver struct {
tufClient tuf.Downloader
opts *Options
}
func NewResolver(tufClient tuf.Downloader, opts *Options) *Resolver {
return &Resolver{
tufClient: tufClient,
opts: opts,
}
}
func (r *Resolver) ResolvePolicy(_ context.Context, imageName string) (*Policy, error) {
p, err := r.resolvePolicyByID()
if err != nil {
return nil, fmt.Errorf("failed to resolve policy by id: %w", err)
}
if p != nil {
return p, nil
}
imageName, err = normalizeImageName(imageName)
if err != nil {
return nil, fmt.Errorf("failed to parse image name: %w", err)
}
localMappings, err := config.LoadLocalMappings(r.opts.LocalPolicyDir)
if err != nil {
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
}
match, err := findPolicyMatch(imageName, localMappings)
if err != nil {
return nil, err
}
if match.matchType == matchTypePolicy {
return r.resolveLocalPolicy(match.policy, imageName, match.matchedName)
}
if !r.opts.DisableTUF {
tufMappings, err := config.LoadTUFMappings(r.tufClient, r.opts.LocalTargetsDir)
if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err)
}
// it's a mirror of a tuf policy
if match.matchType == matchTypeMatchNoPolicy {
for _, mapping := range tufMappings.Policies {
if mapping.ID == match.rule.PolicyID {
return r.resolveTUFPolicy(mapping, imageName, match.matchedName)
}
}
}
// try to resolve a tuf policy directly
match, err = findPolicyMatch(imageName, tufMappings)
if err != nil {
return nil, err
}
if match.matchType == matchTypePolicy {
return r.resolveTUFPolicy(match.policy, imageName, match.matchedName)
}
}
return nil, nil
}
func (r *Resolver) resolveLocalPolicy(mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
if r.opts.LocalPolicyDir == "" {
return nil, fmt.Errorf("local policy dir not set")
}
var URI string
var digest map[string]string
files := make([]*File, 0, len(mapping.Files))
for _, f := range mapping.Files {
filename := f.Path
filePath := path.Join(r.opts.LocalPolicyDir, filename)
fileContents, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read policy file %s: %w", filename, err)
}
files = append(files, &File{
Path: filename,
Content: fileContents,
})
// if the file is a policy file, store the URI and digest
if filepath.Ext(filename) == ".rego" {
// TODO: support multiple rego files, need some way to identify the main policy file
if URI != "" {
return nil, fmt.Errorf("multiple policy files found in policy mapping")
}
URI = filePath
digest = map[string]string{"sha256": util.SHA256Hex(fileContents)}
}
}
if URI == "" {
return nil, fmt.Errorf("no policy file found in policy mapping")
}
policy := &Policy{
InputFiles: files,
Mapping: mapping,
URI: URI,
Digest: digest,
}
if imageName != matchedName {
policy.ResolvedName = matchedName
}
return policy, nil
}
func (r *Resolver) resolveTUFPolicy(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 := r.tufClient.DownloadTarget(filename, filepath.Join(r.opts.LocalTargetsDir, filename))
if err != nil {
return nil, fmt.Errorf("failed to download policy file %s: %w", filename, err)
}
files = append(files, &File{
Path: filename,
Content: file.Data,
})
// if the file is a policy file, store the URI and digest
if filepath.Ext(filename) == ".rego" {
// TODO: support multiple rego files, need some way to identify the main policy file
if URI != "" {
return nil, fmt.Errorf("multiple policy files found in policy mapping")
}
URI = file.TargetURI
digest = map[string]string{"sha256": file.Digest}
}
}
if URI == "" {
return nil, fmt.Errorf("no policy file found in policy mapping")
}
policy := &Policy{
InputFiles: files,
Mapping: mapping,
URI: URI,
Digest: digest,
}
if imageName != matchedName {
policy.ResolvedName = matchedName
}
return policy, nil
}
func (r *Resolver) resolvePolicyByID() (*Policy, error) {
if r.opts.PolicyID != "" {
localMappings, err := config.LoadLocalMappings(r.opts.LocalPolicyDir)
if err != nil {
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
}
if localMappings != nil {
policy := localMappings.Policies[r.opts.PolicyID]
if policy != nil {
return r.resolveLocalPolicy(policy, "", "")
}
}
if !r.opts.DisableTUF {
tufMappings, err := config.LoadTUFMappings(r.tufClient, r.opts.LocalTargetsDir)
if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err)
}
policy := tufMappings.Policies[r.opts.PolicyID]
if policy != nil {
return r.resolveTUFPolicy(policy, "", "")
}
}
return nil, fmt.Errorf("policy with id %s not found", r.opts.PolicyID)
}
return nil, nil
}
func normalizeImageName(imageName string) (string, error) {
named, err := reference.ParseNormalizedNamed(imageName)
if err != nil {
return "", fmt.Errorf("failed to parse image name: %w", err)
}
return named.Name(), nil
}

65
policy/resolver_test.go Normal file
View File

@@ -0,0 +1,65 @@
package policy_test
import (
"context"
"testing"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/policy"
"github.com/docker/attest/tuf"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestResolvePolicy(t *testing.T) {
localPolicyPath := "testdata/policies/allow"
tufPolicyPath := "testdata/policies/allow-canonical"
noLocalPolicyPath := "testdata/policies/no-policy"
testPolicyID := "docker-official-images"
testImageName := "localhost:5001/test/repo:tag"
testCases := []struct {
name string
policyPath string
policyID string
localOverridesTUF bool // if a policy is provided locally, it should override TUF
DisableTUF bool
}{
{name: "resolve by id (TUF only)", policyID: testPolicyID, DisableTUF: false},
{name: "resolve by id (local mapping, TUF policy)", policyPath: noLocalPolicyPath, policyID: testPolicyID, DisableTUF: false},
{name: "resolve by id (local mapping, local policy, no TUF)", policyPath: localPolicyPath, policyID: testPolicyID, DisableTUF: true},
{name: "resolve by id (local mapping, local policy)", policyPath: localPolicyPath, policyID: testPolicyID, DisableTUF: false, localOverridesTUF: true},
{name: "resolve by match (TUF only)", DisableTUF: false},
{name: "resolve by match (local mapping, TUF policy)", policyPath: noLocalPolicyPath, DisableTUF: false},
{name: "resolve by match (local mapping, local policy, no TUF)", policyPath: localPolicyPath, DisableTUF: true},
{name: "resolve by match (local mapping, local policy)", policyPath: localPolicyPath, DisableTUF: false, localOverridesTUF: true},
}
var tufClient tuf.Downloader
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
opts := &policy.Options{}
tempDir := test.CreateTempDir(t, "", "tuf-dest")
if !tc.DisableTUF {
tufClient = tuf.NewMockTufClient(tufPolicyPath)
}
if tc.policyID != "" {
opts.PolicyID = tc.policyID
}
if tc.policyPath != "" {
opts.LocalPolicyDir = tc.policyPath
}
opts.DisableTUF = tc.DisableTUF
opts.LocalTargetsDir = tempDir
resolver := policy.NewResolver(tufClient, opts)
policy, err := resolver.ResolvePolicy(context.Background(), testImageName)
require.NoError(t, err)
assert.NotNil(t, policy)
if tc.DisableTUF || tc.localOverridesTUF {
assert.Contains(t, policy.URI, localPolicyPath)
} else {
assert.Contains(t, policy.URI, tufPolicyPath)
}
})
}
}

View File

@@ -0,0 +1,11 @@
package attest
import rego.v1
default canonical = false
canonical if {
not input.tag
}
result := {"success": canonical}

View File

@@ -0,0 +1,10 @@
# map repos to policies
version: v1
kind: policy-mapping
rules:
- pattern: "^docker[.]io/library/(.*)$"
policy-id: docker-official-images
- pattern: ^localhost:5001/(.*)$
rewrite: docker.io/library/$1
- pattern: ^registry[.]local:5000/(.*)$
rewrite: docker.io/library/$1

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

@@ -1,8 +1,8 @@
package policy
import (
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/tuf"
"github.com/docker/attest/config"
"github.com/docker/attest/tuf"
intoto "github.com/in-toto/in-toto-golang/in_toto"
)
@@ -26,29 +26,37 @@ type Result struct {
Summary Summary `json:"summary"`
}
type PolicyOptions struct {
TufClient tuf.TUFClient
type Options struct {
TUFClientOptions *tuf.ClientOptions
DisableTUF bool
LocalTargetsDir string
LocalPolicyDir string
PolicyId string
PolicyID string
ReferrersRepo string
AttestationStyle config.AttestationStyle
Debug bool
}
type Policy struct {
InputFiles []*PolicyFile
InputFiles []*File
Query string
Mapping *config.PolicyMapping
ResolvedName string
URI string
Digest map[string]string
}
type PolicyInput struct {
Digest string `json:"digest"`
Purl string `json:"purl"`
IsCanonical bool `json:"isCanonical"`
type Input struct {
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 PolicyFile struct {
type File struct {
Path string
Content []byte
}

Some files were not shown because too many files have changed in this diff Show More