65 Commits

Author SHA1 Message Date
James Carnegie
357768d421 Various fixes (#63)
* Fix digest resolution and attestation style

* Add a bunch more tests

* Rename fields for consistency

* Remove copy-pasta

* Value -> pointer
2024-06-21 22:12:42 +01:00
James Carnegie
6bd57e02b6 Add support for separate attestation storage repo (#62)
* Add support for separate attestation storage repo
* Move mapping file types and parsing to config package
* Change signature of Verify to take image/platform
* Separate Attestation Resolvers to their own files (registry, layout and referrers)
* Add support configuring referrers resolution style in mapping.yaml
* Add registry test
2024-06-21 11:29:16 +01:00
dependabot[bot]
86878482c3 feat(deps): bump github.com/aws/aws-sdk-go-v2/config (#58) 2024-06-18 15:06:00 +00:00
James Carnegie
130e1f640b Support referrers using digest, not just tag (#55)
* Support referrers using digest, not just tag

* ParseRef and switch on type

* Call DigestStr instead of String
2024-06-17 17:30:12 +01:00
Jonny Stoten
0d0d86854c Return policy input with verification result (#56) 2024-06-17 17:28:22 +01:00
Jonny Stoten
1d9e14b99f Avoid pointers to map (#57) 2024-06-17 17:24:29 +01:00
dependabot[bot]
83c7d7634a feat(deps): bump github.com/google/go-containerregistry (#54)
Bumps [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) from 0.19.1 to 0.19.2.
- [Release notes](https://github.com/google/go-containerregistry/releases)
- [Changelog](https://github.com/google/go-containerregistry/blob/main/.goreleaser.yml)
- [Commits](https://github.com/google/go-containerregistry/compare/v0.19.1...v0.19.2)

---
updated-dependencies:
- dependency-name: github.com/google/go-containerregistry
  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-06-17 11:03:08 +01:00
Joel Kamp
5c07bd70d9 Merge pull request #53 from docker/fix-default-mkdir-perms
fix: mkdir perms
2024-06-14 15:42:23 -05:00
mrjoelkamp
c02e628600 fix: mkdir perms 2024-06-14 15:23:25 -05:00
Joel Kamp
3d46780a1c Merge pull request #52 from docker/refactor-use-interface-value
refactor: use interface value
2024-06-14 11:58:45 -05:00
mrjoelkamp
83dfd746b9 fix: update output dir permissions 2024-06-14 11:11:48 -05:00
mrjoelkamp
845fe93c11 refactor: remove any; split into functions 2024-06-14 10:04:18 -05:00
mrjoelkamp
c154613c52 refactor: use interface value 2024-06-14 10:03:39 -05:00
James Carnegie
e44390d2bc Don't use pointers for image interfaces (#51)
* Don't use pointers for image interfaces

* Also for oci layout

* Remove default case
2024-06-14 10:28:14 +01:00
James Carnegie
8ba9656645 Add support for OCI Referrers and fallback (#50)
* Add support for OCI Referrers and fallback
2024-06-13 16:10:41 +01:00
dependabot[bot]
e120439035 feat(deps): bump github.com/containerd/containerd from 1.7.17 to 1.7.18 (#48) 2024-06-12 20:16:09 +00:00
dependabot[bot]
b20f452004 feat(deps): bump github.com/aws/aws-sdk-go-v2/config (#49) 2024-06-10 17:23:42 +00:00
James Carnegie
4be882aeb0 Handle errors from Go in Rego. Support for skipping TL (#47)
* Make TL logging/verification optional

* Return errors from go-lang fns

* Update pkg/policy/rego.go

Co-authored-by: Jonny Stoten <jonny@jonnystoten.com>

* Update pkg/attestation/sign.go

Co-authored-by: Joel Kamp <joel.kamp@docker.com>

* Move public key marshelling until later

* Simplify logSignature and pass down opts

---------

Co-authored-by: Jonny Stoten <jonny@jonnystoten.com>
Co-authored-by: Joel Kamp <joel.kamp@docker.com>
2024-06-06 09:59:32 +01:00
dependabot[bot]
3b5c506739 feat(deps): bump github.com/aws/aws-sdk-go-v2/config (#46)
Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.27.16 to 1.27.17.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.27.16...config/v1.27.17)

---
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-06-04 15:53:00 +01:00
dependabot[bot]
f36bb50af5 feat(deps): bump github.com/open-policy-agent/opa from 0.64.1 to 0.65.0 (#44)
Bumps [github.com/open-policy-agent/opa](https://github.com/open-policy-agent/opa) from 0.64.1 to 0.65.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.64.1...v0.65.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-31 11:15:43 +01:00
James Carnegie
c8c148c70a Expose ParsePlatform (#45) 2024-05-31 11:02:14 +01:00
James Carnegie
a334599635 *Breaking* Parse platform earlier (#43)
* *Breaking* Parse platform earlier

* Use constructors and hide fields to avoid confusion
2024-05-30 17:38:58 +01:00
dependabot[bot]
e81016fc31 feat(deps): bump github.com/sigstore/sigstore/pkg/signature/kms/aws (#42)
Bumps [github.com/sigstore/sigstore/pkg/signature/kms/aws](https://github.com/sigstore/sigstore) from 1.8.3 to 1.8.4.
- [Release notes](https://github.com/sigstore/sigstore/releases)
- [Commits](https://github.com/sigstore/sigstore/compare/v1.8.3...v1.8.4)

---
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-29 12:04:38 +01:00
James Carnegie
2ae5606c92 Add support for selecting a policy by ID (#41) 2024-05-28 15:17:37 +01:00
dependabot[bot]
8a6e75ce39 feat(deps): bump github.com/aws/aws-sdk-go-v2/config (#40) 2024-05-24 13:47:05 +00:00
Jonny Stoten
6397dcede8 Check version of attest against constraints in TUF (#19)
* Check version of attest against constraints in TUF

* Add link to semver lib constraints docs
2024-05-22 17:02:25 +01:00
Jonny Stoten
1a7897a052 Return VSA and rich errors from verification (#38)
* Start of richer results from verification

* Pull out VSA code from signing

* Expose attestation signing fns

* Add VSA test

* Notes for policy result

* Require separate policy for VSA creation

* Load test signing key from tests

* Return rich object from policy

* Add result object schema and fix tests

* Ensure example test runs

* Remove data.yaml files from mock policies

* Don't run example - TUF policy isn't compatible

* Add attestation to manifests for all subjects

* Ensure adding attestation doesn't touch statements

* Don't export sign function

* Remove attestations from VerificationResult

* Change bool to Outcome enum in result

* Use outputLayout directly

* Make clearer that Outcome strings are for VSA

* Return multiple SLSA levels from policy

* Fix unmarshalling of policy-id (#39)

* Rename function

* Rename policy.VerificationResult -> policy.Result

* Re-add test for canonical input

---------

Co-authored-by: James Carnegie <james.carnegie@docker.com>
Co-authored-by: James Carnegie <kipz@users.noreply.github.com>
2024-05-22 14:49:23 +01:00
James Carnegie
745eea09e8 Fix image detection based on platform (#33) 2024-05-20 09:37:53 +01:00
dependabot[bot]
84d7903c46 feat(deps): bump github.com/containerd/containerd from 1.7.16 to 1.7.17 (#35) 2024-05-17 17:19:30 +00:00
dependabot[bot]
7234e29829 feat(deps): bump github.com/package-url/packageurl-go (#36) 2024-05-17 17:14:13 +00:00
Joel Kamp
b46f544f0c Merge pull request #34 from docker/dependabot/go_modules/github.com/aws/aws-sdk-go-v2/config-1.27.15
feat(deps): bump github.com/aws/aws-sdk-go-v2/config from 1.27.14 to 1.27.15
2024-05-17 12:13:31 -05:00
dependabot[bot]
85d7b34e18 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.14 to 1.27.15.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.27.14...config/v1.27.15)

---
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-05-17 17:07:46 +00:00
Joel Kamp
c416c11e10 Merge pull request #37 from docker/fix-is-canonical-policy
fix: canonical policy
2024-05-17 09:34:27 -05:00
mrjoelkamp
0020ece3b4 fix: canonical policy 2024-05-17 09:29:06 -05:00
James Carnegie
ec1c994f04 Use id/policy-id in mapping.yaml (#32) 2024-05-16 15:34:19 +01:00
James Carnegie
6ebf042966 Upgrade some deps to fix vulnerabilities (#31) 2024-05-16 15:22:30 +01:00
James Carnegie
a86c8c1209 Use policy files from mapping.yaml (#30)
* Use policy files from mapping.yaml

* Rename location to root in mapping.yaml

* Remove location/root
2024-05-16 14:49:57 +01:00
dependabot[bot]
dd621e2a13 feat(deps): bump github.com/aws/aws-sdk-go-v2/config (#29) 2024-05-16 13:12:49 +00:00
Joel Kamp
b05523e7ea Merge pull request #28 from docker/fix-missing-download-dir
fix: no such directory error
2024-05-15 18:06:19 -05:00
mrjoelkamp
eddb277d7e feat: add tuf download target tests 2024-05-15 16:22:35 -05:00
mrjoelkamp
a103e0e9d7 revert: query 2024-05-15 15:23:22 -05:00
mrjoelkamp
249cf5bcf3 fix: query 2024-05-15 15:21:54 -05:00
mrjoelkamp
33a1996b2b fix: no such directory error 2024-05-15 14:47:20 -05:00
Joel Kamp
1b24098027 Merge pull request #27 from docker/revert-forked-go-tuf
revert: go-tuf fork
2024-05-13 10:02:53 -05:00
mrjoelkamp
64f3c9b149 revert: go-tuf fork 2024-05-13 09:48:04 -05:00
dependabot[bot]
3ee718ee67 feat(deps): bump github.com/aws/aws-sdk-go-v2/config (#26)
Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.27.12 to 1.27.13.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.27.12...config/v1.27.13)

---
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-05-13 09:54:32 +01:00
dependabot[bot]
06947cf992 feat(deps): bump github.com/aws/aws-sdk-go-v2/config (#21)
Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.27.11 to 1.27.12.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.27.11...config/v1.27.12)

---
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-05-10 12:15:15 +01:00
dependabot[bot]
4648680a75 feat(deps): bump github.com/testcontainers/testcontainers-go/modules/registry (#24)
Bumps [github.com/testcontainers/testcontainers-go/modules/registry](https://github.com/testcontainers/testcontainers-go) from 0.30.0 to 0.31.0.
- [Release notes](https://github.com/testcontainers/testcontainers-go/releases)
- [Commits](https://github.com/testcontainers/testcontainers-go/compare/v0.30.0...v0.31.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>
2024-05-10 12:14:52 +01:00
Jonny Stoten
17902c4eb8 Merge pull request #20 from docker/small-tidies
Small tidies
2024-05-08 15:54:31 +01:00
Jonny Stoten
bd6d130e17 Don't use builtin print function 2024-05-08 13:12:40 +01:00
Jonny Stoten
bd849d9b43 Simplify some string concats 2024-05-08 13:09:25 +01:00
Jonny Stoten
8d45522fe8 Use assert.NoError for nil checks on errors 2024-05-08 13:09:25 +01:00
Jonny Stoten
da22f71207 Use maps.Clone from stdlib 2024-05-08 13:09:25 +01:00
Jonny Stoten
c69a9586c5 Remove string contains func (it's in the stdlib) 2024-05-08 13:09:25 +01:00
Jonny Stoten
e3d02ab2e1 Simplify and rename hash functions 2024-05-08 13:09:25 +01:00
Jonny Stoten
d5b059043f Merge pull request #18 from docker/docs--update-examples-in-README.md
docs: update examples in README.md
2024-05-08 13:04:56 +01:00
mrjoelkamp
54996b3c0b docs: pr comments 2024-05-02 16:07:04 -05:00
Joel Kamp
4566ea56b3 Update pkg/attest/example_verify_test.go
Co-authored-by: David Dooling <141646279+whalelines@users.noreply.github.com>
2024-05-02 15:57:27 -05:00
Joel Kamp
20dd9da7c0 Update pkg/attest/example_verify_test.go
Co-authored-by: David Dooling <141646279+whalelines@users.noreply.github.com>
2024-05-02 15:57:19 -05:00
Joel Kamp
3aa738b246 Update pkg/tuf/example_registry_test.go
Co-authored-by: David Dooling <141646279+whalelines@users.noreply.github.com>
2024-05-02 15:57:11 -05:00
Joel Kamp
c99f90cbbf docs: update examples in README.md 2024-05-02 13:49:14 -05:00
mrjoelkamp
3701942bf1 docs: update examples in README.md 2024-05-02 13:35:57 -05:00
James Carnegie
0cadeefe6f Fix query and tests (#17) 2024-05-02 16:03:59 +01:00
James Carnegie
bc7139deaa Move policy mock for external use (#16) 2024-05-02 14:46:21 +01:00
James Carnegie
b461c7f8d8 Revert "revert: rego evaluator result" (#15)
This reverts commit 0126ba9a0b.
2024-05-02 11:36:29 +01:00
81 changed files with 3351 additions and 1440 deletions

128
README.md
View File

@@ -2,129 +2,13 @@
library to create, verify, and evaluate policy for attestations on container images
# usage
## verifying attestations
1. create a TUF client
* using OCI registry for TUF
```go
tufOutputPath = "/.docker/tuf"
metadataURI = "docker/tuf-metadata:latest"
targetsURI = "docker/tuf-targets"
tufClient, err := tuf.NewTufClient(embed.DefaultRoot, tufOutputPath, metadataURI, targetsURI)
```
* using HTTPS for TUF
```go
tufOutputPath = "/.docker/tuf"
metadataURI = "https://docker.github.io/tuf/metadata"
targetsURI = "https://docker.github.io/tuf/targets"
tufClient, err := tuf.NewTufClient(embed.DefaultRoot, tufOutputPath, metadataURI, targetsURI)
```
## signing and verifying attestations
See [example_sign_test.go](./pkg/attest/example_sign_test.go)
1. configure an attestation resolver
* using OCI registry
```go
var resolver oci.AttestationResolver
resolver = &oci.RegistryResolver{
Image: image, // path to image index in OCI registry containing image attestations (e.g. docker/nginx:latest)
Platform: platform, // platform of subject image (image that attestations are being verified against)
}
```
* using local OCI layout
```go
var resolver oci.AttestationResolver
resolver = &oci.OCILayoutResolver{
Path: path, // file path to OCI layout containing image attestations (e.g. /myimage)
Platform: platform, // platform of subject image (image that attestations are being verified against)
}
```
See [example_verify_test.go](./pkg/attest/example_verify_test.go)
1. configure policy options
```go
opts := &policy.PolicyOptions{
TufClient: tufClient,
LocalTargetsDir: "/.docker/policy", // location to store policy files downloaded from TUF
LocalPolicyDir: "", // overrides TUF policy for local policy files
}
```
## mirroring TUF repositories to OCI
See [example_mirror_test.go](./pkg/mirror/example_mirror_test.go)
1. verify attestations
```go
policy, err := attest.Verify(ctx, opts, resolver)
if err != nil {
return false // failed policy or attestation signature verification
}
if policy {
return true // passed policy
}
return true // no policy for image
```
## signing attestations
1. generate an image with intoto Statements (optional)
```sh
docker buildx build <PATH TO DOCKERFILE> --sbom true --provenance true --output type=oci,tar=false,name=<REPO>:<TAG>,dest=<OUTPUT DIR>
```
1. confgiure a `dsse.SignerVerifier`
```go
var signer dsse.SignerVerifier
signer, err = signerverifier.GetAWSSigner(cmd.Context(), aws_arn, aws_region)
```
1. configure signing options
```go
opts := &attest.SigningOptions{
Replace: true, // replace unsigned intoto statements with signed intoto attestations, otherwise leave in place
}
```
* add [Verification Summary Attestation (VSA)](https://slsa.dev/spec/v1.0/verification_summary) for all intoto attestations (optional)
```go
opts.VSAOptions = &attestation.VSAOptions{
BuildLevel: "SLSA_BUILD_LEVEL_" + slsaBuildLevel,
PolicyURI: slsaPolicyUri,
VerifierID: slsaVerifierId,
}
```
1. load attestations
* oci registry
```go
ref := "docker/attest:latest"
att, err := oci.AttestationIndexFromRemote(ref)
```
* local filepath
```go
path := "/test-image"
att, err := oci.AttestationIndexFromPath(path)
```
1. sign attestations
```go
signedImageIndex, err := attest.Sign(ctx, att, signer, opts)
```
`attest.Sign()` iterates over attestation manifests in the image index and signs all intoto statements (optionally generates a VSA), returning a mutated ImageIndex with all intoto statements signed as attestations.
1. save output (optional)
* push to oci registry
```go
err = mirror.PushToRegistry(signedImageIndex, ref)
```
* save to local filesystem
```go
idx := v1.ImageIndex(empty.Index)
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
Add: signedImageIndex,
Descriptor: v1.Descriptor{
Annotations: map[string]string{
oci.OciReferenceTarget: att.Name,
},
},
})
err = mirror.SaveAsOCILayout(idx, path)
```
## mirroring TUF repositories
TODO: write content for this outline
### mirroring TUF metadata to OCI
#### delegated metadata
### mirroring TUF targets to OCI
#### delegated targets
### using `go-tuf` OCI registry client
See [example_registry_test.go](./pkg/tuf/example_registry_test.go)

126
go.mod
View File

@@ -3,68 +3,70 @@ module github.com/docker/attest
go 1.22.1
require (
github.com/aws/aws-sdk-go-v2/config v1.27.11
github.com/containerd/containerd v1.7.16
github.com/Masterminds/semver/v3 v3.2.1
github.com/aws/aws-sdk-go-v2/config v1.27.19
github.com/containerd/containerd v1.7.18
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.19.1
github.com/google/go-containerregistry v0.19.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.64.1
github.com/open-policy-agent/opa v0.65.0
github.com/opencontainers/image-spec v1.1.0
github.com/package-url/packageurl-go v0.1.2
github.com/package-url/packageurl-go v0.1.3
github.com/pkg/errors v0.9.1
github.com/secure-systems-lab/go-securesystemslib v0.8.0
github.com/sigstore/cosign/v2 v2.2.4
github.com/sigstore/rekor v1.3.6
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.4
github.com/stretchr/testify v1.9.0
github.com/testcontainers/testcontainers-go v0.30.0
github.com/testcontainers/testcontainers-go/modules/registry v0.30.0
github.com/theupdateframework/go-tuf/v2 v2.0.0-20240402164131-b2e024ad4752
github.com/testcontainers/testcontainers-go v0.31.0
github.com/testcontainers/testcontainers-go/modules/registry v0.31.0
github.com/theupdateframework/go-tuf/v2 v2.0.0-20240504210453-5a634eb214ae // for https://github.com/theupdateframework/go-tuf/pull/632
gopkg.in/yaml.v3 v3.0.1
sigs.k8s.io/yaml v1.4.0
)
replace github.com/theupdateframework/go-tuf/v2 => github.com/mrjoelkamp/go-tuf/v2 v2.0.1 // for https://github.com/theupdateframework/go-tuf/pull/632
require (
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.1 // indirect
github.com/Microsoft/hcsshim v0.11.4 // 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.26.1 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.11 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
github.com/aws/aws-sdk-go-v2 v1.28.0 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.19 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.30.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.12 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.32.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.20.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.28.13 // indirect
github.com/aws/smithy-go v1.20.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // 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/errdefs v0.1.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
github.com/cpuguy83/dockercfg v0.3.1 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20231011164504-785e29786b46 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f // indirect
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 v24.0.7+incompatible // indirect
github.com/docker/cli v26.1.3+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v25.0.5+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.0 // indirect
github.com/docker/docker v26.1.3+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
github.com/dustin/go-humanize v1.0.1 // indirect
@@ -72,9 +74,10 @@ require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-chi/chi v4.1.2+incompatible // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/analysis v0.23.0 // indirect
github.com/go-openapi/errors v0.22.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
@@ -89,19 +92,20 @@ require (
github.com/google/certificate-transparency-go v1.1.8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
github.com/hashicorp/go-retryablehttp v0.7.6 // 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/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/letsencrypt/boulder v0.0.0-20231026200631-000cd05d5491 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/letsencrypt/boulder v0.0.0-20240515153123-6ae6aa8e9055 // indirect
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
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
@@ -111,19 +115,19 @@ require (
github.com/oklog/ulid v1.3.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_golang v1.19.0 // 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_model v0.6.1 // indirect
github.com/prometheus/common v0.51.1 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/prometheus/common v0.53.0 // indirect
github.com/prometheus/procfs v0.15.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sassoftware/relic v7.2.1+incompatible // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // 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.3 // indirect
github.com/sigstore/timestamp-authority v1.2.2 // indirect
@@ -139,34 +143,32 @@ require (
github.com/tchap/go-patricia/v2 v2.3.1 // 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.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/transparency-dev/merkle v0.0.2 // indirect
github.com/vbatts/tar-split v0.11.5 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/yashtewari/glob-intersection v0.2.0 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.mongodb.org/mongo-driver v1.14.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver v1.15.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect
go.opentelemetry.io/otel v1.26.0 // indirect
go.opentelemetry.io/otel/metric v1.26.0 // indirect
go.opentelemetry.io/otel/sdk v1.26.0 // indirect
go.opentelemetry.io/otel/trace v1.26.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/term v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.19.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
google.golang.org/grpc v1.63.2 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect
google.golang.org/grpc v1.64.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.120.1 // indirect

318
go.sum
View File

@@ -1,6 +1,6 @@
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
cloud.google.com/go/compute v1.25.0 h1:H1/4SqSUhjPFE7L5ddzHOfY2bCAvjwNRZPNl6Ni5oYU=
cloud.google.com/go/compute v1.25.0/go.mod h1:GR7F0ZPZH8EhChlMo9FkLd7eUTwEymjqQagxzilIxIE=
cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc=
@@ -53,14 +53,16 @@ github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUM
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
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/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=
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/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 v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ThalesIgnite/crypto11 v1.2.5 h1:1IiIIEqYmBvUYFeMnHqRft4bwf/O36jryEUpY+9ef8E=
github.com/ThalesIgnite/crypto11 v1.2.5/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE=
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
@@ -93,20 +95,20 @@ 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.51.6 h1:Ld36dn9r7P9IjU8WZSaswQ8Y/XUCRpewim5980DwYiU=
github.com/aws/aws-sdk-go v1.51.6/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA=
github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
github.com/aws/aws-sdk-go-v2/config v1.27.11 h1:f47rANd2LQEYHda2ddSCKYId18/8BhSRM4BULGmfgNA=
github.com/aws/aws-sdk-go-v2/config v1.27.11/go.mod h1:SMsV78RIOYdve1vf36z8LmnszlRWkwMQtomCAI0/mIE=
github.com/aws/aws-sdk-go-v2/credentials v1.17.11 h1:YuIB1dJNf1Re822rriUOTxopaHHvIq0l/pX3fwO+Tzs=
github.com/aws/aws-sdk-go-v2/credentials v1.17.11/go.mod h1:AQtFPsDH9bI2O+71anW6EKL+NcD7LG3dpKGMV4SShgo=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc=
github.com/aws/aws-sdk-go v1.53.10 h1:3enP5l5WtezT9Ql+XZqs56JBf5YUd/FEzTCg///OIGY=
github.com/aws/aws-sdk-go v1.53.10/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go-v2 v1.28.0 h1:ne6ftNhY0lUvlazMUQF15FF6NH80wKmPRFG7g2q6TCw=
github.com/aws/aws-sdk-go-v2 v1.28.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
github.com/aws/aws-sdk-go-v2/config v1.27.19 h1:+DBS8gJP6VsxYkZ6UEV0/VsRM2rYpbQCYsosW9RRmeQ=
github.com/aws/aws-sdk-go-v2/config v1.27.19/go.mod h1:KzZcioJWzy9oV+oS5CobYXlDtU9+eW7bPG1g7gizTW4=
github.com/aws/aws-sdk-go-v2/credentials v1.17.19 h1:R18G7nBBGLby51CFEqUBFF2IVl7LUdCtYj6iosUwh/0=
github.com/aws/aws-sdk-go-v2/credentials v1.17.19/go.mod h1:xr9kUMnaLTB866HItT6pg58JgiBP77fSQLBwIa//zk8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.6 h1:vVOuhRyslJ6T/HteG71ZWCTas1q2w6f0NKsNbkXHs/A=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.6/go.mod h1:jimWaqLiT0sJGLh51dKCLLtExRYPtMU7MpxuCgtbkxg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.10 h1:LZIUb8sQG2cb89QaVFtMSnER10gyKkqU1k3hP3g9das=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.10/go.mod h1:BRIqay//vnIOCZjoXWSLffL2uzbtxEmnSlfbvVh7Z/4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.10 h1:HY7CXLA0GiQUo3WYxOP7WYkLcwvRX4cLPf5joUcrQGk=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.10/go.mod h1:kfRBSxRa+I+VyON7el3wLZdrO91oxUxEwdAaWgFqN90=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/service/ecr v1.20.2 h1:y6LX9GUoEA3mO0qpFl1ZQHj1rFyPWVphlzebiSt2tKE=
@@ -115,16 +117,16 @@ github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.18.2 h1:PpbXaecV3sLAS6rjQiaKw4
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.18.2/go.mod h1:fUHpGXr4DrXkEDpGAjClPsviWf+Bszeb0daKE0blxv8=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk=
github.com/aws/aws-sdk-go-v2/service/kms v1.30.0 h1:yS0JkEdV6h9JOo8sy2JSpjX+i7vsKifU8SIeHrqiDhU=
github.com/aws/aws-sdk-go-v2/service/kms v1.30.0/go.mod h1:+I8VUUSVD4p5ISQtzpgSva4I8cJ4SQ4b1dcBcof7O+g=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 h1:vN8hEbpRnL7+Hopy9dzmRle1xmDc7o8tmY0klsr175w=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.5/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 h1:Jux+gDDyi1Lruk+KHF91tK2KCuY61kzoCpvtvJJBtOE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 h1:cwIxeBttqPN3qkaAjcEcsh8NYr8n2HZPkcKgPAi1phU=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.6/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.12 h1:kO2J7WMroF/OTHN9WTcUtMjPhJ7ZoNxx0dwv6UCXQgY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.12/go.mod h1:mrNxrjYvXaSjZe5fkKaWgDnOQ6BExLn/7Ru9OpRsMPY=
github.com/aws/aws-sdk-go-v2/service/kms v1.32.1 h1:FARrQLRQXpCFYylIUVF1dRij6YbPCmtwudq9NBk4kFc=
github.com/aws/aws-sdk-go-v2/service/kms v1.32.1/go.mod h1:8lETO9lelSG2B6KMXFh2OwPPqGV6WQM3RqLAEjP1xaU=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.12 h1:FsYii6U+2k8ynYBo+pywlCBY9HNAFRh+iICRHbn+Qyw=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.12/go.mod h1:j9Rps+Lcs2A0tYypWsNBeJOjgsIYUf1Styppo9Es0Wo=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.6 h1:lEE+xEcq3lh9bk362tgErP1+n689q5ERdmTwmF1XT3M=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.6/go.mod h1:2tR0x1DCL5IgnVZ1NQNFDNg5/XL/kiQgWI5l7I/N5Js=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.13 h1:TSzmuUeruVJ4XWYp3bYzKCXue70ECpJWmbP3UfEvhYY=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.13/go.mod h1:FppRtFjBA9mSWTj2cIAWCP66+bbBPMuPpBfWRXC5Yi0=
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8 h1:SoFYaT9UyGkR0+nogNyD/Lj+bsixB+SNuAS4ABlEs6M=
@@ -139,16 +141,17 @@ github.com/buildkite/go-pipeline v0.3.2 h1:SW4EaXNwfjow7xDRPGgX0Rcx+dPj5C1kV9LKC
github.com/buildkite/go-pipeline v0.3.2/go.mod h1:iY5jzs3Afc8yHg6KDUcu3EJVkfaUkd9x/v/OH98qyUA=
github.com/buildkite/interpolate v0.0.0-20200526001904-07f35b4ae251 h1:k6UDF1uPYOs0iy1HPeotNa155qXRWrzKnqAaGXHLZCE=
github.com/buildkite/interpolate v0.0.0-20200526001904-07f35b4ae251/go.mod h1:gbPR1gPu9dB96mucYIR7T3B7p/78hRVSOuzIWLHK2Y4=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA=
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q=
github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 h1:krfRl01rzPzxSxyLyrChD+U+MzsBXbm0OwYYB67uF+4=
github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589/go.mod h1:OuDyvmLnMCwa2ep4Jkm6nyA0ocJuZlGyk2gGseVzERM=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -156,20 +159,23 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI=
github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg=
github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
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.16 h1:7Zsfe8Fkj4Wi2My6DXGQ87hiqIrmOXolm72ZEkFU5Mg=
github.com/containerd/containerd v1.7.16/go.mod h1:NL49g7A/Fui7ccmxV6zkBWwqMgmMxFWzujYCc+JLt7k=
github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao=
github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4=
github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM=
github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0=
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/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E=
@@ -177,10 +183,10 @@ github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHf
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyberphone/json-canonicalization v0.0.0-20231011164504-785e29786b46 h1:2Dx4IHfC1yHWI12AxQDJM1QbRCDfk6M+blLzlZCXdrc=
github.com/cyberphone/json-canonicalization v0.0.0-20231011164504-785e29786b46/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE=
github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec=
github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f h1:eHnXnuK47UlSTOQexbzxAZfekVz6i+LKRdj1CU5DPaM=
github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs=
github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -200,14 +206,14 @@ 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 v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg=
github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
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/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 v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE=
github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8=
github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40=
github.com/docker/docker v26.1.3+incompatible h1:lLCzRbrVZrljpVNobJu1J2FHk8V0s4BawoZippkc+xo=
github.com/docker/docker v26.1.3+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-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -218,8 +224,8 @@ github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxER
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emicklei/proto v1.12.1 h1:6n/Z2pZAnBwuhU66Gs8160B8rrrYKo7h2F2sCOnNceE=
github.com/emicklei/proto v1.12.1/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
@@ -242,12 +248,13 @@ github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQr
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
@@ -314,8 +321,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY=
github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
github.com/google/go-containerregistry v0.19.2 h1:TannFKE1QSajsP6hPWb5oJNgKe1IKjHukIKDUmvsV6w=
github.com/google/go-containerregistry v0.19.2/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
github.com/google/go-github/v55 v55.0.0 h1:4pp/1tNMB9X/LuAhs5i0KQAE40NmiR/y6prLNb9x9cg=
github.com/google/go-github/v55 v55.0.0/go.mod h1:JLahOTA1DnXzhxEymmFF5PP2tSS9JVNj68mSZNDwskA=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
@@ -344,13 +351,12 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/hashicorp/go-retryablehttp v0.7.6 h1:TwRYfx2z2C4cLbXmT8I5PgP/xmuqASDyiVuGYfs9GZM=
github.com/hashicorp/go-retryablehttp v0.7.6/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs=
@@ -387,28 +393,29 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
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.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/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=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/letsencrypt/boulder v0.0.0-20231026200631-000cd05d5491 h1:WGrKdjHtWC67RX96eTkYD2f53NDHhrq/7robWTAfk4s=
github.com/letsencrypt/boulder v0.0.0-20231026200631-000cd05d5491/go.mod h1:o158RFmdEbYyIZmXAbrvmJWesbyxlLKee6X64VPVuOc=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/letsencrypt/boulder v0.0.0-20240515153123-6ae6aa8e9055 h1:sl8s8GXv/oHUSid9gd4B+Rovu9DOW4PxnKT2rNRfmzM=
github.com/letsencrypt/boulder v0.0.0-20240515153123-6ae6aa8e9055/go.mod h1:wGJPvcZTEexA3UpMx+4cZ19nk6gRrzrdW4jFEPsEqf0=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI=
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -417,6 +424,8 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
@@ -433,15 +442,14 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mozillazg/docker-credential-acr-helper v0.3.0 h1:DVWFZ3/O8BP6Ue3iS/Olw+G07u1hCq1EOVCDZZjCIBI=
github.com/mozillazg/docker-credential-acr-helper v0.3.0/go.mod h1:cZlu3tof523ujmLuiNUb6JsjtHcNA70u1jitrrdnuyA=
github.com/mrjoelkamp/go-tuf/v2 v2.0.1 h1:nDJGPlrU05sirPlA16M1XJiGDqM0zMwguA4cVgCJ9YY=
github.com/mrjoelkamp/go-tuf/v2 v2.0.1/go.mod h1:LJo5jrV0LYV0jVSbCjPem6+0zrkPz8FnimzIECzsFDY=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 h1:Up6+btDp321ZG5/zdSLo48H9Iaq0UQGthrhWC6pCxzE=
github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481/go.mod h1:yKZQO8QE2bHlgozqWDiRVqTFlLQSj30K/6SAK8EeYFw=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/oleiade/reflections v1.0.1 h1:D1XO3LVEYroYskEsoSiGItp9RUxG6jWnCVvrqH0HHQM=
@@ -457,20 +465,20 @@ 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.64.1 h1:n8IJTYlFWzqiOYx+JiawbErVxiqAyXohovcZxYbskxQ=
github.com/open-policy-agent/opa v0.64.1/go.mod h1:j4VeLorVpKipnkQ2TDjWshEuV3cvP/rHzQhYaraUXZY=
github.com/open-policy-agent/opa v0.65.0 h1:wnEU0pEk80YjFi3yoDbFTMluyNssgPI4VJNJetD9a4U=
github.com/open-policy-agent/opa v0.65.0/go.mod h1:CNoLL44LuCH1Yot/zoeZXRKFylQtCJV+oGFiP2TeeEc=
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=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/package-url/packageurl-go v0.1.2 h1:0H2DQt6DHd/NeRlVwW4EZ4oEI6Bn40XlNPRqegcxuo4=
github.com/package-url/packageurl-go v0.1.2/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c=
github.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoXLtmE3I0PLs=
github.com/package-url/packageurl-go v0.1.3/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0=
github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -478,16 +486,17 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
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_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.51.1 h1:eIjN50Bwglz6a/c3hAgSMcofL3nD+nFQkV6Dd4DsQCw=
github.com/prometheus/common v0.51.1/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek=
github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk=
github.com/protocolbuffers/txtpbfmt v0.0.0-20231025115547-084445ff1adf h1:014O62zIzQwvoD7Ekj3ePDF5bv9Xxy0w6AZk0qYbjUk=
github.com/protocolbuffers/txtpbfmt v0.0.0-20231025115547-084445ff1adf/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
@@ -511,8 +520,8 @@ github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
@@ -525,8 +534,8 @@ 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.3 h1:G7LVXqL+ekgYtYdksBks9B38dPoIsbscjQJX/MGWkA4=
github.com/sigstore/sigstore v1.8.3/go.mod h1:mqbTEariiGA94cn6G3xnDiV6BD8eSLdL/eA7bvJ0fVs=
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3 h1:LTfPadUAo+PDRUbbdqbeSl2OuoFQwUFTnJ4stu+nwWw=
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3/go.mod h1:QV/Lxlxm0POyhfyBtIbTWxNeF18clMlkkyL9mu45y18=
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.4 h1:okxaVlaTrQowE1FA4UQ3rw54f7BUjdnzERIxbZTBZuc=
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.4/go.mod h1:jkcPErmnCECuSJajUaUq5pwCMOeBF19VzQo6bv4l1D0=
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3 h1:xgbPRCr2npmmsuVVteJqi/ERw9+I13Wou7kq0Yk4D8g=
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3/go.mod h1:G4+I83FILPX6MtnoaUdmv/bRGEVtR3JdLeJa/kXdk/0=
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.3 h1:vDl2fqPT0h3D/k6NZPlqnKFd1tz3335wm39qjvpZNJc=
@@ -556,7 +565,7 @@ github.com/spiffe/go-spiffe/v2 v2.2.0/go.mod h1:Urzb779b3+IwDJD2ZbN8fVl3Aa8G4N/P
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -572,22 +581,26 @@ 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.30.0 h1:jmn/XS22q4YRrcMwWg0pAwlClzs/abopbsBzrepyc4E=
github.com/testcontainers/testcontainers-go v0.30.0/go.mod h1:K+kHNGiM5zjklKjgTtcrEetF3uhWbMUyqAQoyoh8Pf0=
github.com/testcontainers/testcontainers-go/modules/registry v0.30.0 h1:/GYaNnQ09Gvwv3GvhWYbzL2gQiqwzlqDyQZ175uVPC4=
github.com/testcontainers/testcontainers-go/modules/registry v0.30.0/go.mod h1:bu2AS7kGxJQgZ16qbb5SHKSuEVrriENPIpKugl0aCHA=
github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jXjVCA27AkmbnZ+UTh3U=
github.com/testcontainers/testcontainers-go v0.31.0/go.mod h1:D2lAoA0zUFiSY+eAflqK5mcUx/A5hrrORaEQrd0SefI=
github.com/testcontainers/testcontainers-go/modules/registry v0.31.0 h1:QiQb8omImfD5ZWSh0YR0WNrFeRU+j2Cqfd8+dYdLgaE=
github.com/testcontainers/testcontainers-go/modules/registry v0.31.0/go.mod h1:rrkCrh2acVVbQw9JfN4DOBm/ODVCIHbveEq+k+HSyfU=
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=
github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug=
github.com/theupdateframework/go-tuf/v2 v2.0.0-20240504210453-5a634eb214ae h1:Cb5/8rY0k9oB+SigleRtEP5BeQ3PZQGX051cFIyBNaM=
github.com/theupdateframework/go-tuf/v2 v2.0.0-20240504210453-5a634eb214ae/go.mod h1:LJo5jrV0LYV0jVSbCjPem6+0zrkPz8FnimzIECzsFDY=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4=
github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A=
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
@@ -602,34 +615,35 @@ github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBe
github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms=
github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc=
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.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0=
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.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8=
go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs=
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.step.sm/crypto v0.44.2 h1:t3p3uQ7raP2jp2ha9P6xkQF85TJZh+87xmjSLaib+jk=
@@ -643,31 +657,43 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w=
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
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.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
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=
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.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
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.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -690,21 +716,33 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.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.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
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.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
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=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -712,8 +750,10 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
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.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
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=
@@ -723,12 +763,12 @@ google.golang.org/api v0.172.0 h1:/1OcMZGPmW1rX2LCu2CmGUD1KXK1+pfzxotxyRUCCdk=
google.golang.org/api v0.172.0/go.mod h1:+fJZq6QXWfa9pXhnIzsjx4yI22d4aI9ZpLb58gvXjis=
google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7 h1:ImUcDPHjTrAqNhlOkSocDLfG9rrNHH7w7uoKWPaWZ8s=
google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7/go.mod h1:/3XmxOjePkvmKrHuBy4zNFw7IzxJXtAgdpXi8Ll990U=
google.golang.org/genproto/googleapis/api v0.0.0-20240311173647-c811ad7063a7 h1:oqta3O3AnlWbmIE3bFnWbu4bRxZjfbWCp0cKSuZh01E=
google.golang.org/genproto/googleapis/api v0.0.0-20240311173647-c811ad7063a7/go.mod h1:VQW3tUculP/D4B+xVCo+VgSq8As6wA9ZjHl//pmk+6s=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 h1:AgADTJarZTBqgjiUzRgfaBchgYB3/WFTC80GPwsMcRI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
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=
@@ -737,14 +777,12 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs=
gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=

View File

@@ -6,11 +6,11 @@ import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
"github.com/docker/attest/pkg/signerverifier"
"github.com/docker/attest/pkg/tlog"
@@ -58,7 +58,7 @@ func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) {
var policyEvaluator policy.PolicyEvaluator
if USE_MOCK_POLICY {
policyEvaluator = GetMockPolicy()
policyEvaluator = policy.GetMockPolicy()
} else {
policyEvaluator = policy.NewRegoEvaluator(true)
}
@@ -83,26 +83,11 @@ func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) {
}
func GetMockSigner(ctx context.Context) (dsse.SignerVerifier, error) {
return signerverifier.GenKeyPair()
}
type MockPolicyEvaluator struct {
EvaluateFunc func(ctx context.Context, resolver oci.AttestationResolver, policy []*policy.PolicyFile, input *policy.PolicyInput) error
}
func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, policy []*policy.PolicyFile, input *policy.PolicyInput) error {
if pe.EvaluateFunc != nil {
return pe.EvaluateFunc(ctx, resolver, policy, input)
}
return nil
}
func GetMockPolicy() policy.PolicyEvaluator {
return &MockPolicyEvaluator{
EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, policy []*policy.PolicyFile, input *policy.PolicyInput) error {
return nil
},
priv, err := os.ReadFile(filepath.Join("..", "..", "test", "testdata", "test-signing-key.pem"))
if err != nil {
return nil, err
}
return signerverifier.LoadKeyPair(priv)
}
type AnnotatedStatement struct {
@@ -111,23 +96,8 @@ type AnnotatedStatement struct {
Annotations map[string]string
}
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)
}
mfs2, err := mfs.IndexManifest()
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)
}
@@ -135,11 +105,11 @@ func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStat
var statements []*AnnotatedStatement
for _, mf := range mfs2.Manifests {
if mf.Annotations["vnd.docker.reference.type"] != "attestation-manifest" {
if mf.Annotations[attestation.DockerReferenceType] != "attestation-manifest" {
continue
}
attestationImage, err := mfs.Image(mf.Digest)
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)
}
@@ -204,3 +174,22 @@ func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStat
}
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

@@ -5,14 +5,11 @@ import (
"encoding/hex"
)
func HexHashBytes(input []byte) string {
s256 := sha256.New()
s256.Write(input)
hashSum := s256.Sum(nil)
return hex.EncodeToString(hashSum)
func SHA256Hex(input []byte) string {
return hex.EncodeToString(SHA256(input))
}
func S256(data []byte) []byte {
func SHA256(data []byte) []byte {
h := sha256.Sum256(data)
return h[:]
}

View File

@@ -1,10 +0,0 @@
package util
func StringInSlice(str string, list []string) bool {
for _, v := range list {
if v == str {
return true
}
}
return false
}

View File

@@ -0,0 +1,69 @@
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"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
)
func ExampleSign_remote() {
// configure signerverifier
// local signer (unsafe for production)
signer, err := signerverifier.GenKeyPair()
if err != nil {
panic(err)
}
// example using AWS KMS signer
// aws_arn := "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012"
// aws_region := "us-west-2"
// signer, err := signerverifier.GetAWSSigner(cmd.Context(), aws_arn, aws_region)
// configure signing options
opts := &attestation.SigningOptions{
Replace: true, // replace unsigned intoto statements with signed intoto attestations, otherwise leave in place
}
// load image index with unsigned attestation-manifests
ref := "docker/image-signer-verifier:latest"
att, err := oci.SubjectIndexFromRemote(ref)
if err != nil {
panic(err)
}
// example for local image index
// path := "/myimage"
// att, err := oci.AttestationIndexFromLocal(path)
// sign attestations
signedImageIndex, err := attest.Sign(context.Background(), att.Index, signer, opts)
if err != nil {
panic(err)
}
// push image index with signed attestation-manifests
err = mirror.PushIndexToRegistry(signedImageIndex, ref)
if err != nil {
panic(err)
}
// output image index to filesystem (optional)
path := "/myimage"
idx := v1.ImageIndex(empty.Index)
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
Add: signedImageIndex,
Descriptor: v1.Descriptor{
Annotations: map[string]string{
oci.OciReferenceTarget: att.Name,
},
},
})
err = mirror.SaveIndexAsOCILayout(idx, path)
if err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,68 @@
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.StagingRoot, 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
}
// verify attestations
src, err := oci.ParseImageSpec(image, oci.WithPlatform(platform))
if err != nil {
panic(err)
}
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

@@ -6,6 +6,7 @@ import (
"fmt"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/oci"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/match"
@@ -17,56 +18,147 @@ import (
"github.com/secure-systems-lab/go-securesystemslib/dsse"
)
func Sign(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *SigningOptions) (v1.ImageIndex, error) {
func Sign(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) (v1.ImageIndex, error) {
images, err := SignedAttestationImages(ctx, idx, signer, opts)
if err != nil {
return nil, fmt.Errorf("failed to sign attestation images: %w", err)
}
for _, image := range images {
idx, err = addImageToIndex(idx, image.Image, image.Descriptor, image.AttestationManifest)
if err != nil {
return nil, fmt.Errorf("failed to add signed layers to index: %w", err)
}
}
return idx, nil
}
func SignedAttestationImages(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) ([]*attestation.SignedAttestationImage, error) {
// extract attestation manifests from index
attestationManifests, err := attestation.GetAttestationManifestsFromIndex(idx)
if err != nil {
return nil, fmt.Errorf("failed to get attestation manifests: %w", err)
}
if len(attestationManifests) == 0 {
return nil, fmt.Errorf("no attestation manifests found")
}
images := []*attestation.SignedAttestationImage{}
// sign every attestation layer in each manifest
for _, manifest := range attestationManifests {
attestationLayers, err := attestation.GetAttestationsFromImage(manifest.Attestation.Image)
newImg, newDescriptor, err := SignLayersAndAddToImage(ctx, manifest.Attestation.Layers, manifest, signer, opts)
if err != nil {
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
return nil, fmt.Errorf("failed to add signed layers to image: %w", err)
}
signedLayers, err := signLayers(ctx, attestationLayers, signer)
if err != nil {
return nil, fmt.Errorf("failed to sign attestations: %w", err)
}
if opts.VSAOptions != nil {
newLayer, err := generateVSA(ctx, manifest, signer, opts)
if err != nil {
return nil, fmt.Errorf("failed to generate VSA: %w", err)
}
signedLayers = append(signedLayers, *newLayer)
}
newImg, err := addSignedLayers(signedLayers, manifest, opts)
if err != nil {
return nil, fmt.Errorf("failed to add signed layers: %w", err)
}
newDesc, err := partial.Descriptor(newImg)
if err != nil {
return nil, fmt.Errorf("failed to get descriptor: %w", err)
}
cf, err := manifest.Attestation.Image.ConfigFile()
if err != nil {
return nil, fmt.Errorf("failed to get config file: %w", err)
}
newDesc.Platform = cf.Platform()
newDesc.MediaType = manifest.MediaType
newDesc.Annotations = manifest.Annotations
idx = mutate.RemoveManifests(idx, match.Digests(manifest.Digest))
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
Add: newImg,
Descriptor: *newDesc,
images = append(images, &attestation.SignedAttestationImage{
Image: newImg,
Descriptor: newDescriptor,
AttestationManifest: manifest,
})
}
return images, nil
}
func AddAttestation(ctx context.Context, idx v1.ImageIndex, statement *intoto.Statement, signer dsse.SignerVerifier) (v1.ImageIndex, error) {
if len(statement.Subject) == 0 {
return nil, fmt.Errorf("statement has no subjects")
}
subjectDigests := make(map[string]bool)
for _, subject := range statement.Subject {
subjectDigest := fmt.Sprintf("sha256:%s", subject.Digest["sha256"])
subjectDigests[subjectDigest] = true
}
attestationManifests, err := attestation.GetAttestationManifestsFromIndex(idx)
if err != nil {
return nil, fmt.Errorf("failed to get attestation manifests: %w", err)
}
updatedIndex := false
for _, manifest := range attestationManifests {
if subjectDigests[manifest.Annotations[attestation.DockerReferenceDigest]] {
attestationLayers := []attestation.AttestationLayer{
{
Statement: statement,
MediaType: types.MediaType(intoto.PayloadType),
Annotations: map[string]string{
oci.InTotoPredicateType: statement.PredicateType,
},
},
}
// hard-coding replace to false here, because if it's true we will remove any unsigned statements, even unrelated ones
newImg, newDec, err := SignLayersAndAddToImage(ctx, attestationLayers, manifest, signer, &attestation.SigningOptions{Replace: false})
if err != nil {
return nil, fmt.Errorf("failed to add signed layers to image: %w", err)
}
idx, err = addImageToIndex(idx, newImg, newDec, manifest)
if err != nil {
return nil, fmt.Errorf("failed to add attestation image to index: %w", err)
}
updatedIndex = true
}
}
if !updatedIndex {
return nil, fmt.Errorf("no attestation manifest found for statement")
}
return idx, nil
}
func SignLayersAndAddToImage(
ctx context.Context,
attestationLayers []attestation.AttestationLayer,
manifest attestation.AttestationManifest,
signer dsse.SignerVerifier,
opts *attestation.SigningOptions) (v1.Image, *v1.Descriptor, error) {
signedLayers, err := signLayers(ctx, attestationLayers, signer, opts)
if err != nil {
return nil, nil, fmt.Errorf("failed to sign attestations: %w", err)
}
newImg, err := addSignedLayers(signedLayers, manifest, opts)
if err != nil {
return nil, nil, fmt.Errorf("failed to add signed layers: %w", err)
}
if !opts.SkipSubject {
newImg = mutate.Subject(newImg, *manifest.SubjectDescriptor).(v1.Image)
}
newDesc, err := partial.Descriptor(newImg)
if err != nil {
return nil, nil, fmt.Errorf("failed to get descriptor: %w", err)
}
cf, err := manifest.Attestation.Image.ConfigFile()
if err != nil {
return nil, nil, fmt.Errorf("failed to get config file: %w", err)
}
newDesc.Platform = cf.Platform()
if newDesc.Platform == nil {
newDesc.Platform = &v1.Platform{
Architecture: "unknown",
OS: "unknown",
}
}
newDesc.MediaType = manifest.MediaType
newDesc.Annotations = manifest.Annotations
return newImg, newDesc, nil
}
func addImageToIndex(
idx v1.ImageIndex,
img v1.Image,
desc *v1.Descriptor,
manifest attestation.AttestationManifest,
) (v1.ImageIndex, error) {
idx = mutate.RemoveManifests(idx, match.Digests(manifest.Digest))
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
Add: img,
Descriptor: *desc,
})
return idx, nil
}
// signLayers signs each intoto attestation layer with the given signer
func signLayers(ctx context.Context, layers []attestation.AttestationLayer, signer dsse.SignerVerifier) ([]mutate.Addendum, error) {
func signLayers(ctx context.Context, layers []attestation.AttestationLayer, signer dsse.SignerVerifier, opts *attestation.SigningOptions) ([]mutate.Addendum, error) {
var signedLayers []mutate.Addendum
for _, layer := range layers {
// only sign intoto layers
@@ -77,14 +169,11 @@ func signLayers(ctx context.Context, layers []attestation.AttestationLayer, sign
layer.Annotations[InTotoReferenceLifecycleStage] = LifecycleStageExperimental
// sign the statement
payload, err := json.Marshal(layer.Statement)
if err != nil {
return nil, fmt.Errorf("failed to marshal statement: %w", err)
}
env, err := attestation.SignDSSE(ctx, payload, intoto.PayloadType, signer)
env, err := signInTotoStatement(ctx, layer.Statement, signer, opts)
if err != nil {
return nil, fmt.Errorf("failed to sign statement: %w", err)
}
mediaType, err := attestation.DSSEMediaType(layer.Statement.PredicateType)
if err != nil {
return nil, fmt.Errorf("failed to get DSSE media type: %w", err)
@@ -103,8 +192,27 @@ func signLayers(ctx context.Context, layers []attestation.AttestationLayer, sign
return signedLayers, nil
}
func signInTotoStatement(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *attestation.SigningOptions) (*attestation.Envelope, error) {
payload, err := json.Marshal(statement)
if err != nil {
return nil, fmt.Errorf("failed to marshal statement: %w", err)
}
env, err := attestation.SignDSSE(ctx, payload, signer, opts)
if err != nil {
return nil, fmt.Errorf("failed to sign statement: %w", err)
}
return env, nil
}
// addSignedLayers adds signed layers to a new or existing attestation image
func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.AttestationManifest, opts *SigningOptions) (v1.Image, error) {
func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.AttestationManifest, opts *attestation.SigningOptions) (v1.Image, error) {
withAnnotations := func(img v1.Image) v1.Image {
// this is handy when dealing with referrers
return mutate.Annotations(img, map[string]string{
attestation.DockerReferenceType: attestation.AttestationManifestType,
attestation.DockerReferenceDigest: manifest.SubjectDescriptor.Digest.String(),
}).(v1.Image)
}
var err error
if opts.Replace {
// create a new attestation image with only signed layers
@@ -126,7 +234,7 @@ func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.Attest
}
}
}
return newImg, nil
return withAnnotations(newImg), nil
}
// Add signed layers to the existing image
for _, layer := range signedLayers {
@@ -135,5 +243,5 @@ func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.Attest
return nil, fmt.Errorf("failed to append layer: %w", err)
}
}
return manifest.Attestation.Image, nil
return withAnnotations(manifest.Attestation.Image), nil
}

View File

@@ -2,6 +2,7 @@ package attest
import (
"encoding/json"
"fmt"
"path/filepath"
"testing"
@@ -18,12 +19,15 @@ import (
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")
LocalPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy")
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
TestTempDir = "attest-sign-test"
)
@@ -38,31 +42,25 @@ func TestSignVerifyOCILayout(t *testing.T) {
replace bool
}{
{"signed replaced (does nothing)", UnsignedTestImage, 0, 6, true},
{"without replace", UnsignedTestImage, 4, 6, false},
{"signed replaced", UnsignedTestImage, 0, 4, true},
{"without replace", UnsignedTestImage, 4, 4, false},
// image without provenance doesn't fail
{"no provenance (replace)", NoProvenanceImage, 0, 4, true},
{"no provenance (no replace)", NoProvenanceImage, 2, 4, false},
{"no provenance (replace)", NoProvenanceImage, 0, 2, true},
{"no provenance (no replace)", NoProvenanceImage, 2, 2, false},
}
policyResolver := &policy.PolicyOptions{
LocalPolicyDir: LocalPolicyDir,
policyOpts := &policy.PolicyOptions{
LocalPolicyDir: PassPolicyDir,
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tempDir := test.CreateTempDir(t, "", TestTempDir)
outputLayout := tempDir
opts := &SigningOptions{
outputLayout := test.CreateTempDir(t, "", TestTempDir)
opts := &attestation.SigningOptions{
Replace: tc.replace,
VSAOptions: &attestation.VSAOptions{
BuildLevel: "SLSA_BUILD_LEVEL_3",
PolicyURI: "https://docker.com/attest/policy",
VerifierID: "https://docker.com",
},
}
attIdx, err := oci.AttestationIndexFromPath(tc.TestImage)
assert.NoError(t, err)
attIdx, err := oci.SubjectIndexFromPath(tc.TestImage)
require.NoError(t, err)
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
assert.NoError(t, err)
require.NoError(t, err)
// output signed attestations
idx := v1.ImageIndex(empty.Index)
@@ -75,25 +73,18 @@ func TestSignVerifyOCILayout(t *testing.T) {
},
})
_, err = layout.Write(outputLayout, idx)
assert.NoError(t, err)
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")
resolver := &oci.OCILayoutResolver{
Path: outputLayout,
Platform: "",
}
policy, err := Verify(ctx, policyResolver, resolver)
assert.NoError(t, err)
assert.Truef(t, policy, "Policy should have been found")
mt, _ := attestation.DSSEMediaType(attestation.VSAPredicateType)
vsas, err := test.ExtractAnnotatedStatements(tempDir, mt)
assert.NoError(t, err)
assert.Equalf(t, len(vsas), 2, "expected %d vsa statement, got %d", 2, len(vsas))
var allEnvelopes []*test.AnnotatedStatement
for _, predicate := range []string{intoto.PredicateSPDX, v02.PredicateSLSAProvenance, attestation.VSAPredicateType} {
mt, _ := attestation.DSSEMediaType(predicate)
statements, err := test.ExtractAnnotatedStatements(tempDir, mt)
assert.NoError(t, err)
statements, err := test.ExtractAnnotatedStatements(outputLayout, mt)
require.NoError(t, err)
allEnvelopes = append(allEnvelopes, statements...)
for _, stmt := range statements {
@@ -102,13 +93,77 @@ func TestSignVerifyOCILayout(t *testing.T) {
}
}
assert.Equalf(t, tc.expectedAttestations, len(allEnvelopes), "expected %d attestations, got %d", tc.expectedAttestations, len(allEnvelopes))
statements, err := test.ExtractAnnotatedStatements(tempDir, intoto.PayloadType)
assert.NoError(t, err)
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 TestAddAttestation(t *testing.T) {
ctx, signer := test.Setup(t)
expectedAttestations := 2
expectedStatements := 4
outputLayout := test.CreateTempDir(t, "", TestTempDir)
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
require.NoError(t, err)
statementToAdd := &intoto.Statement{
StatementHeader: intoto.StatementHeader{
PredicateType: attestation.VSAPredicateType,
Type: intoto.StatementInTotoV01,
Subject: []intoto.Subject{
{
Name: attIdx.Name,
Digest: map[string]string{
"sha256": "da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620",
},
},
{
Name: attIdx.Name,
Digest: map[string]string{
"sha256": "7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e",
},
},
},
},
}
signedIndex, err := AddAttestation(ctx, attIdx.Index, statementToAdd, signer)
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)
var allEnvelopes []*test.AnnotatedStatement
mt, _ := attestation.DSSEMediaType(attestation.VSAPredicateType)
statements, err := test.ExtractAnnotatedStatements(outputLayout, mt)
require.NoError(t, err)
allEnvelopes = append(allEnvelopes, statements...)
for _, stmt := range statements {
assert.Equalf(t, attestation.VSAPredicateType, stmt.Annotations[oci.InTotoPredicateType], "expected predicate-type annotation to be set to %s, got %s", attestation.VSAPredicateType, stmt.Annotations[oci.InTotoPredicateType])
assert.Equalf(t, LifecycleStageExperimental, stmt.Annotations[InTotoReferenceLifecycleStage], "expected reference lifecycle stage annotation to be set to %s, got %s", LifecycleStageExperimental, stmt.Annotations[InTotoReferenceLifecycleStage])
}
assert.Equalf(t, expectedAttestations, len(allEnvelopes), "expected %d attestations, got %d", expectedAttestations, len(allEnvelopes))
statements, err = test.ExtractAnnotatedStatements(outputLayout, intoto.PayloadType)
fmt.Printf("statements: %+v\n", statements)
require.NoError(t, err)
assert.Equalf(t, expectedStatements, len(statements), "expected %d statement, got %d", expectedStatements, len(statements))
}
func TestAddSignedLayerAnnotations(t *testing.T) {
testCases := []struct {
name string
@@ -131,7 +186,7 @@ func TestAddSignedLayerAnnotations(t *testing.T) {
data = []byte("test")
testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType))
mediaType := types.OCIManifestSchema1
opts := &SigningOptions{
opts := &attestation.SigningOptions{
Replace: tc.replace,
}
manifest := attestation.AttestationManifest{
@@ -145,9 +200,10 @@ func TestAddSignedLayerAnnotations(t *testing.T) {
},
},
},
SubjectDescriptor: &v1.Descriptor{},
}
newImg, err := addSignedLayers(signedLayers, manifest, opts)
assert.NoError(t, err)
require.NoError(t, err)
mf, _ := newImg.RawManifest()
type Annotations struct {
Annotations map[string]string `json:"annotations"`
@@ -157,7 +213,7 @@ func TestAddSignedLayerAnnotations(t *testing.T) {
}
l := &Layers{}
err = json.Unmarshal(mf, l)
assert.NoError(t, err)
require.NoError(t, err)
_, ok := l.Layers[0].Annotations["test"]
assert.Truef(t, ok, "missing annotations")
})

View File

@@ -1,7 +1,10 @@
package attest
import (
"github.com/docker/attest/pkg/attestation"
"fmt"
"github.com/docker/attest/pkg/policy"
intoto "github.com/in-toto/in-toto-golang/in_toto"
)
const (
@@ -9,7 +12,29 @@ const (
LifecycleStageExperimental = "experimental"
)
type SigningOptions struct {
Replace bool
VSAOptions *attestation.VSAOptions
type Outcome string
const (
OutcomeSuccess Outcome = "success"
OutcomeFailure Outcome = "failure"
OutcomeNoPolicy Outcome = "no_policy"
)
func (o Outcome) StringForVSA() (string, error) {
switch o {
case OutcomeSuccess:
return "PASSED", nil
case OutcomeFailure:
return "FAILED", nil
default:
return "", fmt.Errorf("unknown outcome: %s", o)
}
}
type VerificationResult struct {
Outcome Outcome
Policy *policy.Policy
Input *policy.PolicyInput
VSA *intoto.Statement
Violations []policy.Violation
}

View File

@@ -3,23 +3,128 @@ package attest
import (
"context"
"fmt"
"time"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
intoto "github.com/in-toto/in-toto-golang/in_toto"
)
func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, files []*policy.PolicyFile) error {
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.ReferrersConfig{
Repo: opts.ReferrersRepo,
Style: config.AttestationStyleReferrers,
}
} else if opts.AttestationStyle == config.AttestationStyleAttached {
pctx.Mapping.Attestations = &config.ReferrersConfig{
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 ToPolicyResult(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) {
digest, err := resolver.ImageDigest(ctx)
if err != nil {
return fmt.Errorf("failed to get image digest: %w", err)
return nil, fmt.Errorf("failed to get image digest: %w", err)
}
name, err := resolver.ImageName(ctx)
if err != nil {
return fmt.Errorf("failed to get image name: %w", err)
return nil, fmt.Errorf("failed to get image name: %w", err)
}
purl, canonical, err := oci.RefToPURL(name, resolver.ImagePlatformStr())
platform, err := resolver.ImagePlatform(ctx)
if err != nil {
return fmt.Errorf("failed to convert ref to purl: %w", err)
return nil, err
}
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,
@@ -29,27 +134,11 @@ func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, f
evaluator, err := policy.GetPolicyEvaluator(ctx)
if err != nil {
return err
return nil, err
}
err = evaluator.Evaluate(ctx, resolver, files, input)
result, err := evaluator.Evaluate(ctx, resolver, pctx, input)
if err != nil {
return fmt.Errorf("policy evaluation failed: %w", err)
return nil, fmt.Errorf("policy evaluation failed: %w", err)
}
return nil
}
func Verify(ctx context.Context, opts *policy.PolicyOptions, resolver oci.AttestationResolver) (policyFound bool, err error) {
policyFiles, err := policy.ResolvePolicy(ctx, resolver, opts)
if err != nil {
return false, fmt.Errorf("failed to resolve policy: %w", err)
}
// no policy for image -> success
if policyFiles == nil {
return false, nil
}
// policy found -> verify
return true, VerifyAttestations(ctx, resolver, policyFiles)
return ToPolicyResult(pctx, input, result)
}

View File

@@ -12,13 +12,23 @@ import (
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
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"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
ExampleAttestation = filepath.Join("..", "..", "test", "testdata", "example_attestation.json")
)
const (
LinuxAMD64 = "linux/amd64"
)
func TestVerifyAttestations(t *testing.T) {
ex, err := os.ReadFile(ExampleAttestation)
assert.NoError(t, err)
@@ -42,20 +52,189 @@ func TestVerifyAttestations(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mockPE := test.MockPolicyEvaluator{
EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, pfs []*policy.PolicyFile, input *policy.PolicyInput) error {
return tc.policyEvaluationError
mockPE := policy.MockPolicyEvaluator{
EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, pctx *policy.Policy, input *policy.PolicyInput) (*policy.Result, error) {
return policy.AllowedResult(), tc.policyEvaluationError
},
}
ctx := policy.WithPolicyEvaluator(context.Background(), &mockPE)
err = VerifyAttestations(ctx, resolver, nil)
_, err := VerifyAttestations(ctx, resolver, nil)
if tc.expectedError != nil {
assert.Error(t, err)
assert.Equal(t, tc.expectedError.Error(), err.Error())
if assert.Error(t, err) {
assert.Equal(t, tc.expectedError.Error(), err.Error())
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestVSA(t *testing.T) {
ctx, signer := test.Setup(t)
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
// setup an image with signed attestations
outputLayout := test.CreateTempDir(t, "", TestTempDir)
opts := &attestation.SigningOptions{
Replace: true,
}
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
assert.NoError(t, err)
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
assert.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)
assert.NoError(t, err)
// mocked vsa query should pass
policyOpts := &policy.PolicyOptions{
LocalPolicyDir: PassPolicyDir,
}
src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64))
require.NoError(t, err)
results, err := Verify(ctx, src, policyOpts)
require.NoError(t, err)
assert.Equal(t, OutcomeSuccess, results.Outcome)
assert.Empty(t, results.Violations)
if assert.NotNil(t, results.Input) {
assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", results.Input.Digest)
assert.False(t, results.Input.IsCanonical)
}
assert.Equal(t, intoto.StatementInTotoV01, results.VSA.Type)
assert.Equal(t, attestation.VSAPredicateType, results.VSA.PredicateType)
assert.Len(t, results.VSA.Subject, 1)
require.IsType(t, attestation.VSAPredicate{}, results.VSA.Predicate)
attestationPredicate := results.VSA.Predicate.(attestation.VSAPredicate)
assert.Equal(t, "PASSED", attestationPredicate.VerificationResult)
assert.Equal(t, "docker-official-images", attestationPredicate.Verifier.ID)
assert.Equal(t, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels)
assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI)
}
func TestVerificationFailure(t *testing.T) {
ctx, signer := test.Setup(t)
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
// setup an image with signed attestations
outputLayout := test.CreateTempDir(t, "", TestTempDir)
opts := &attestation.SigningOptions{
Replace: true,
}
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
assert.NoError(t, err)
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
assert.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)
assert.NoError(t, err)
// mocked vsa query should fail
policyOpts := &policy.PolicyOptions{
LocalPolicyDir: FailPolicyDir,
}
src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64))
require.NoError(t, err)
results, err := Verify(ctx, src, policyOpts)
require.NoError(t, err)
assert.Equal(t, OutcomeFailure, results.Outcome)
assert.Len(t, results.Violations, 1)
violation := results.Violations[0]
assert.Equal(t, "missing_attestation", violation.Type)
assert.Equal(t, "Attestation missing for subject", violation.Description)
assert.Nil(t, violation.Attestation)
assert.Equal(t, intoto.StatementInTotoV01, results.VSA.Type)
assert.Equal(t, attestation.VSAPredicateType, results.VSA.PredicateType)
assert.Len(t, results.VSA.Subject, 1)
require.IsType(t, attestation.VSAPredicate{}, results.VSA.Predicate)
attestationPredicate := results.VSA.Predicate.(attestation.VSAPredicate)
assert.Equal(t, "FAILED", attestationPredicate.VerificationResult)
assert.Equal(t, "docker-official-images", attestationPredicate.Verifier.ID)
assert.Equal(t, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels)
assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI)
}
// test signing without a TL entry
func TestSignVerifyNoTL(t *testing.T) {
ctx, signer := test.Setup(t)
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
// setup an image with signed attestations
outputLayout := test.CreateTempDir(t, "", TestTempDir)
testCases := []struct {
name string
signTL bool
policyDir string
success bool
}{
{name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir, success: true},
{name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir, success: false},
{name: "no tl", signTL: false, policyDir: PassPolicyDir, success: false},
}
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
assert.NoError(t, err)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
opts := &attestation.SigningOptions{
Replace: true,
SkipTL: tc.signTL,
}
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
assert.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)
assert.NoError(t, err)
policyOpts := &policy.PolicyOptions{
LocalPolicyDir: tc.policyDir,
}
src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64))
require.NoError(t, err)
results, err := Verify(ctx, src, policyOpts)
require.NoError(t, err)
assert.Equal(t, OutcomeSuccess, results.Outcome)
})
}
}

View File

@@ -1,96 +0,0 @@
package attest
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/oci"
"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"
"github.com/secure-systems-lab/go-securesystemslib/dsse"
)
// generateVSA generates a VSA from the attestation manifest
// TODO: remove signing logic and move generateVSA to attestation/vsa.go
func generateVSA(ctx context.Context, manifest attestation.AttestationManifest, signer dsse.SignerVerifier, opts *SigningOptions) (*mutate.Addendum, error) {
if len(manifest.Attestation.Layers) == 0 {
return nil, fmt.Errorf("no attestations found to generate VSA from")
}
sub := manifest.Attestation.Layers[0].Statement.Subject[0]
stype := manifest.Attestation.Layers[0].Statement.Type
uri, err := attestation.ToVSAResourceURI(sub)
if err != nil {
return nil, fmt.Errorf("failed to generate VSA resource URI: %w", err)
}
inputs := make([]attestation.VSAInputAttestation, 0, len(manifest.Attestation.Layers))
for _, att := range manifest.Attestation.Layers {
mt, err := att.Layer.MediaType()
if err != nil {
return nil, fmt.Errorf("failed to get layer media type: %w", err)
}
if !strings.HasSuffix(string(mt), "+dsse") {
continue
}
dgst, err := att.Layer.Digest()
if err != nil {
return nil, fmt.Errorf("failed to get layer digest: %w", err)
}
inputs = append(inputs, attestation.VSAInputAttestation{
Digest: map[string]string{"sha256": dgst.Hex},
MediaType: string(mt),
})
}
vsaStatement := &intoto.Statement{
StatementHeader: intoto.StatementHeader{
PredicateType: attestation.VSAPredicateType,
Type: stype,
Subject: manifest.Attestation.Layers[0].Statement.Subject,
},
Predicate: attestation.VSAPredicate{
Verifier: attestation.VSAVerifier{
ID: opts.VSAOptions.VerifierID,
},
TimeVerified: time.Now().UTC().Format(time.RFC3339),
ResourceUri: uri,
Policy: attestation.VSAPolicy{URI: opts.VSAOptions.PolicyURI},
VerificationResult: "PASSED",
VerifiedLevels: []string{opts.VSAOptions.BuildLevel},
InputAttestations: inputs,
},
}
payload, err := json.Marshal(vsaStatement)
if err != nil {
return nil, fmt.Errorf("failed to marshal statement: %w", err)
}
env, err := attestation.SignDSSE(ctx, payload, intoto.PayloadType, signer)
if err != nil {
return nil, fmt.Errorf("failed to sign statement: %w", err)
}
mediaType, err := attestation.DSSEMediaType(vsaStatement.PredicateType)
if err != nil {
return nil, fmt.Errorf("failed to get DSSE media type: %w", err)
}
data, err := json.Marshal(env)
if err != nil {
return nil, fmt.Errorf("failed to marshal envelope: %w", err)
}
mt := types.MediaType(mediaType)
newLayer := static.NewLayer(data, mt)
ann := make(map[string]string)
ann[InTotoReferenceLifecycleStage] = LifecycleStageExperimental
ann[oci.InTotoPredicateType] = attestation.VSAPredicateType
withAnnotations := mutate.Addendum{
Layer: newLayer,
Annotations: ann,
}
return &withAnnotations, nil
}

View File

@@ -3,6 +3,7 @@ package attestation
import (
"encoding/json"
"fmt"
"maps"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
@@ -16,9 +17,19 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]AttestationManifes
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
}
var attestationManifests []AttestationManifest
for _, manifest := range idx.Manifests {
if manifest.Annotations[DockerReferenceType] == AttestationManifestType {
subject := subjects[manifest.Annotations[DockerReferenceDigest]]
if subject == nil {
return nil, fmt.Errorf("failed to find subject for attestation manifest: %w", err)
}
attestationImage, err := index.Image(manifest.Digest)
if err != nil {
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", manifest.Digest.String(), err)
@@ -29,7 +40,8 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]AttestationManifes
}
attestationManifests = append(attestationManifests,
AttestationManifest{
Manifest: manifest,
Descriptor: manifest,
SubjectDescriptor: subject,
Attestation: AttestationImage{
Layers: attestationLayers,
Image: attestationImage},
@@ -64,10 +76,7 @@ func GetAttestationsFromImage(image v1.Image) ([]AttestationLayer, error) {
return nil, fmt.Errorf("failed to get descriptor for layer: %w", err)
}
// copy original annotations
ann := make(map[string]string)
for k, v := range layerDesc.Annotations {
ann[k] = v
}
ann := maps.Clone(layerDesc.Annotations)
// only decode intoto statements
var stmt = new(intoto.Statement)
if mt == types.MediaType(intoto.PayloadType) {

View File

@@ -0,0 +1,253 @@
package attestation_test
import (
"fmt"
"net/http/httptest"
"net/url"
"path/filepath"
"testing"
"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/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/registry"
"github.com/google/go-containerregistry/pkg/v1/remote"
"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")
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 {
server *httptest.Server
referrersServer *httptest.Server
skipSubject bool
useDigest bool
referrersRepo string
attestationSource config.AttestationStyle
expectFailure bool
policyDir string
}{
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
},
{
server: httptest.NewServer(registry.New()),
},
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
skipSubject: true,
attestationSource: config.AttestationStyleAttached,
},
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
useDigest: true,
},
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
expectFailure: true, //mismatched args
attestationSource: config.AttestationStyleAttached,
referrersRepo: "referrers",
},
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
expectFailure: true, // no policy
attestationSource: config.AttestationStyleReferrers,
referrersRepo: "referrers",
},
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
attestationSource: config.AttestationStyleReferrers,
},
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(false))),
attestationSource: config.AttestationStyleReferrers,
referrersServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
},
} {
t.Run(fmt.Sprint(tc), func(t *testing.T) {
s := tc.server
defer s.Close()
if tc.referrersServer != nil {
defer tc.referrersServer.Close()
}
u, err := url.Parse(s.URL)
require.NoError(t, err)
opts := &attestation.SigningOptions{
Replace: true,
SkipSubject: tc.skipSubject,
}
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
require.NoError(t, err)
indexName := fmt.Sprintf("%s/repo:root", u.Host)
require.NoError(t, err)
if tc.referrersServer != nil {
ru, err := url.Parse(s.URL)
require.NoError(t, err)
repo := fmt.Sprintf("%s/referrers", ru.Host)
tc.referrersRepo = repo
images, err := attest.SignedAttestationImages(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
for _, img := range images {
err = mirror.PushImageToRegistry(img.Image, fmt.Sprintf("%s:tag-does-not-matter", repo))
require.NoError(t, err)
}
} else {
signedIndex, err := attest.Sign(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
err = mirror.PushIndexToRegistry(signedIndex, indexName)
require.NoError(t, err)
}
for _, platform := range platforms {
// can eval policy in the normal way
ref := indexName
if tc.useDigest {
options := oci.WithOptions(ctx, nil)
subjectRef, err := name.ParseReference(indexName)
require.NoError(t, err)
desc, err := remote.Index(subjectRef, options...)
require.NoError(t, err)
idxDigest, err := desc.Digest()
require.NoError(t, err)
ref = fmt.Sprintf("%s/repo@%s", u.Host, idxDigest.String())
}
policyOpts := &policy.PolicyOptions{
LocalPolicyDir: LocalPolicy,
}
if tc.policyDir != "" {
policyOpts.LocalPolicyDir = tc.policyDir
}
if tc.referrersRepo != "" {
policyOpts.ReferrersRepo = tc.referrersRepo
}
if tc.attestationSource != "" {
policyOpts.AttestationStyle = tc.attestationSource
}
src, err := oci.ParseImageSpec(ref, oci.WithPlatform(platform))
require.NoError(t, err)
results, err := attest.Verify(ctx, src, policyOpts)
if tc.expectFailure {
require.Error(t, err)
continue
}
require.NoError(t, err)
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
if !tc.skipSubject {
// can evaluate policy using referrers
if tc.useDigest {
p, err := oci.ParsePlatform(platform)
require.NoError(t, err)
options := oci.WithOptions(ctx, p)
subjectRef, err := name.ParseReference(indexName)
require.NoError(t, err)
desc, err := remote.Image(subjectRef, options...)
require.NoError(t, err)
subjectDigest, err := desc.Digest()
require.NoError(t, err)
ref = fmt.Sprintf("%s/repo@%s", u.Host, subjectDigest.String())
}
src, err := oci.ParseImageSpec(ref, oci.WithPlatform(platform))
require.NoError(t, err)
results, err = attest.Verify(ctx, src, policyOpts)
require.NoError(t, err)
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
}
}
})
}
}
func TestReferencesInDifferentRepo(t *testing.T) {
ctx, signer := test.Setup(t)
repoName := "repo"
for _, tc := range []struct {
server *httptest.Server
refServer *httptest.Server
}{
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
refServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
},
{
server: httptest.NewServer(registry.New()),
refServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
},
} {
server := tc.server
defer server.Close()
serverUrl, err := url.Parse(server.URL)
require.NoError(t, err)
refServer := tc.refServer
defer refServer.Close()
refServerUrl, err := url.Parse(refServer.URL)
require.NoError(t, err)
opts := &attestation.SigningOptions{
Replace: true,
SkipTL: true,
}
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
require.NoError(t, err)
indexName := fmt.Sprintf("%s/%s:latest", serverUrl.Host, repoName)
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
require.NoError(t, err)
signedImages, err := attest.SignedAttestationImages(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
// push signed attestation image to the ref server
for _, img := range signedImages {
// push references using subject-digest.att convention
err = mirror.PushImageToRegistry(img.Image, 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
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{
LocalPolicyDir: PassPolicyDir,
}
src, err := oci.ParseImageSpec(referencedImage)
require.NoError(t, err)
results, err := attest.Verify(ctx, src, policyOpts)
require.NoError(t, err)
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
}
}
}

View File

@@ -6,20 +6,20 @@ import (
"github.com/docker/attest/internal/util"
"github.com/docker/attest/pkg/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
func SignDSSE(ctx context.Context, payload []byte, payloadType string, signer dsse.SignerVerifier) (*Envelope, error) {
t := tlog.GetTL(ctx)
func SignDSSE(ctx context.Context, payload []byte, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) {
payloadType := intoto.PayloadType
env := new(Envelope)
env.Payload = base64Encoding.EncodeToString(payload)
env.PayloadType = payloadType
encPayload := dsse.PAE(payloadType, payload)
// statement message digest
hash := util.S256(encPayload)
hash := util.SHA256(encPayload)
// sign message digest
sig, err := signer.Sign(ctx, hash)
@@ -33,8 +33,31 @@ func SignDSSE(ctx context.Context, payload []byte, payloadType string, signer ds
return nil, fmt.Errorf("error getting public key ID: %w", err)
}
// upload to TL
entry, err := t.UploadLogEntry(ctx, keyId, encPayload, sig, signer)
dsseSig := Signature{
KeyID: keyId,
Sig: base64Encoding.EncodeToString(sig),
}
if !opts.SkipTL {
ext, err := logSignature(ctx, tlog.GetTL(ctx), &sig, &encPayload, signer)
if err != nil {
return nil, fmt.Errorf("failed to log to rekor: %w", err)
}
dsseSig.Extension = *ext
}
// add signature to dsse envelope
env.Signatures = []Signature{dsseSig}
return env, nil
}
// 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()
if err != nil {
return nil, fmt.Errorf("error getting public key ID: %w", err)
}
entry, err := t.UploadLogEntry(ctx, keyId, *encPayload, *sig, signer)
if err != nil {
return nil, fmt.Errorf("error uploading TL entry: %w", err)
}
@@ -42,21 +65,13 @@ func SignDSSE(ctx context.Context, payload []byte, payloadType string, signer ds
if err != nil {
return nil, fmt.Errorf("error unmarshaling tl entry: %w", err)
}
// add signature w/ tl extension to dsse envelope
env.Signatures = append(env.Signatures, Signature{
KeyID: keyId,
Sig: base64Encoding.EncodeToString(sig),
Extension: Extension{
Kind: DockerDsseExtKind,
Ext: DockerDsseExtension{
Tl: DockerTlExtension{
Kind: RekorTlExtKind,
Data: entryObj, // transparency log entry metadata
},
return &Extension{
Kind: DockerDsseExtKind,
Ext: DockerDsseExtension{
Tl: DockerTlExtension{
Kind: RekorTlExtKind,
Data: entryObj, // transparency log entry metadata
},
},
})
return env, nil
}, nil
}

View File

@@ -14,6 +14,7 @@ import (
"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) {
@@ -27,17 +28,17 @@ func TestSignVerifyAttestation(t *testing.T) {
}
payload, err := json.Marshal(stmt)
assert.NoError(t, err)
env, err := attestation.SignDSSE(ctx, payload, intoto.PayloadType, signer)
assert.NoError(t, err)
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)
assert.NoError(t, err)
require.NoError(t, err)
deserializedEnv := new(attestation.Envelope)
err = json.Unmarshal(serializedEnv, deserializedEnv)
assert.NoError(t, err)
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
@@ -49,10 +50,10 @@ func TestSignVerifyAttestation(t *testing.T) {
assert.NoError(t, err)
badKeyPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
assert.NoError(t, err)
require.NoError(t, err)
badKey := &badKeyPriv.PublicKey
badPEM, err := signerverifier.ToPEM(badKey)
assert.NoError(t, err)
require.NoError(t, err)
testCases := []struct {
name string
@@ -136,7 +137,10 @@ func TestSignVerifyAttestation(t *testing.T) {
To: tc.to,
Status: tc.status,
}
_, err = attestation.VerifyDSSE(ctx, deserializedEnv, attestation.KeysMap{tc.keyId: keyMeta})
opts := &attestation.VerifyOptions{
Keys: attestation.Keys{keyMeta},
}
_, err = attestation.VerifyDSSE(ctx, deserializedEnv, opts)
if tc.expectedError != "" {
assert.Contains(t, err.Error(), tc.expectedError)
} else {

View File

@@ -14,6 +14,7 @@ import (
const (
DockerReferenceType = "vnd.docker.reference.type"
AttestationManifestType = "attestation-manifest"
DockerReferenceDigest = "vnd.docker.reference.digest"
DockerDsseExtKind = "application/vnd.docker.attestation-verification.v1+json"
RekorTlExtKind = "Rekor"
OCIDescriptorDSSEMediaType = ociv1.MediaTypeDescriptor + "+dsse"
@@ -33,12 +34,19 @@ type AttestationImage struct {
Image v1.Image
}
type SignedAttestationImage struct {
Image v1.Image
Descriptor *v1.Descriptor
AttestationManifest AttestationManifest
}
type AttestationManifest struct {
Manifest v1.Descriptor
Attestation AttestationImage
MediaType types.MediaType
Annotations map[string]string
Digest v1.Hash
Descriptor v1.Descriptor
Attestation AttestationImage
MediaType types.MediaType
Annotations map[string]string
Digest v1.Hash
SubjectDescriptor *v1.Descriptor
}
// the following types are needed until https://github.com/secure-systems-lab/dsse/pull/61 is merged
@@ -66,6 +74,20 @@ type DockerTlExtension struct {
Data any `json:"data"`
}
type VerifyOptions struct {
Keys []KeyMetadata `json:"keys"`
SkipTL bool `json:"skip_tl"`
}
type SigningOptions struct {
// replace unsigned statements with signed attestations
Replace bool
// don't log to the configured transparency log
SkipTL bool
// don't add OCI subject field to attestation image
SkipSubject bool
}
func DSSEMediaType(predicateType string) (string, error) {
var predicateName string
switch predicateType {

View File

@@ -30,7 +30,7 @@ type KeyMetadata struct {
type Keys []KeyMetadata
type KeysMap map[string]KeyMetadata
func VerifyDSSE(ctx context.Context, env *Envelope, keys KeysMap) ([]byte, error) {
func VerifyDSSE(ctx context.Context, env *Envelope, opts *VerifyOptions) ([]byte, error) {
// enforce payload type
if !ValidPayloadType(env.PayloadType) {
return nil, fmt.Errorf("unsupported payload type %s", env.PayloadType)
@@ -49,7 +49,7 @@ func VerifyDSSE(ctx context.Context, env *Envelope, keys KeysMap) ([]byte, error
// verify signatures and transparency log entry
for _, sig := range env.Signatures {
err := verifySignature(ctx, sig, encPayload, keys)
err := verifySignature(ctx, sig, encPayload, opts)
if err != nil {
return nil, err
}
@@ -58,31 +58,11 @@ func VerifyDSSE(ctx context.Context, env *Envelope, keys KeysMap) ([]byte, error
return payload, nil
}
func verifySignature(ctx context.Context, sig Signature, payload []byte, keys KeysMap) error {
t := tlog.GetTL(ctx)
if sig.Extension.Kind == "" {
return fmt.Errorf("error missing signature extension kind")
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
}
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)
}
entry := sig.Extension.Ext.Tl.Data
entryBytes, err := json.Marshal(entry)
if err != nil {
return fmt.Errorf("failed to marshal TL entry: %w", err)
}
integratedTime, err := t.VerifyLogEntry(ctx, entryBytes)
if err != nil {
return fmt.Errorf("TL entry failed verification: %w", err)
}
keyMeta, ok := keys[sig.KeyID]
if !ok {
return fmt.Errorf("error key not found: %s", sig.KeyID)
@@ -91,37 +71,60 @@ func verifySignature(ctx context.Context, sig Signature, payload []byte, keys Ke
if keyMeta.Distrust {
return fmt.Errorf("key %s is distrusted", keyMeta.ID)
}
if integratedTime.Before(keyMeta.From) {
return fmt.Errorf("key %s was not yet valid at TL log time %s (key valid from %s)", keyMeta.ID, integratedTime, keyMeta.From)
}
if keyMeta.To != nil && !integratedTime.Before(*keyMeta.To) {
return fmt.Errorf("key %s was already %s at TL log time %s (key %s at %s)", keyMeta.ID, keyMeta.Status, integratedTime, keyMeta.Status, *keyMeta.To)
}
// TODO: this is unmarshalling with MarshalPKIXPublicKey only for us to marshal it again
publicKey, err := signerverifier.Parse([]byte(keyMeta.PEM))
if err != nil {
return fmt.Errorf("failed to parse public key: %w", err)
}
// verify TL entry payload
encodedPub, err := x509.MarshalPKIXPublicKey(publicKey)
if err != nil {
return fmt.Errorf("error failed to marshal public key: %w", err)
}
err = t.VerifyEntryPayload(entryBytes, payload, encodedPub)
if err != nil {
return fmt.Errorf("TL entry failed payload verification: %w", err)
if !opts.SkipTL {
t := tlog.GetTL(ctx)
if sig.Extension.Kind == "" {
return fmt.Errorf("error missing signature extension kind")
}
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)
}
entry := sig.Extension.Ext.Tl.Data
entryBytes, err := json.Marshal(entry)
if err != nil {
return fmt.Errorf("failed to marshal TL entry: %w", err)
}
integratedTime, err := t.VerifyLogEntry(ctx, entryBytes)
if err != nil {
return fmt.Errorf("TL entry failed verification: %w", err)
}
if integratedTime.Before(keyMeta.From) {
return fmt.Errorf("key %s was not yet valid at TL log time %s (key valid from %s)", keyMeta.ID, integratedTime, keyMeta.From)
}
if keyMeta.To != nil && !integratedTime.Before(*keyMeta.To) {
return fmt.Errorf("key %s was already %s at TL log time %s (key %s at %s)", keyMeta.ID, keyMeta.Status, integratedTime, keyMeta.Status, *keyMeta.To)
}
// verify TL entry payload
encodedPub, err := x509.MarshalPKIXPublicKey(publicKey)
if err != nil {
return fmt.Errorf("error failed to marshal public key: %w", err)
}
err = t.VerifyEntryPayload(entryBytes, payload, encodedPub)
if err != nil {
return fmt.Errorf("TL entry failed payload verification: %w", err)
}
}
// decode signature
signature, err := base64.StdEncoding.Strict().DecodeString(sig.Sig)
if err != nil {
return fmt.Errorf("error failed to decode signature: %w", err)
}
// verify payload ecdsa signature
ok = ecdsa.VerifyASN1(publicKey, util.S256(payload), signature)
ok = ecdsa.VerifyASN1(publicKey, util.SHA256(payload), signature)
if !ok {
return fmt.Errorf("payload signature is not valid")
}

View File

@@ -39,8 +39,11 @@ func TestVerifyUnsignedAttestation(t *testing.T) {
Payload: base64.StdEncoding.EncodeToString(payload),
PayloadType: intoto.PayloadType,
}
opts := &attestation.VerifyOptions{
Keys: attestation.Keys{},
}
_, err := attestation.VerifyDSSE(ctx, env, attestation.KeysMap{})
_, err := attestation.VerifyDSSE(ctx, env, opts)
assert.Error(t, err)
assert.Contains(t, err.Error(), "no signatures")
}

View File

@@ -34,12 +34,6 @@ type VSAInputAttestation struct {
MediaType string `json:"mediaType"`
}
type VSAOptions struct {
BuildLevel string
PolicyURI string
VerifierID string
}
func ToVSAResourceURI(sub intoto.Subject) (string, error) {
//parse purl
purl, err := packageurl.FromString(sub.Name)

49
pkg/config/config.go Normal file
View File

@@ -0,0 +1,49 @@
package config
import (
"fmt"
"os"
"path/filepath"
"github.com/docker/attest/pkg/tuf"
goyaml "gopkg.in/yaml.v3"
)
const (
MappingFilename = "mapping.yaml"
)
func LoadLocalMappings(configDir string) (*PolicyMappings, error) {
if configDir == "" {
return nil, nil
}
mappings := &PolicyMappings{}
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 = goyaml.Unmarshal(mappingFile, mappings)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", path, err)
}
return mappings, nil
}
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 := &PolicyMappings{}
err = goyaml.Unmarshal(fileContents, mappings)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", filename, err)
}
return mappings, nil
}

48
pkg/config/types.go Normal file
View File

@@ -0,0 +1,48 @@
package config
type PolicyMappings struct {
Version string `json:"version"`
Kind string `json:"kind"`
Policies []*PolicyMapping `json:"policies"`
Mirrors []*PolicyMirror `json:"mirrors"`
}
type AttestationStyle string
const (
AttestationStyleAttached AttestationStyle = "attached"
AttestationStyleReferrers AttestationStyle = "referrers"
)
type PolicyMapping struct {
Id string `json:"id"`
Description string `json:"description"`
Origin *PolicyOrigin `json:"origin"`
Files []PolicyMappingFile `json:"files"`
Attestations *ReferrersConfig `json:"attestations"`
}
type ReferrersConfig struct {
Style AttestationStyle `json:"style"`
Repo string `json:"repo"`
}
type PolicyMappingFile struct {
Path string `json:"path"`
}
type PolicyMirror struct {
PolicyId string `yaml:"policy-id"`
Mirror MirrorSpec `json:"mirror"`
}
type MirrorSpec struct {
Domains []string `json:"domains"`
Prefix string `json:"prefix"`
}
type PolicyOrigin struct {
Name string `json:"name"`
Prefix string `json:"prefix"`
Domain string `json:"domain"`
}

View File

@@ -0,0 +1,152 @@
package mirror_test
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/pkg/mirror"
"github.com/docker/attest/pkg/tuf"
v1 "github.com/google/go-containerregistry/pkg/v1"
)
type TufMirrorOutput struct {
metadata v1.Image
delegatedMetadata []*mirror.MirrorImage
targets []*mirror.MirrorImage
delegatedTargets []*mirror.MirrorIndex
}
func ExampleNewTufMirror() {
home, err := os.UserHomeDir()
if err != nil {
panic(err)
}
tufOutputPath := filepath.Join(home, ".docker", "tuf")
// configure TUF mirror
metadataURI := "https://docker.github.io/tuf-staging/metadata"
targetsURI := "https://docker.github.io/tuf-staging/targets"
m, err := mirror.NewTufMirror(embed.StagingRoot, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker())
if err != nil {
panic(err)
}
// create metadata manifest
metadataManifest, err := m.GetMetadataManifest(metadataURI)
if err != nil {
panic(err)
}
// create delegated targets metadata manifests
delegatedMetadata, err := m.GetDelegatedMetadataMirrors()
if err != nil {
panic(err)
}
// create targets manifest
targets, err := m.GetTufTargetMirrors()
if err != nil {
panic(err)
}
// create delegated targets manifests
delegatedTargets, err := m.GetDelegatedTargetMirrors()
if err != nil {
panic(err)
}
mirrorOutput := &TufMirrorOutput{
metadata: metadataManifest,
delegatedMetadata: delegatedMetadata,
targets: targets,
delegatedTargets: delegatedTargets,
}
// push metadata and targets to registry (optional)
err = mirrorToRegistry(mirrorOutput)
if err != nil {
panic(err)
}
// save metadata and targets to local directory (optional)
mirrorOutputPath := filepath.Join(home, ".docker", "tuf", "mirror")
err = mirrorToLocal(mirrorOutput, mirrorOutputPath)
if err != nil {
panic(err)
}
}
func mirrorToRegistry(o *TufMirrorOutput) error {
// push metadata to registry
metadataRepo := "registry-1.docker.io/docker/tuf-metadata:latest"
err := mirror.PushImageToRegistry(o.metadata, metadataRepo)
if err != nil {
return err
}
// push delegated metadata to registry
for _, metadata := range o.delegatedMetadata {
repo, _, ok := strings.Cut(metadataRepo, ":")
if !ok {
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)
if err != nil {
return err
}
}
// push top-level targets to registry
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)
if err != nil {
return err
}
}
// push delegated targets to registry
for _, target := range o.delegatedTargets {
imageName := fmt.Sprintf("%s:%s", targetsRepo, target.Tag)
err = mirror.PushIndexToRegistry(target.Index, imageName)
if err != nil {
return err
}
}
return nil
}
func mirrorToLocal(o *TufMirrorOutput, outputPath string) error {
// output metadata to local directory
err := mirror.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)
if err != nil {
return err
}
}
// output top-level targets to local directory
for _, target := range o.targets {
path := filepath.Join(outputPath, target.Tag)
err = mirror.SaveImageAsOCILayout(target.Image, path)
if err != nil {
return err
}
}
// output delegated targets to local directory
for _, target := range o.delegatedTargets {
path := filepath.Join(outputPath, target.Tag)
err = mirror.SaveIndexAsOCILayout(target.Index, path)
if err != nil {
return err
}
}
return nil
}

View File

@@ -17,7 +17,7 @@ import (
// -----------------
// GetMetadataManifest returns an image with TUF root metadata as layers
func (m *TufMirror) GetMetadataManifest(metadataURL string) (*v1.Image, error) {
func (m *TufMirror) GetMetadataManifest(metadataURL string) (v1.Image, error) {
metadata, err := m.getTufMetadataMirror(metadataURL)
if err != nil {
return nil, fmt.Errorf("failed to get metadata: %w", err)
@@ -78,7 +78,7 @@ func (m *TufMirror) getTufMetadataMirror(metadataURL string) (*TufMetadata, erro
}
// buildMetadataManifest returns an OCI image with TUF metadata as layers with annotations
func (m *TufMirror) buildMetadataManifest(metadata *TufMetadata) (*v1.Image, error) {
func (m *TufMirror) buildMetadataManifest(metadata *TufMetadata) (v1.Image, error) {
img := empty.Image
img = mutate.MediaType(img, types.OCIManifestSchema1)
img = mutate.ConfigMediaType(img, types.OCIConfigJSON)
@@ -87,17 +87,17 @@ func (m *TufMirror) buildMetadataManifest(metadata *TufMetadata) (*v1.Image, err
if err != nil {
return nil, fmt.Errorf("failed to make role layer: %w", err)
}
img, err = mutate.Append(img, *layers...)
img, err = mutate.Append(img, layers...)
if err != nil {
return nil, fmt.Errorf("failed to append role layer to image: %w", err)
}
}
return &img, nil
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) {
layers := new([]mutate.Addendum)
func (m *TufMirror) makeRoleLayers(role TufRole, tufMetadata *TufMetadata) ([]mutate.Addendum, error) {
var layers []mutate.Addendum
ann := map[string]string{tufFileAnnotation: ""}
switch role {
case metadata.ROOT:
@@ -108,7 +108,7 @@ func (m *TufMirror) makeRoleLayers(role TufRole, tufMetadata *TufMetadata) (*[]m
layers = m.annotatedMetaLayers(tufMetadata.Targets)
case metadata.TIMESTAMP:
ann[tufFileAnnotation] = fmt.Sprintf("%s.json", role)
*layers = append(*layers, mutate.Addendum{Layer: static.NewLayer(tufMetadata.Timestamp, tufMetadataMediaType), Annotations: ann})
layers = append(layers, mutate.Addendum{Layer: static.NewLayer(tufMetadata.Timestamp, tufMetadataMediaType), Annotations: ann})
default:
return nil, fmt.Errorf("unsupported TUF role: %s", role)
}
@@ -116,11 +116,11 @@ func (m *TufMirror) makeRoleLayers(role TufRole, tufMetadata *TufMetadata) (*[]m
}
// annotatedMetaLayers returns a list of layers with annotations for each TUF metadata file
func (m *TufMirror) annotatedMetaLayers(meta map[string][]byte) *[]mutate.Addendum {
layers := new([]mutate.Addendum)
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}
*layers = append(*layers, mutate.Addendum{Layer: static.NewLayer(data, tufMetadataMediaType), Annotations: ann})
layers = append(layers, mutate.Addendum{Layer: static.NewLayer(data, tufMetadataMediaType), Annotations: ann})
}
return layers
}
@@ -144,8 +144,8 @@ func (m *TufMirror) GetDelegatedMetadataMirrors() ([]*MirrorImage, error) {
}
// getDelegatedTargetsMetadata returns delegated targets metadata as a list of DelegatedTargetMetadata (role name and data)
func (m *TufMirror) getDelegatedTargetsMetadata() (*[]DelegatedTargetMetadata, error) {
delegatedTargets := new([]DelegatedTargetMetadata)
func (m *TufMirror) getDelegatedTargetsMetadata() ([]DelegatedTargetMetadata, error) {
var delegatedTargets []DelegatedTargetMetadata
md := m.TufClient.GetMetadata()
for _, role := range md.Targets[metadata.TARGETS].Signed.Delegations.Roles {
roleMetadata, err := m.TufClient.LoadDelegatedTargets(role.Name, metadata.TARGETS)
@@ -165,15 +165,15 @@ func (m *TufMirror) getDelegatedTargetsMetadata() (*[]DelegatedTargetMetadata, e
if md.Root.Signed.ConsistentSnapshot {
version = strconv.FormatInt(meta.Version, 10)
}
*delegatedTargets = append(*delegatedTargets, DelegatedTargetMetadata{Name: role.Name, Version: version, Data: roleBytes})
delegatedTargets = append(delegatedTargets, DelegatedTargetMetadata{Name: role.Name, Version: version, Data: roleBytes})
}
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) {
func (m *TufMirror) buildDelegatedMetadataManifests(delegated []DelegatedTargetMetadata) ([]*MirrorImage, error) {
manifests := []*MirrorImage{}
for _, role := range *delegated {
for _, role := range delegated {
img := empty.Image
img = mutate.MediaType(img, types.OCIManifestSchema1)
img = mutate.ConfigMediaType(img, types.OCIConfigJSON)
@@ -183,7 +183,7 @@ func (m *TufMirror) buildDelegatedMetadataManifests(delegated *[]DelegatedTarget
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, &MirrorImage{Image: img, Tag: role.Name})
}
return manifests, nil
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/tuf"
"github.com/stretchr/testify/assert"
"github.com/theupdateframework/go-tuf/v2/metadata"
)
@@ -20,11 +21,11 @@ func TestGetTufMetadataMirror(t *testing.T) {
defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets")
assert.Nil(t, err)
m, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
assert.NoError(t, err)
tufMetadata, err := m.getTufMetadataMirror(server.URL + "/metadata")
assert.Nil(t, err)
assert.NoError(t, err)
// check that all roles are not empty
assert.Greater(t, len(tufMetadata.Root), 0)
@@ -38,16 +39,15 @@ func TestGetMetadataManifest(t *testing.T) {
defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets")
assert.Nil(t, err)
m, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
assert.NoError(t, err)
img, err := m.GetMetadataManifest(server.URL + "/metadata")
assert.Nil(t, err)
assert.NoError(t, err)
assert.NotNil(t, img)
image := *img
mf, err := image.RawManifest()
assert.Nil(t, err)
mf, err := img.RawManifest()
assert.NoError(t, err)
type Annotations struct {
Annotations map[string]string `json:"annotations"`
@@ -57,7 +57,7 @@ func TestGetMetadataManifest(t *testing.T) {
}
l := &Layers{}
err = json.Unmarshal(mf, l)
assert.Nil(t, err)
assert.NoError(t, err)
// check that layers are annotated and use consistent snapshot naming
for _, layer := range l.Layers {
@@ -69,7 +69,7 @@ func TestGetMetadataManifest(t *testing.T) {
continue
}
_, err := strconv.Atoi(parts[0])
assert.Nil(t, err)
assert.NoError(t, err)
}
}
@@ -78,11 +78,11 @@ func TestGetDelegatedMetadataMirrors(t *testing.T) {
defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets")
assert.Nil(t, err)
m, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
assert.NoError(t, err)
delegations, err := m.GetDelegatedMetadataMirrors()
assert.Nil(t, err)
assert.NoError(t, err)
assert.NotNil(t, delegations)
assert.Greater(t, len(delegations), 0)

View File

@@ -2,7 +2,6 @@ package mirror
import (
"fmt"
"log"
"os"
"github.com/docker/attest/internal/embed"
@@ -15,73 +14,71 @@ import (
"github.com/google/go-containerregistry/pkg/v1/remote"
)
func NewTufMirror(root []byte, tufPath, metadataURL, targetsURL string) (*TufMirror, error) {
func NewTufMirror(root []byte, tufPath, metadataURL, targetsURL string, versionChecker tuf.VersionChecker) (*TufMirror, error) {
if root == nil {
root = embed.DefaultRoot
}
tufClient, err := tuf.NewTufClient(root, tufPath, metadataURL, targetsURL)
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
}
func PushToRegistry(image any, imageName string) error {
func PushImageToRegistry(image v1.Image, imageName string) error {
// Parse the image name
ref, err := name.ParseReference(imageName)
if err != nil {
log.Fatalf("Failed to parse image name: %v", err)
return fmt.Errorf("Failed to parse image name '%s': %w", imageName, err)
}
// Get the authenticator from the default Docker keychain
auth, err := authn.DefaultKeychain.Resolve(ref.Context())
if err != nil {
log.Fatalf("Failed to get authenticator: %v", err)
return fmt.Errorf("Failed to get authenticator: %w", err)
}
// Push the image to the registry
switch image := image.(type) {
case *v1.Image:
if err := remote.Write(ref, *image, remote.WithAuth(auth)); err != nil {
return fmt.Errorf("failed to push image %s: %w", imageName, err)
}
case *v1.ImageIndex:
if err := remote.WriteIndex(ref, *image, remote.WithAuth(auth)); err != nil {
return fmt.Errorf("failed to push image index %s: %w", imageName, err)
}
default:
if err := remote.WriteIndex(ref, image.(v1.ImageIndex), remote.WithAuth(auth)); err != nil {
return fmt.Errorf("failed to push image index %s: %w", imageName, err)
}
}
return nil
return remote.Write(ref, image, remote.WithAuth(auth))
}
func SaveAsOCILayout(image any, path string) error {
func PushIndexToRegistry(image v1.ImageIndex, imageName string) error {
// Parse the index name
ref, err := name.ParseReference(imageName)
if err != nil {
return fmt.Errorf("Failed to parse image name: %w", err)
}
// Get the authenticator from the default Docker keychain
auth, err := authn.DefaultKeychain.Resolve(ref.Context())
if err != nil {
return fmt.Errorf("Failed to get authenticator: %w", err)
}
// Push the index to the registry
return remote.WriteIndex(ref, image, remote.WithAuth(auth))
}
func SaveImageAsOCILayout(image v1.Image, path string) error {
// Save the image to the local filesystem
err := os.MkdirAll(path, os.FileMode(0744))
err := os.MkdirAll(path, os.FileMode(0777))
if err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
switch image := image.(type) {
case *v1.Image:
index := empty.Index
l, err := layout.Write(path, index)
if err != nil {
return fmt.Errorf("failed to create index: %w", err)
}
err = l.AppendImage(*image)
if err != nil {
return fmt.Errorf("failed to append image to index: %w", err)
}
case *v1.ImageIndex:
_, err := layout.Write(path, *image)
if err != nil {
return fmt.Errorf("failed to create index: %w", err)
}
default:
_, err := layout.Write(path, image.(v1.ImageIndex))
if err != nil {
return fmt.Errorf("failed to create index: %w", err)
}
index := empty.Index
l, err := layout.Write(path, index)
if err != nil {
return fmt.Errorf("failed to create index: %w", err)
}
return l.AppendImage(image)
}
func SaveIndexAsOCILayout(image v1.ImageIndex, path string) error {
// Save the index to the local filesystem
err := os.MkdirAll(path, os.FileMode(0777))
if err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
_, err = layout.Write(path, image)
if err != nil {
return fmt.Errorf("failed to create index: %w", err)
}
return nil
}

View File

@@ -35,14 +35,14 @@ func (m *TufMirror) GetTufTargetMirrors() ([]*MirrorImage, error) {
if !ok {
return nil, fmt.Errorf("missing sha256 hash for target %s", t.Path)
}
name := strings.Join([]string{hash.String(), t.Path}, ".")
name := hash.String() + "." + t.Path
ann := map[string]string{tufFileAnnotation: name}
layer := mutate.Addendum{Layer: static.NewLayer(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, &MirrorImage{Image: img, Tag: name})
}
return targetMirrors, nil
}
@@ -86,7 +86,7 @@ func (m *TufMirror) GetDelegatedTargetMirrors() ([]*MirrorIndex, error) {
if !ok {
return nil, fmt.Errorf("failed to find target subdirectory [%s] in path: %s", subdir, target.Path)
}
name := strings.Join([]string{hash.String(), filename}, ".")
name := hash.String() + "." + filename
ann := map[string]string{tufFileAnnotation: name}
layer := mutate.Addendum{Layer: static.NewLayer(data, tufTargetMediaType), Annotations: ann}
img, err = mutate.Append(img, layer)
@@ -103,7 +103,7 @@ func (m *TufMirror) GetDelegatedTargetMirrors() ([]*MirrorIndex, error) {
},
})
}
mirror = append(mirror, &MirrorIndex{Index: &index, Tag: role.Name})
mirror = append(mirror, &MirrorIndex{Index: index, Tag: role.Name})
}
return mirror, nil
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/tuf"
"github.com/stretchr/testify/assert"
)
@@ -26,23 +27,23 @@ func TestGetTufTargetsMirror(t *testing.T) {
defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets")
assert.Nil(t, err)
m, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
assert.NoError(t, err)
targets, err := m.GetTufTargetMirrors()
assert.Nil(t, err)
assert.NoError(t, err)
assert.Greater(t, len(targets), 0)
// check for image layer annotations
for _, target := range targets {
img := *target.Image
img := target.Image
mf, err := img.RawManifest()
assert.Nil(t, err)
assert.NoError(t, err)
// unmarshal manifest with annotations
l := &Layers{}
err = json.Unmarshal(mf, l)
assert.Nil(t, err)
assert.NoError(t, err)
// check that layers are annotated
for _, layer := range l.Layers {
@@ -60,11 +61,11 @@ func TestTargetDelegationMetadata(t *testing.T) {
defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp")
tm, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets")
assert.Nil(t, err)
tm, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
assert.NoError(t, err)
targets, err := tm.TufClient.LoadDelegatedTargets("test-role", "targets")
assert.Nil(t, err)
assert.NoError(t, err)
assert.Greater(t, len(targets.Signed.Targets), 0)
}
@@ -73,23 +74,23 @@ func TestGetDelegatedTargetMirrors(t *testing.T) {
defer server.Close()
path := test.CreateTempDir(t, "", "tuf_temp")
m, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets")
assert.Nil(t, err)
m, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
assert.NoError(t, err)
mirrors, err := m.GetDelegatedTargetMirrors()
assert.Nil(t, err)
assert.NoError(t, err)
assert.Greater(t, len(mirrors), 0)
// check for index image annotations
for _, mirror := range mirrors {
idx := *mirror.Index
idx := mirror.Index
mf, err := idx.RawManifest()
assert.Nil(t, err)
assert.NoError(t, err)
// unmarshal manifest with annotations
l := &Layers{}
err = json.Unmarshal(mf, l)
assert.Nil(t, err)
assert.NoError(t, err)
// check that layers are annotated
for _, layer := range l.Layers {

View File

@@ -32,12 +32,12 @@ type DelegatedTargetMetadata struct {
}
type MirrorImage struct {
Image *v1.Image
Image v1.Image
Tag string
}
type MirrorIndex struct {
Index *v1.ImageIndex
Index v1.ImageIndex
Tag string
}

151
pkg/oci/layout.go Normal file
View File

@@ -0,0 +1,151 @@
package oci
import (
"context"
"encoding/json"
"fmt"
"strings"
att "github.com/docker/attest/pkg/attestation"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/pkg/errors"
)
// implementation of AttestationResolver that closes over attestations from an oci layout
type OCILayoutResolver struct {
*AttestationManifest
*ImageSpec
}
func NewOCILayoutAttestationResolver(src *ImageSpec) (*OCILayoutResolver, error) {
r := &OCILayoutResolver{
ImageSpec: src,
}
_, err := r.fetchAttestationManifest()
if err != nil {
return nil, err
}
return r, nil
}
func (r *OCILayoutResolver) fetchAttestationManifest() (*AttestationManifest, error) {
if r.AttestationManifest == nil {
m, err := attestationManifestFromOCILayout(r.Identifier, r.ImageSpec.Platform)
if err != nil {
return nil, err
}
r.AttestationManifest = m
}
return r.AttestationManifest, nil
}
func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
attestationImage := r.AttestationManifest.Image
layers, err := attestationImage.Layers()
if err != nil {
return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err)
}
var envs []*att.Envelope
manifest := r.AttestationManifest.Manifest
for i, l := range manifest.Layers {
if l.Annotations[InTotoPredicateType] != predicateType {
continue
}
layer := layers[i]
mt, err := layer.MediaType()
if err != nil {
return nil, fmt.Errorf("failed to get layer media type: %w", err)
}
mts := string(mt)
if !strings.HasSuffix(mts, "+dsse") {
continue
}
var env = new(att.Envelope)
// parse layer blob as json
r, err := layer.Uncompressed()
if err != nil {
return nil, fmt.Errorf("failed to get layer contents: %w", err)
}
defer r.Close()
err = json.NewDecoder(r).Decode(env)
if err != nil {
return nil, fmt.Errorf("failed to decode envelope: %w", err)
}
envs = append(envs, env)
}
return envs, nil
}
func (r *OCILayoutResolver) ImageName(ctx context.Context) (string, error) {
return r.Name, nil
}
func (r *OCILayoutResolver) ImageDigest(ctx context.Context) (string, error) {
return r.Digest, nil
}
func (r *OCILayoutResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
return r.ImageSpec.Platform, nil
}
func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*AttestationManifest, error) {
idx, err := layout.ImageIndexFromPath(path)
if err != nil {
return nil, err
}
idxm, err := idx.IndexManifest()
if err != nil {
return nil, fmt.Errorf("failed to get digest: %w", err)
}
idxDescriptor := idxm.Manifests[0]
name := idxDescriptor.Annotations["org.opencontainers.image.ref.name"]
idxDigest := idxDescriptor.Digest
mfs, err := idx.ImageIndex(idxDigest)
if err != nil {
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
}
mfs2, err := mfs.IndexManifest()
if err != nil {
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
}
var imageDigest string
for _, mf := range mfs2.Manifests {
if mf.Platform.Equals(*platform) {
imageDigest = mf.Digest.String()
}
}
for _, mf := range mfs2.Manifests {
if mf.Annotations[att.DockerReferenceType] != AttestationManifestType {
continue
}
if mf.Annotations[att.DockerReferenceDigest] != imageDigest {
continue
}
attestationImage, err := mfs.Image(mf.Digest)
if err != nil {
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
}
manifest, err := attestationImage.Manifest()
if err != nil {
return nil, fmt.Errorf("failed to get manifest: %w", err)
}
attest := &AttestationManifest{
Name: name,
Image: attestationImage,
Manifest: manifest,
Descriptor: &mf,
Digest: imageDigest,
Platform: platform,
}
return attest, nil
}
return nil, errors.New("attestation manifest not found")
}

View File

@@ -10,18 +10,17 @@ import (
"github.com/distribution/reference"
att "github.com/docker/attest/pkg/attestation"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/package-url/packageurl-go"
"github.com/pkg/errors"
)
// parsePlatform parses the provided platform string or attempts to obtain
// ParsePlatform parses the provided platform string or attempts to obtain
// the platform of the current host system
func parsePlatform(platformStr string) (*v1.Platform, error) {
func ParsePlatform(platformStr string) (*v1.Platform, error) {
if platformStr == "" {
cdp := platforms.Normalize(platforms.DefaultSpec())
if cdp.OS != "windows" {
@@ -37,255 +36,7 @@ func parsePlatform(platformStr string) (*v1.Platform, error) {
}
}
func attestationManifestFromOCILayout(path string, platformStr string) (*AttestationManifest, 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)
}
idxDescriptor := idxm.Manifests[0]
name := idxDescriptor.Annotations["org.opencontainers.image.ref.name"]
idxDigest := idxDescriptor.Digest
mfs, err := idx.ImageIndex(idxDigest)
if err != nil {
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
}
mfs2, err := mfs.IndexManifest()
if err != nil {
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
}
platform, err := parsePlatform(platformStr)
if err != nil {
return nil, fmt.Errorf("failed to parse platform: %w", err)
}
var imageDigest string
for _, mf := range mfs2.Manifests {
if mf.Platform.Equals(*platform) {
imageDigest = mf.Digest.String()
}
}
for _, mf := range mfs2.Manifests {
if mf.Annotations[DockerReferenceType] != AttestationManifestType {
continue
}
if mf.Annotations[DockerReferenceDigest] != imageDigest {
continue
}
attestationImage, err := mfs.Image(mf.Digest)
if err != nil {
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
}
manifest, err := attestationImage.Manifest()
if err != nil {
return nil, fmt.Errorf("failed to get manifest: %w", err)
}
attest := &AttestationManifest{
Name: name,
Image: attestationImage,
Manifest: manifest,
Descriptor: &mf,
Digest: imageDigest,
Platform: platform,
}
return attest, nil
}
return nil, errors.New("attestation manifest not found")
}
// implementation of AttestationResolver that closes over attestations from an oci layout
type OCILayoutResolver struct {
Path string
Platform string
*AttestationManifest
}
func (r *OCILayoutResolver) ImagePlatformStr() string {
return r.Platform
}
func (r *OCILayoutResolver) fetchAttestationManifest() (*AttestationManifest, error) {
if r.AttestationManifest == nil {
m, err := attestationManifestFromOCILayout(r.Path, r.Platform)
if err != nil {
return nil, fmt.Errorf("failed to get attestation manifest: %w", err)
}
r.AttestationManifest = m
}
return r.AttestationManifest, nil
}
func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
if r.AttestationManifest == nil {
_, err := r.fetchAttestationManifest()
if err != nil {
return nil, fmt.Errorf("failed to get attestation manifest: %w", err)
}
}
attestationImage := r.AttestationManifest.Image
layers, err := attestationImage.Layers()
if err != nil {
return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err)
}
var envs []*att.Envelope
manifest := r.AttestationManifest.Manifest
for i, l := range manifest.Layers {
if l.Annotations[InTotoPredicateType] != predicateType {
continue
}
layer := layers[i]
mt, err := layer.MediaType()
if err != nil {
return nil, fmt.Errorf("failed to get layer media type: %w", err)
}
mts := string(mt)
if !strings.HasSuffix(mts, "+dsse") {
continue
}
var env = new(att.Envelope)
// parse layer blob as json
r, err := layer.Uncompressed()
if err != nil {
return nil, fmt.Errorf("failed to get layer contents: %w", err)
}
defer r.Close()
err = json.NewDecoder(r).Decode(env)
if err != nil {
return nil, fmt.Errorf("failed to decode envelope: %w", err)
}
envs = append(envs, env)
}
return envs, nil
}
func (r *OCILayoutResolver) ImageName(ctx context.Context) (string, error) {
if r.AttestationManifest == nil {
_, err := r.fetchAttestationManifest()
if err != nil {
return "", fmt.Errorf("failed to get attestation manifest: %w", err)
}
}
return r.Name, nil
}
func (r *OCILayoutResolver) ImageDigest(ctx context.Context) (string, error) {
if r.AttestationManifest == nil {
_, err := r.fetchAttestationManifest()
if err != nil {
return "", fmt.Errorf("failed to get attestation manifest: %w", err)
}
}
return r.Digest, nil
}
type RegistryResolver struct {
Image string
Platform string
*AttestationManifest
}
func (r *RegistryResolver) ImageName(ctx context.Context) (string, error) {
return r.Image, nil
}
func (r *RegistryResolver) ImagePlatformStr() string {
return r.Platform
}
func (r *RegistryResolver) ImageDigest(ctx context.Context) (string, error) {
if r.AttestationManifest == nil {
attest, err := FetchAttestationManifest(ctx, r.Image, r.Platform)
if err != nil {
return "", fmt.Errorf("failed to get attestation manifest: %w", err)
}
r.AttestationManifest = attest
}
return r.Digest, nil
}
func (r *RegistryResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
if r.AttestationManifest == nil {
attest, err := FetchAttestationManifest(ctx, r.Image, r.Platform)
if err != nil {
return nil, fmt.Errorf("failed to get attestation manifest: %w", err)
}
r.AttestationManifest = attest
}
return ExtractEnvelopes(r.AttestationManifest, predicateType)
}
func FetchAttestationManifest(ctx context.Context, image, platformStr string) (*AttestationManifest, error) {
platform, err := parsePlatform(platformStr)
if err != nil {
return nil, fmt.Errorf("failed to parse platform %s: %w", platform, err)
}
// 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)
}
desc, err := remote.Index(ref, options...)
if err != nil {
return nil, fmt.Errorf("failed to obtain index manifest: %w", err)
}
ix, err := desc.IndexManifest()
if err != nil {
return nil, fmt.Errorf("failed to obtain index manifest: %w", err)
}
digest, err := imageDigestForPlatform(ix, platform)
if err != nil {
return nil, fmt.Errorf("failed to obtain image for platform: %w", err)
}
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(ix, 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)
}
manifest := new(v1.Manifest)
err = json.Unmarshal(remoteDescriptor.Manifest, manifest)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal attestation: %w", err)
}
attestationImage, err := remoteDescriptor.Image()
if err != nil {
return nil, fmt.Errorf("failed to get attestation image: %w", err)
}
attest := &AttestationManifest{
Name: image,
Image: attestationImage,
Manifest: manifest,
Descriptor: &remoteDescriptor.Descriptor,
Digest: digest,
Platform: platform,
}
return attest, nil
}
func withOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
// prepare options
options := []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain), remote.WithTransport(HttpTransport()), remote.WithContext(ctx)}
@@ -298,11 +49,9 @@ func withOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
func ExtractEnvelopes(ia *AttestationManifest, predicateType string) ([]*att.Envelope, error) {
manifest := ia.Manifest
im := ia.Image
image := ia.Image
var envs []*att.Envelope
ls, err := im.Layers()
layers, err := image.Layers()
if err != nil {
return nil, fmt.Errorf("failed to get layers: %w", err)
}
@@ -310,7 +59,7 @@ func ExtractEnvelopes(ia *AttestationManifest, predicateType string) ([]*att.Env
if (strings.HasPrefix(string(l.MediaType), "application/vnd.in-toto.")) &&
strings.HasSuffix(string(l.MediaType), "+dsse") &&
l.Annotations[InTotoPredicateType] == predicateType {
reader, err := ls[i].Uncompressed()
reader, err := layers[i].Uncompressed()
if err != nil {
return nil, fmt.Errorf("failed to get layer contents: %w", err)
}
@@ -329,7 +78,7 @@ func ExtractEnvelopes(ia *AttestationManifest, predicateType string) ([]*att.Env
func imageDigestForPlatform(ix *v1.IndexManifest, platform *v1.Platform) (string, error) {
for _, m := range ix.Manifests {
if m.MediaType == ocispec.MediaTypeImageManifest || m.MediaType == "application/vnd.docker.distribution.manifest.v2+json" && m.Platform.Equals(*platform) {
if (m.MediaType == ocispec.MediaTypeImageManifest || m.MediaType == "application/vnd.docker.distribution.manifest.v2+json") && m.Platform.Equals(*platform) {
return m.Digest.String(), nil
}
}
@@ -338,8 +87,8 @@ func imageDigestForPlatform(ix *v1.IndexManifest, platform *v1.Platform) (string
func attestationDigestForDigest(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) {
for _, m := range ix.Manifests {
if v, ok := m.Annotations["vnd.docker.reference.type"]; ok && v == attestType {
if d, ok := m.Annotations["vnd.docker.reference.digest"]; ok && d == imageDigest {
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
}
}
@@ -347,7 +96,7 @@ func attestationDigestForDigest(ix *v1.IndexManifest, imageDigest string, attest
return "", errors.New(fmt.Sprintf("no attestation found for image %s", imageDigest))
}
func RefToPURL(ref string, platform string) (string, bool, error) {
func RefToPURL(ref string, platform *v1.Platform) (string, bool, error) {
var isCanonical bool
named, err := reference.ParseNormalizedNamed(ref)
if err != nil {
@@ -379,17 +128,23 @@ func RefToPURL(ref string, platform string) (string, bool, error) {
}
name = parts[len(parts)-1]
pf, err := parsePlatform(platform)
if err != nil {
return "", false, fmt.Errorf("failed to parse platform %q: %w", platform, err)
}
if pf != nil {
if platform != nil {
qualifiers = append(qualifiers, packageurl.Qualifier{
Key: "platform",
Value: pf.String(),
Value: platform.String(),
})
}
p := packageurl.NewPackageURL("docker", ns, name, version, qualifiers, "")
return p.ToString(), isCanonical, nil
}
func SplitDigest(digest string) (common.DigestSet, error) {
parts := strings.SplitN(digest, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid digest %q", digest)
}
return common.DigestSet{
parts[0]: parts[1],
}, nil
}

View File

@@ -1,49 +1,108 @@
package oci
import (
"path/filepath"
"testing"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRefToPurl(t *testing.T) {
purl, canonical, err := RefToPURL("alpine", "arm64/linux")
arm, err := ParsePlatform("arm64/linux")
require.NoError(t, err)
purl, canonical, err := RefToPURL("alpine", 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", "arm64/linux")
purl, canonical, err = RefToPURL("alpine:123", 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", "arm64/linux")
purl, canonical, err = RefToPURL("google/alpine:123", 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", "arm64/linux")
purl, canonical, err = RefToPURL("library/alpine:123", 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", "arm64/linux")
purl, canonical, err = RefToPURL("docker.io/library/alpine:123", 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", "arm64/linux")
purl, canonical, err = RefToPURL("localhost:5001/library/alpine:123", 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", "arm64/linux")
purl, canonical, err = RefToPURL("localhost:5001/alpine:123", 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", "arm64/linux")
purl, canonical, err = RefToPURL("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b", 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)
assert.NoError(t, err)
idxm, err := idx.IndexManifest()
assert.NoError(t, err)
idxDescriptor := idxm.Manifests[0]
idxDigest := idxDescriptor.Digest
mfs, err := idx.ImageIndex(idxDigest)
assert.NoError(t, err)
mfs2, err := mfs.IndexManifest()
assert.NoError(t, err)
p, err := ParsePlatform("linux/amd64")
assert.NoError(t, err)
digest, err := imageDigestForPlatform(mfs2, p)
assert.NoError(t, err)
assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", digest)
p, err = ParsePlatform("linux/arm64")
assert.NoError(t, err)
digest, err = imageDigestForPlatform(mfs2, p)
assert.NoError(t, err)
assert.Equal(t, "sha256:7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e", digest)
}
func TestWithoutTag(t *testing.T) {
tc := []struct {
name string
expected string
}{
{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: "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"},
}
for _, c := range tc {
t.Run(c.name, func(t *testing.T) {
notag, _ := WithoutTag(c.name)
assert.Equal(t, c.expected, notag)
})
}
}

121
pkg/oci/referrers.go Normal file
View File

@@ -0,0 +1,121 @@
package oci
import (
"context"
"fmt"
att "github.com/docker/attest/pkg/attestation"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/pkg/errors"
)
type ReferrersResolver struct {
digest string
referrersRepo string
manifests []*AttestationManifest
*RegistryImageDetailsResolver
}
func NewReferrersAttestationResolver(src *RegistryImageDetailsResolver, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) {
res := &ReferrersResolver{
RegistryImageDetailsResolver: src,
}
for _, opt := range options {
err := opt(res)
if err != nil {
return nil, err
}
}
return res, nil
}
func WithReferrersRepo(repo string) func(*ReferrersResolver) error {
return func(r *ReferrersResolver) error {
r.referrersRepo = repo
return nil
}
}
func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error {
if r.manifests == nil {
subjectRef, err := name.ParseReference(r.Identifier)
if err != nil {
return fmt.Errorf("failed to parse reference: %w", err)
}
subjectDigest, err := r.ImageDigest(ctx)
if err != nil {
return 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))
if err != nil {
return fmt.Errorf("failed to create referrers reference: %w", err)
}
} else {
referrersSubjectRef = subjectRef.Context().Digest(subjectDigest)
}
referrersIndex, err := remote.Referrers(referrersSubjectRef)
if err != nil {
return fmt.Errorf("failed to get referrers: %w", err)
}
referrersIndexManifest, err := referrersIndex.IndexManifest()
if err != nil {
return fmt.Errorf("failed to get index manifest: %w", err)
}
if len(referrersIndexManifest.Manifests) == 0 {
return errors.New("no referrers found")
}
aManifests := make([]*AttestationManifest, 0)
for _, m := range referrersIndexManifest.Manifests {
remoteRef := referrersSubjectRef.Context().Digest(m.Digest.String())
attestationImage, err := remote.Image(remoteRef)
if err != nil {
return fmt.Errorf("failed to get referred image: %w", err)
}
manifest, err := attestationImage.Manifest()
if err != nil {
return fmt.Errorf("failed to get manifest: %w", err)
}
if manifest.Annotations[att.DockerReferenceType] != AttestationManifestType {
continue
}
if manifest.Annotations[att.DockerReferenceDigest] != subjectDigest {
continue
}
attest := &AttestationManifest{
Name: r.Identifier,
Image: attestationImage,
Manifest: manifest,
Descriptor: &m,
Digest: subjectDigest,
Platform: r.Platform,
}
aManifests = append(aManifests, attest)
}
if len(aManifests) == 0 {
return errors.New("no attestation manifests found")
}
r.manifests = aManifests
}
return nil
}
func (r *ReferrersResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
err := r.resolveAttestations(ctx)
if err != nil {
return nil, fmt.Errorf("failed to resolve attestations: %w", err)
}
var envs []*att.Envelope
for _, attest := range r.manifests {
es, err := ExtractEnvelopes(attest, predicateType)
if err != nil {
return nil, fmt.Errorf("failed to extract envelopes: %w", err)
}
envs = append(envs, es...)
}
return envs, nil
}

129
pkg/oci/registry.go Normal file
View File

@@ -0,0 +1,129 @@
package oci
import (
"context"
"encoding/json"
"fmt"
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
*AttestationManifest
}
type RegistryImageDetailsResolver struct {
*ImageSpec
digest string
}
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) ImageDigest(ctx context.Context) (string, error) {
if r.digest == "" {
subjectRef, err := name.ParseReference(r.Identifier)
if err != nil {
return "", fmt.Errorf("failed to parse reference: %w", err)
}
options := WithOptions(ctx, r.Platform)
desc, err := remote.Image(subjectRef, options...)
if err != nil {
return "", fmt.Errorf("failed to get image manifest: %w", err)
}
subjectDigest, err := desc.Digest()
if err != nil {
return "", fmt.Errorf("failed to get image digest: %w", err)
}
r.digest = subjectDigest.String()
}
return r.digest, 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) (*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)
}
digest, err := imageDigestForPlatform(indexManifest, platform)
if err != nil {
return nil, fmt.Errorf("failed to obtain image for platform: %w", err)
}
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)
}
manifest := new(v1.Manifest)
err = json.Unmarshal(remoteDescriptor.Manifest, manifest)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal attestation: %w", err)
}
attestationImage, err := remoteDescriptor.Image()
if err != nil {
return nil, fmt.Errorf("failed to get attestation image: %w", err)
}
attest := &AttestationManifest{
Name: image,
Image: attestationImage,
Manifest: manifest,
Descriptor: &remoteDescriptor.Descriptor,
Digest: digest,
Platform: platform,
}
return attest, nil
}

50
pkg/oci/registry_test.go Normal file
View File

@@ -0,0 +1,50 @@
package oci_test
import (
"fmt"
"net/http/httptest"
"net/url"
"strings"
"testing"
"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/google/go-containerregistry/pkg/registry"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRegistry(t *testing.T) {
ctx, signer := test.Setup(t)
server := httptest.NewServer(registry.New(registry.WithReferrersSupport(false)))
defer server.Close()
u, err := url.Parse(server.URL)
require.NoError(t, err)
opts := &attestation.SigningOptions{
Replace: true,
SkipSubject: true,
}
attIdx, err := oci.SubjectIndexFromPath(oci.UnsignedTestImage)
require.NoError(t, err)
signedIndex, err := attest.Sign(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
indexName := fmt.Sprintf("%s/repo:root", u.Host)
require.NoError(t, err)
err = mirror.PushIndexToRegistry(signedIndex, indexName)
require.NoError(t, err)
spec, err := oci.ParseImageSpec(indexName)
require.NoError(t, err)
resolver, err := policy.CreateImageDetailsResolver(spec)
require.NoError(t, err)
digest, err := resolver.ImageDigest(ctx)
require.NoError(t, err)
assert.True(t, strings.Contains(digest, "sha256:"))
}

View File

@@ -7,6 +7,10 @@ import (
v1 "github.com/google/go-containerregistry/pkg/v1"
)
type AttestationManifests struct {
Manifests []*AttestationManifest
}
type AttestationManifest struct {
// attestation image details
Image v1.Image
@@ -19,12 +23,16 @@ type AttestationManifest struct {
}
type AttestationResolver interface {
ImageName(ctx context.Context) (string, error)
ImagePlatformStr() string
ImageDigest(ctx context.Context) (string, error)
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)
ImageDigest(ctx context.Context) (string, error)
}
type MockResolver struct {
Envs []*att.Envelope
}
@@ -41,6 +49,6 @@ func (r MockResolver) ImageDigest(ctx context.Context) (string, error) {
return "sha256:test-digest", nil
}
func (r MockResolver) ImagePlatformStr() string {
return "linux/amd64"
func (r MockResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
return ParsePlatform("linux/amd64")
}

View File

@@ -2,7 +2,7 @@ package oci
import (
"fmt"
"log"
"strings"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
@@ -12,19 +12,38 @@ import (
)
const (
DockerReferenceType = "vnd.docker.reference.type"
DockerReferenceDigest = "vnd.docker.reference.digest"
AttestationManifestType = "attestation-manifest"
InTotoPredicateType = "in-toto.io/predicate-type"
OciReferenceTarget = "org.opencontainers.image.ref.name"
AttestationManifestType = "attestation-manifest"
InTotoPredicateType = "in-toto.io/predicate-type"
OciReferenceTarget = "org.opencontainers.image.ref.name"
LocalPrefix = "oci://"
RegistryPrefix = "docker://"
OCI SourceType = "OCI"
Docker SourceType = "Docker"
)
type AttestationIndex struct {
type SourceType string
type SubjectIndex struct {
Index v1.ImageIndex
Name string
}
func AttestationIndexFromPath(path string) (*AttestationIndex, error) {
type AttestationOptions struct {
NoReferrers bool
Attach bool
ReferrersRepo string
}
type ImageSpecOption func(*ImageSpec) error
type ImageSpec struct {
// OCI or Docker
Type SourceType
// without oci:// or docker:// (name or path)
Identifier string
Platform *v1.Platform
}
func SubjectIndexFromPath(path string) (*SubjectIndex, error) {
wrapperIdx, err := layout.ImageIndexFromPath(path)
if err != nil {
return nil, fmt.Errorf("failed to load image index: %w", err)
@@ -41,29 +60,129 @@ func AttestationIndexFromPath(path string) (*AttestationIndex, error) {
if err != nil {
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
}
return &AttestationIndex{
return &SubjectIndex{
Index: idx,
Name: imageName,
}, nil
}
func AttestationIndexFromRemote(image string) (*AttestationIndex, error) {
func SubjectIndexFromRemote(image string) (*SubjectIndex, error) {
ref, err := name.ParseReference(image)
if err != nil {
log.Fatalf("Failed to parse image name: %v", err)
return nil, fmt.Errorf("failed to parse image reference %s: %w", image, err)
}
// Get the authenticator from the default Docker keychain
auth, err := authn.DefaultKeychain.Resolve(ref.Context())
if err != nil {
log.Fatalf("Failed to get authenticator: %v", err)
return nil, fmt.Errorf("failed to resolve auth for image %s: %w", image, err)
}
// Pull the image from the registry
idx, err := remote.Index(ref, remote.WithAuth(auth))
if err != nil {
return nil, fmt.Errorf("failed to pull image %s: %w", image, err)
}
return &AttestationIndex{
return &SubjectIndex{
Index: idx,
Name: image,
}, nil
}
func LoadSubjectIndex(input *ImageSpec) (*SubjectIndex, error) {
if input.Type == OCI {
return SubjectIndexFromPath(input.Identifier)
} else {
return SubjectIndexFromRemote(input.Identifier)
}
}
func (i *ImageSpec) ForPlatforms(platform string) ([]*ImageSpec, error) {
platforms := strings.Split(platform, ",")
var specs []*ImageSpec
for _, pStr := range platforms {
p, err := ParsePlatform(pStr)
if err != nil {
return nil, err
}
spec := &ImageSpec{
Type: i.Type,
Identifier: i.Identifier,
Platform: p,
}
specs = append(specs, spec)
}
return specs, nil
}
func ParseImageSpec(img string, options ...ImageSpecOption) (*ImageSpec, error) {
img = strings.TrimSpace(img)
if strings.Contains(img, ",") {
return nil, fmt.Errorf("only one image is supported")
}
withoutPrefix := strings.TrimPrefix(strings.TrimPrefix(img, LocalPrefix), RegistryPrefix)
src := &ImageSpec{
Identifier: withoutPrefix,
}
if strings.HasPrefix(img, LocalPrefix) {
src.Type = OCI
} else {
src.Type = Docker
}
for _, option := range options {
err := option(src)
if err != nil {
return nil, err
}
}
if src.Platform == nil {
platform, err := ParsePlatform("")
if err != nil {
return nil, err
}
src.Platform = platform
}
return src, nil
}
func WithPlatform(platform string) ImageSpecOption {
return func(i *ImageSpec) error {
if strings.Contains(platform, ",") {
return fmt.Errorf("only one platform is supported")
}
p, err := ParsePlatform(platform)
if err != nil {
return err
}
i.Platform = p
return nil
}
}
func ParseImageSpecs(img string) ([]*ImageSpec, error) {
outputs := strings.Split(img, ",")
var sources []*ImageSpec
for _, output := range outputs {
src, err := ParseImageSpec(output)
if err != nil {
return nil, err
}
sources = append(sources, src)
}
return sources, nil
}
func WithoutTag(image string) (string, error) {
if strings.HasPrefix(image, LocalPrefix) {
return image, nil
}
prefix := ""
if strings.HasPrefix(image, RegistryPrefix) {
image = strings.TrimPrefix(image, RegistryPrefix)
prefix = RegistryPrefix
}
ref, err := name.ParseReference(image)
if err != nil {
return "", err
}
repo := ref.Context().Name()
return prefix + repo, nil
}

View File

@@ -26,5 +26,5 @@ func GetPolicyEvaluator(ctx context.Context) (PolicyEvaluator, error) {
}
type PolicyEvaluator interface {
Evaluate(ctx context.Context, resolver oci.AttestationResolver, policy []*PolicyFile, input *PolicyInput) error
Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *PolicyInput) (*Result, error)
}

32
pkg/policy/mock.go Normal file
View File

@@ -0,0 +1,32 @@
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

@@ -6,78 +6,22 @@ import (
"os"
"path"
"path/filepath"
"slices"
"strings"
"github.com/distribution/reference"
"github.com/docker/attest/internal/util"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/tuf"
goyaml "gopkg.in/yaml.v3"
)
const (
PolicyMappingFileName = "mapping.yaml"
)
var (
PolicyFileNames = []string{"data.yaml", "policy.rego"}
)
type PolicyMappings struct {
Version string `json:"version"`
Kind string `json:"kind"`
Policies []PolicyMapping `json:"policies"`
Mirrors []PolicyMirror `json:"mirrors"`
}
type PolicyMapping struct {
Name string `json:"namespace"`
Location string `json:"location"`
Description string `json:"description"`
Origin PolicyOrigin `json:"origin"`
}
type PolicyMirror struct {
Name string `json:"name"`
Mirror MirrorSpec `json:"mirror"`
}
type MirrorSpec struct {
Domains []string `json:"domains"`
Prefix string `json:"prefix"`
}
type PolicyOrigin struct {
Name string `json:"name"`
Prefix string `json:"prefix"`
Domain string `json:"domain"`
}
type PolicyOptions struct {
TufClient tuf.TUFClient
LocalTargetsDir string
LocalPolicyDir string
}
type PolicyInput struct {
Digest string `json:"digest"`
Purl string `json:"purl"`
IsCanonical bool `json:"isCanonical"`
}
type PolicyFile struct {
Path string
Content []byte
}
func resolveLocalPolicy(opts *PolicyOptions, mapping *PolicyMapping) ([]*PolicyFile, error) {
func resolveLocalPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*Policy, error) {
if opts.LocalPolicyDir == "" {
return nil, fmt.Errorf("local policy dir not set")
}
files := make([]*PolicyFile, 0, len(PolicyFileNames))
for _, filename := range PolicyFileNames {
filePath := path.Join(opts.LocalPolicyDir, mapping.Location, filename)
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)
@@ -87,31 +31,18 @@ func resolveLocalPolicy(opts *PolicyOptions, mapping *PolicyMapping) ([]*PolicyF
Content: fileContents,
})
}
return files, nil
policy := &Policy{
InputFiles: files,
Mapping: mapping,
}
return policy, nil
}
func loadLocalMappings(opts *PolicyOptions) (*PolicyMappings, error) {
if opts.LocalPolicyDir == "" {
return nil, nil
}
mappings := &PolicyMappings{}
path := path.Join(opts.LocalPolicyDir, PolicyMappingFileName)
mappingFile, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read policy mapping file %s: %w", path, err)
}
err = goyaml.Unmarshal(mappingFile, mappings)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", path, err)
}
return mappings, nil
}
func resolveTufPolicy(opts *PolicyOptions, mapping *PolicyMapping) ([]*PolicyFile, error) {
files := make([]*PolicyFile, 0, len(PolicyFileNames))
for _, filename := range PolicyFileNames {
filePath := path.Join(mapping.Location, filename)
_, fileContents, err := opts.TufClient.DownloadTarget(filePath, filepath.Join(opts.LocalTargetsDir, filePath))
func resolveTufPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*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)
}
@@ -120,50 +51,76 @@ func resolveTufPolicy(opts *PolicyOptions, mapping *PolicyMapping) ([]*PolicyFil
Content: fileContents,
})
}
return files, nil
policy := &Policy{
InputFiles: files,
Mapping: mapping,
}
return policy, nil
}
func loadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMappings, error) {
filename := PolicyMappingFileName
_, fileContents, err := tufClient.DownloadTarget(filename, filepath.Join(localTargetsDir, filename))
if err != nil {
return nil, fmt.Errorf("failed to download policy file %s: %w", filename, err)
}
mappings := &PolicyMappings{}
err = goyaml.Unmarshal(fileContents, mappings)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", filename, err)
}
return mappings, nil
}
func findPolicyMatch(named reference.Named, mappings *PolicyMappings) (*PolicyMapping, *PolicyMirror) {
func findPolicyMatch(named reference.Named, mappings *config.PolicyMappings) (*config.PolicyMapping, *config.PolicyMirror) {
if mappings != nil {
for _, mapping := range mappings.Policies {
if mapping.Origin.Domain == reference.Domain(named) &&
strings.HasPrefix(reference.Path(named), mapping.Origin.Prefix) {
return &mapping, nil
return mapping, nil
}
}
// now search mirrors
for _, mirror := range mappings.Mirrors {
if util.StringInSlice(reference.Domain(named), mirror.Mirror.Domains) &&
if (slices.Contains(mirror.Mirror.Domains, reference.Domain(named)) ||
slices.Contains(mirror.Mirror.Domains, "*")) &&
strings.HasPrefix(reference.Path(named), mirror.Mirror.Prefix) {
for _, mapping := range mappings.Policies {
if mapping.Name == mirror.Name {
return &mapping, nil
if mapping.Id == mirror.PolicyId {
return mapping, nil
}
}
return nil, &mirror
return nil, mirror
}
}
}
return nil, nil
}
func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *PolicyOptions) ([]*PolicyFile, error) {
imageName, err := resolver.ImageName(ctx)
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 {
for _, mapping := range localMappings.Policies {
if mapping.Id == opts.PolicyId {
return resolveLocalPolicy(opts, mapping)
}
}
}
// 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)
}
for _, mapping := range tufMappings.Policies {
if mapping.Id == opts.PolicyId {
return resolveTufPolicy(opts, mapping)
}
}
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)
}
@@ -171,7 +128,7 @@ func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *
if err != nil {
return nil, fmt.Errorf("failed to parse image name: %w", err)
}
localMappings, err := loadLocalMappings(opts)
localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir)
if err != nil {
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
}
@@ -180,16 +137,16 @@ func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *
return resolveLocalPolicy(opts, mapping)
}
// must check tuf
tufMappings, err := loadTufMappings(opts.TufClient, opts.LocalTargetsDir)
tufMappings, err := config.LoadTufMappings(opts.TufClient, opts.LocalTargetsDir)
if err != nil {
return nil, fmt.Errorf("failed to load tuf policy mappings: %w", err)
return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err)
}
// it's a mirror of a tuf policy
if mirror != nil {
for _, mapping := range tufMappings.Policies {
if mapping.Name == mirror.Name {
return resolveTufPolicy(opts, &mapping)
if mapping.Id == mirror.PolicyId {
return resolveTufPolicy(opts, mapping)
}
}
}
@@ -201,3 +158,32 @@ func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *
}
return resolveTufPolicy(opts, mapping)
}
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

@@ -8,10 +8,12 @@ import (
"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 {
@@ -30,80 +32,83 @@ func loadAttestation(t *testing.T, path string) *attestation.Envelope {
func TestRegoEvaluator_Evaluate(t *testing.T) {
ctx, _ := test.Setup(t)
errorStr := "failed to resolve policy by id: policy with id non-existent-policy-id not found"
TestDataPath := filepath.Join("..", "..", "test", "testdata")
MockTufRepo := filepath.Join(TestDataPath, "local-policy")
ExampleAttestation := filepath.Join(TestDataPath, "example_attestation.json")
VSA := filepath.Join(TestDataPath, "vsa.json")
re := policy.NewRegoEvaluator(true)
defaultInput := &policy.PolicyInput{
Digest: "sha256:test-digest",
Purl: "test-purl",
IsCanonical: true,
}
defaultResolver := oci.MockResolver{
Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)},
}
vsaResolver := oci.MockResolver{
Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation), loadAttestation(t, VSA)},
}
testCases := []struct {
repo string
expectSuccess bool
input *policy.PolicyInput
isCanonical bool
resolver oci.AttestationResolver
policy *policy.PolicyOptions
policyId string
errorStr string
}{
{repo: "testdata/mock-tuf-allow", expectSuccess: true, input: defaultInput, resolver: defaultResolver},
{repo: "testdata/mock-tuf-deny", expectSuccess: false, input: defaultInput, resolver: defaultResolver},
{repo: "testdata/mock-tuf-verify-sig", expectSuccess: true, input: defaultInput, resolver: defaultResolver},
{repo: "testdata/mock-tuf-wrong-key", expectSuccess: false, input: defaultInput, resolver: defaultResolver},
{repo: MockTufRepo, expectSuccess: true, input: &policy.PolicyInput{
Digest: "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620",
Purl: "pkg:docker/test-image@test?platform=linux%2Famd64",
IsCanonical: true,
}, resolver: vsaResolver},
{repo: MockTufRepo, expectSuccess: true, input: &policy.PolicyInput{
Digest: "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620",
Purl: "pkg:docker/test-image@test?platform=linux%2Famd64",
IsCanonical: false,
}, resolver: vsaResolver},
// not a doi
{repo: MockTufRepo, expectSuccess: false, input: defaultInput, resolver: vsaResolver, policy: &policy.PolicyOptions{
LocalPolicyDir: "testdata/mock-tuf-deny",
}},
// digest mismatch
{repo: MockTufRepo, expectSuccess: false, input: &policy.PolicyInput{
Digest: "sha256:test-digest-wrong",
Purl: "test-purl",
IsCanonical: false,
}, resolver: vsaResolver},
{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)
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")
result, err := re.Evaluate(ctx, tc.resolver, policy, input)
require.NoErrorf(t, err, "Evaluate failed")
policyFiles, err := policy.ResolvePolicy(ctx, tc.resolver, tc.policy)
assert.NoErrorf(t, err, "failed to resolve policy")
err = re.Evaluate(ctx, tc.resolver, policyFiles, tc.input)
if tc.expectSuccess {
assert.NoErrorf(t, err, "Evaluate failed")
assert.True(t, result.Success, "Evaluate should have succeeded")
} else {
assert.Errorf(t, err, "Evaluate should have failed")
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.Mirrors), 1)
for _, mirror := range policyMappings.Mirrors {
assert.Equal(t, "docker-official-images", mirror.PolicyId)
}
}

View File

@@ -23,16 +23,20 @@ import (
type regoEvaluator struct {
debug bool
query string
}
const (
DefaultQuery = "result := data.attest.result"
resultBinding = "result"
)
func NewRegoEvaluator(debug bool) PolicyEvaluator {
return &regoEvaluator{
debug: debug,
}
}
func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, files []*PolicyFile, input *PolicyInput) error {
func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *PolicyInput) (*Result, error) {
var regoOpts []func(*rego.Rego)
// Create a new in-memory store
@@ -41,19 +45,19 @@ func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationR
params.Write = true
txn, err := store.NewTransaction(ctx, params)
if err != nil {
return err
return nil, err
}
for _, target := range files {
for _, target := range pctx.InputFiles {
// load yaml as data (no rego opt for this!?)
if filepath.Ext(target.Path) == ".yaml" {
yamlData, err := loadYAML(target.Path, target.Content)
if err != nil {
return err
return nil, err
}
err = store.Write(ctx, txn, storage.AddOp, storage.Path{}, yamlData)
if err != nil {
return err
return nil, err
}
} else {
regoOpts = append(regoOpts, rego.Module(target.Path, string(target.Content)))
@@ -63,7 +67,7 @@ func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationR
err = store.Commit(ctx, txn)
if err != nil {
store.Abort(ctx, txn)
return err
return nil, err
}
if re.debug {
@@ -73,12 +77,15 @@ func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationR
rego.Dump(os.Stderr),
)
}
query := DefaultQuery
if pctx.Query != "" {
query = pctx.Query
}
regoOpts = append(regoOpts,
rego.Query("data.docker.allow"),
rego.StrictBuiltinErrors(true),
rego.Query(query),
rego.Input(input),
rego.Store(store),
rego.GenerateJSON(jsonGenerator[Result]()),
)
for _, custom := range RegoFunctions(resolver) {
regoOpts = append(regoOpts, custom.Func)
@@ -87,29 +94,81 @@ func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationR
r := rego.New(regoOpts...)
rs, err := r.Eval(ctx)
if err != nil {
return fmt.Errorf("error from Eval: %w", err)
return nil, err
}
if !rs.Allowed() {
return fmt.Errorf("policy evaluation failed")
if len(rs) == 0 {
return nil, fmt.Errorf("no policy evaluation result")
}
binding, ok := rs[0].Bindings[resultBinding]
if !ok {
return nil, fmt.Errorf("failed to extract verification result")
}
result, ok := binding.(Result)
if !ok {
return nil, fmt.Errorf("failed to extract verification result")
}
return nil
return &result, nil
}
var dynamicObj = types.NewObject(nil, types.NewDynamicProperty(types.S, types.A))
var arrayObj = types.NewArray(nil, dynamicObj)
func jsonGenerator[T any]() func(t *ast.Term, ec *rego.EvalContext) (any, error) {
return func(t *ast.Term, ec *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)
if err != nil {
return nil, err
}
v, err := ast.InterfaceToValue(json)
if err != nil {
return nil, err
}
var result T
err = ast.As(v, &result)
if err != nil {
return nil, err
}
return result, nil
}
}
var dynamicObj = types.NewObject(nil, types.NewDynamicProperty(types.A, types.A))
var verifyDecl = &ast.Builtin{
Name: "attestations.verify_envelope",
Decl: types.NewFunction(types.Args(dynamicObj, arrayObj), dynamicObj),
Name: "attest.verify",
Decl: types.NewFunction(types.Args(dynamicObj, dynamicObj), dynamicObj),
Nondeterministic: true,
}
var attestDecl = &ast.Builtin{
Name: "attestations.attestation",
Name: "attest.fetch",
Decl: types.NewFunction(types.Args(types.S), dynamicObj),
Nondeterministic: true,
}
func wrapFunctionResult(value *ast.Term, err error) (*ast.Term, error) {
var terms [][2]*ast.Term
if err != nil {
terms = append(terms, [2]*ast.Term{ast.StringTerm("error"), ast.StringTerm(err.Error())})
}
if value != nil {
terms = append(terms, [2]*ast.Term{ast.StringTerm("value"), value})
}
return ast.ObjectTerm(terms...), nil
}
func handleErrors1(f func(rCtx rego.BuiltinContext, a *ast.Term) (*ast.Term, error)) rego.Builtin1 {
return func(rCtx rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
return wrapFunctionResult(f(rCtx, a))
}
}
func handleErrors2(f func(rCtx rego.BuiltinContext, a, b *ast.Term) (*ast.Term, error)) rego.Builtin2 {
return func(rCtx rego.BuiltinContext, a, b *ast.Term) (*ast.Term, error) {
return wrapFunctionResult(f(rCtx, a, b))
}
}
func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin {
return []*tester.Builtin{
{
@@ -121,7 +180,7 @@ func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin {
Memoize: true,
Nondeterministic: verifyDecl.Nondeterministic,
},
verifyIntotoEnvelope),
handleErrors2(verifyIntotoEnvelope)),
},
{
Decl: attestDecl,
@@ -132,12 +191,12 @@ func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin {
Memoize: true,
Nondeterministic: attestDecl.Nondeterministic,
},
fetchIntotoAttestations(resolver)),
handleErrors1(fetchIntotoAttestations(resolver))),
},
}
}
func fetchIntotoAttestations(resolver oci.AttestationResolver) func(rego.BuiltinContext, *ast.Term) (*ast.Term, error) {
func fetchIntotoAttestations(resolver oci.AttestationResolver) rego.Builtin1 {
return func(rCtx rego.BuiltinContext, predicateTypeTerm *ast.Term) (*ast.Term, error) {
predicateTypeStr, ok := predicateTypeTerm.Value.(ast.String)
if !ok {
@@ -160,28 +219,26 @@ func fetchIntotoAttestations(resolver oci.AttestationResolver) func(rego.Builtin
values[i] = ast.NewTerm(value)
}
// Wrap the values in an ast.Array and convert it to an ast.Term.
array := ast.NewTerm(ast.NewArray(values...))
// Wrap the values in an ast.Set and convert it to an ast.Term.
set := ast.NewTerm(ast.NewSet(values...))
return array, nil
return set, nil
}
}
func verifyIntotoEnvelope(rCtx rego.BuiltinContext, envTerm, keysTerm *ast.Term) (*ast.Term, error) {
func verifyIntotoEnvelope(rCtx rego.BuiltinContext, envTerm, optsTerm *ast.Term) (*ast.Term, error) {
env := new(att.Envelope)
var keys att.Keys
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(keysTerm.Value, &keys)
err = ast.As(optsTerm.Value, &opts)
if err != nil {
return nil, fmt.Errorf("failed to cast keys: %w", err)
return nil, fmt.Errorf("failed to cast verifier options: %w", err)
}
keysmap := make(map[string]att.KeyMetadata, len(keys))
for _, key := range keys {
keysmap[key.ID] = key
}
payload, err := att.VerifyDSSE(rCtx.Context, env, keysmap)
payload, err := att.VerifyDSSE(rCtx.Context, env, opts)
if err != nil {
return nil, err
}
@@ -203,7 +260,6 @@ func verifyIntotoEnvelope(rCtx rego.BuiltinContext, envTerm, keysTerm *ast.Term)
if err != nil {
return nil, err
}
return ast.NewTerm(value), nil
}

View File

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

View File

@@ -0,0 +1,16 @@
# map repos to policies
version: v1
kind: policy-mapping
policies:
- origin:
domain: docker.io
prefix: library/
id: docker-official-images
description: Docker Official Images
files:
- path: doi/policy.rego
mirrors:
- policy-id: docker-official-images
mirror:
domains: [localhost:5001, registry.local:5000]
prefix: ""

View File

@@ -1 +0,0 @@
config:

View File

@@ -1,5 +1,7 @@
package docker
package attest
import rego.v1
allow := true
result := {
"success": true,
}

View File

@@ -5,6 +5,14 @@ policies:
- origin:
domain: docker.io
prefix: library/
name: docker-official-images
id: docker-official-images
description: Docker Official Images
location: doi
attestations:
repo: "localhost:5001/library-refs"
files:
- path: doi/policy.rego
mirrors:
- policy-id: docker-official-images
mirror:
domains: [localhost:5001, registry.local:5000]
prefix: ""

View File

@@ -1 +0,0 @@
config:

View File

@@ -1,5 +1,7 @@
package docker
package attest
import rego.v1
allow := false
result := {
"success": false,
}

View File

@@ -5,6 +5,7 @@ policies:
- origin:
domain: docker.io
prefix: library/
name: docker-official-images
id: docker-official-images
description: Docker Official Images
location: doi
files:
- path: doi/policy.rego

View File

@@ -1 +0,0 @@
config:

View File

@@ -1,15 +1,19 @@
package docker
package attest
import rego.v1
keys := [{
"id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4",
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgH23D1i2+ZIOtVjmfB7iFvX8AhVN\n9CPJ4ie9axw+WRHozGnRy99U2dRge3zueBBg2MweF0zrToXGig2v3YOrdw==\n-----END PUBLIC KEY-----",
"from": "2023-12-15T14:00:00Z",
"to": null
"id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4",
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgH23D1i2+ZIOtVjmfB7iFvX8AhVN\n9CPJ4ie9axw+WRHozGnRy99U2dRge3zueBBg2MweF0zrToXGig2v3YOrdw==\n-----END PUBLIC KEY-----",
"from": "2023-12-15T14:00:00Z",
"to": null,
}]
allow if {
some env in attestations.attestation("foo")
statement := attestations.verify_envelope(env, keys)
opts := {"keys": keys}
success if {
some env in attest.fetch("foo")
statement := attest.verify(env, opts)
}
result := {"success": success}

View File

@@ -5,6 +5,7 @@ policies:
- origin:
domain: docker.io
prefix: library/
name: docker-official-images
id: docker-official-images
description: Docker Official Images
location: doi
files:
- path: doi/policy.rego

View File

@@ -1 +0,0 @@
config:

View File

@@ -1,19 +1,30 @@
package docker
package attest
import rego.v1
keys := {
"a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4": {
"id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4",
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHyZpSgzvqFqNv7f3x7865OS38rAb\nQMcff55zM2UH/KR3Pr84a8QsGDNgaNGzJQJWjtMSgfV8WnNoffNK+svFNg==\n-----END PUBLIC KEY-----",
"from": "2023-12-15T14:00:00Z",
"to": null,
}
keys := [{
"id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4",
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHyZpSgzvqFqNv7f3x7865OS38rAb\nQMcff55zM2UH/KR3Pr84a8QsGDNgaNGzJQJWjtMSgfV8WnNoffNK+svFNg==\n-----END PUBLIC KEY-----",
"from": "2023-12-15T14:00:00Z",
"to": null,
}]
default success := false
provs(pred) := p if {
res := attest.fetch(pred)
not res.error
p := res.value
}
allow if {
some env in attestations.attestation("foo")
statement := attestations.verify_envelope(env, keys)
atts := union({provs("foo")})
opts := {"keys": keys}
success if {
some env in atts
res := attest.verify(env, opts)
not res.error
}
allow := true
result := {"success": success}

View File

@@ -5,6 +5,7 @@ policies:
- origin:
domain: docker.io
prefix: library/
name: docker-official-images
id: docker-official-images
description: Docker Official Images
location: doi
files:
- path: doi/policy.rego

53
pkg/policy/types.go Normal file
View File

@@ -0,0 +1,53 @@
package policy
import (
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/tuf"
intoto "github.com/in-toto/in-toto-golang/in_toto"
)
type Summary struct {
Subjects []intoto.Subject `json:"subjects"`
SLSALevels []string `json:"slsa_levels"`
Verifier string `json:"verifier"`
PolicyURI string `json:"policy_uri"`
}
type Violation struct {
Type string `json:"type"`
Description string `json:"description"`
Attestation *intoto.Statement `json:"attestation"`
Details map[string]any `json:"details"`
}
type Result struct {
Success bool `json:"success"`
Violations []Violation `json:"violations"`
Summary Summary `json:"summary"`
}
type PolicyOptions struct {
TufClient tuf.TUFClient
LocalTargetsDir string
LocalPolicyDir string
PolicyId string
ReferrersRepo string
AttestationStyle config.AttestationStyle
}
type Policy struct {
InputFiles []*PolicyFile
Query string
Mapping *config.PolicyMapping
}
type PolicyInput struct {
Digest string `json:"digest"`
Purl string `json:"purl"`
IsCanonical bool `json:"isCanonical"`
}
type PolicyFile struct {
Path string
Content []byte
}

View File

@@ -6,6 +6,8 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
"github.com/docker/attest/internal/util"
@@ -38,13 +40,39 @@ func (s *ECDSA256_SignerVerifier) Verify(ctx context.Context, data []byte, sig [
if !ok {
return fmt.Errorf("public key is not ecdsa")
}
ok = ecdsa.VerifyASN1(pub, util.S256(data), sig)
ok = ecdsa.VerifyASN1(pub, util.SHA256(data), sig)
if !ok {
return fmt.Errorf("payload signature is not valid")
}
return nil
}
func LoadKeyPair(priv []byte) (dsse.SignerVerifier, error) {
privateKey, err := parsePriv(priv)
if err != nil {
return nil, err
}
return &ECDSA256_SignerVerifier{
Signer: privateKey,
}, nil
}
func parsePriv(privkeyBytes []byte) (*ecdsa.PrivateKey, error) {
p, _ := pem.Decode(privkeyBytes)
if p == nil {
return nil, fmt.Errorf("privkey file does not contain any PEM data")
}
if p.Type != "EC PRIVATE KEY" {
return nil, fmt.Errorf("privkey file does not contain a priavte key")
}
privKey, err := x509.ParseECPrivateKey(p.Bytes)
if err != nil {
return nil, fmt.Errorf("error failed to parse public key: %w", err)
}
return privKey, nil
}
func GenKeyPair() (dsse.SignerVerifier, error) {
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {

View File

@@ -13,5 +13,5 @@ func KeyID(pubKey crypto.PublicKey) (string, error) {
if err != nil {
return "", fmt.Errorf("error marshalling public key: %w", err)
}
return util.HexHashBytes(pub), nil
return util.SHA256Hex(pub), nil
}

View File

@@ -7,7 +7,6 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"fmt"
"math/big"
@@ -214,7 +213,7 @@ func (tl *RekorTL) VerifyEntryPayload(entryBytes, payload, publicKey []byte) err
}
// compare payload hashes
payloadHash := hex.EncodeToString(util.S256(payload))
payloadHash := util.SHA256Hex(payload)
if rekord.Hash != payloadHash {
return fmt.Errorf("error payload and tl entry hash mismatch")
}

View File

@@ -44,7 +44,7 @@ func TestCreateX509Cert(t *testing.T) {
func TestUploadAndVerifyLogEntry(t *testing.T) {
// message digest
payload := []byte("test")
hash := util.S256(payload)
hash := util.SHA256(payload)
// generate ephemeral keys to sign message digest
signer, err := signerverifier.GenKeyPair()

View File

@@ -0,0 +1,45 @@
package tuf_test
import (
"os"
"path/filepath"
"github.com/docker/attest/internal/embed"
"github.com/docker/attest/pkg/tuf"
"github.com/theupdateframework/go-tuf/v2/metadata"
)
func ExampleNewTufClient_registry() {
// create a tuf client
home, err := os.UserHomeDir()
if err != nil {
panic(err)
}
tufOutputPath := filepath.Join(home, ".docker", "tuf")
// using oci tuf metadata and targets
metadataURI := "registry-1.docker.io/docker/tuf-metadata:latest"
targetsURI := "registry-1.docker.io/docker/tuf-targets"
registryClient, err := tuf.NewTufClient(embed.StagingRoot, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker())
if err != nil {
panic(err)
}
// get trusted tuf metadata
trustedMetadata := registryClient.GetMetadata()
if err != nil {
panic(err)
}
// top-level target files
targets := trustedMetadata.Targets[metadata.TARGETS].Signed.Targets
for _, t := range targets {
// download target files
_, _, err := registryClient.DownloadTarget(t.Path, filepath.Join(tufOutputPath, "download"))
if err != nil {
panic(err)
}
}
}

View File

@@ -58,3 +58,15 @@ func (dc *mockTufClient) DownloadTarget(target string, filePath string) (actualF
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

@@ -121,7 +121,7 @@ func TestFindFileInManifest(t *testing.T) {
// make test image manifest
file := "test.json"
data := []byte("test")
hash := v1.Hash{Algorithm: "sha256", Hex: util.HexHashBytes(data)}
hash := v1.Hash{Algorithm: "sha256", Hex: util.SHA256Hex(data)}
img := empty.Image
img = mutate.MediaType(img, types.OCIManifestSchema1)
img = mutate.ConfigMediaType(img, types.OCIConfigJSON)

View File

@@ -36,7 +36,7 @@ type TufClient struct {
}
// NewTufClient creates a new TUF client
func NewTufClient(initialRoot []byte, tufPath, metadataSource, targetsSource string) (*TufClient, error) {
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
@@ -44,7 +44,7 @@ func NewTufClient(initialRoot []byte, tufPath, metadataSource, targetsSource str
tufSource = OciSource
}
tufRootDigest := util.HexHashBytes(initialRoot)
tufRootDigest := util.SHA256Hex(initialRoot)
// create a directory for each initial root.json
metadataPath := filepath.Join(tufPath, tufRootDigest)
@@ -102,8 +102,13 @@ func NewTufClient(initialRoot []byte, tufPath, metadataSource, targetsSource str
updater: up,
cfg: cfg,
}
return client, nil
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
@@ -116,6 +121,14 @@ func (t *TufClient) DownloadTarget(target string, filePath string) (actualFilePa
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 {

View File

@@ -2,6 +2,7 @@ package tuf
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
@@ -10,6 +11,7 @@ import (
"github.com/docker/attest/internal/embed"
"github.com/stretchr/testify/assert"
"github.com/theupdateframework/go-tuf/v2/metadata"
)
var (
@@ -50,6 +52,9 @@ func TestRootInit(t *testing.T) {
}()
LoadRegistryTestData(t, regAddr, OciTufTestDataPath)
alwaysGoodVersionChecker := &mockVersionChecker{err: nil}
alwaysBadVersionChecker := &mockVersionChecker{err: assert.AnError}
testCases := []struct {
name string
metadataSource string
@@ -60,14 +65,71 @@ func TestRootInit(t *testing.T) {
}
for _, tc := range testCases {
_, err := NewTufClient(embed.DevRoot, tufPath, tc.metadataSource, tc.targetsSource)
_, err := NewTufClient(embed.DevRoot, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker)
assert.NoErrorf(t, err, "Failed to create TUF client: %v", err)
// recreation should work with same root
_, err = NewTufClient(embed.DevRoot, tufPath, tc.metadataSource, tc.targetsSource)
_, err = NewTufClient(embed.DevRoot, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker)
assert.NoErrorf(t, err, "Failed to recreate TUF client: %v", err)
_, err = NewTufClient([]byte("broken"), tufPath, tc.metadataSource, tc.targetsSource)
_, err = NewTufClient([]byte("broken"), tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker)
assert.Errorf(t, err, "Expected error recreating TUF client with broken root: %v", err)
_, err = NewTufClient(embed.DevRoot, tufPath, tc.metadataSource, tc.targetsSource, alwaysBadVersionChecker)
assert.Errorf(t, err, "Expected error creating TUF client with bad attest version: %v", err)
}
}
func TestDownloadTarget(t *testing.T) {
tufPath := CreateTempDir(t, "", "tuf_temp")
targetFile := "test.txt"
delegatedRole := "test-role"
delegatedTargetFile := fmt.Sprintf("%s/%s", delegatedRole, targetFile)
// Start a test HTTP server to serve data from /test/testdata/tuf/test-repo/ paths
server := httptest.NewServer(http.FileServer(http.Dir(HttpTufTestDataPath)))
defer server.Close()
// run local registry
registry, regAddr := RunTestRegistry(t)
defer func() {
if err := registry.Terminate(context.Background()); err != nil {
t.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()
LoadRegistryTestData(t, regAddr, OciTufTestDataPath)
alwaysGoodVersionChecker := &mockVersionChecker{err: nil}
testCases := []struct {
name string
metadataSource string
targetsSource string
}{
{"http", server.URL + "/metadata", server.URL + "/targets"},
{"oci", regAddr.Host + "/tuf-metadata:latest", regAddr.Host + "/tuf-targets"},
}
for _, tc := range testCases {
tufClient, err := NewTufClient(embed.DevRoot, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker)
assert.NoErrorf(t, err, "Failed to create TUF client: %v", err)
// get trusted tuf metadata
trustedMetadata := tufClient.updater.GetTrustedMetadataSet()
assert.NotNil(t, trustedMetadata, "Failed to get trusted metadata")
// download top-level target files
targets := trustedMetadata.Targets[metadata.TARGETS].Signed.Targets
for _, target := range targets {
// download target files
_, _, err := tufClient.DownloadTarget(target.Path, filepath.Join(tufPath, "download"))
assert.NoErrorf(t, err, "Failed to download target: %v", err)
}
// download delegated target
targetInfo, err := tufClient.updater.GetTargetInfo(delegatedTargetFile)
assert.NoError(t, err)
_, _, err = tufClient.DownloadTarget(targetInfo.Path, filepath.Join(tufPath, targetInfo.Path))
assert.NoError(t, err)
}
}

89
pkg/tuf/version.go Normal file
View File

@@ -0,0 +1,89 @@
package tuf
import (
"fmt"
"runtime/debug"
"strings"
"github.com/Masterminds/semver/v3"
)
const ThisModulePath = "github.com/docker/attest"
type VersionChecker interface {
// CheckVersion checks if the current version of this library meets the constraints from the TUF repo
CheckVersion(tufClient TUFClient) error
}
type InvalidVersionError struct {
AttestVersion string
VersionConstraint string
Errors []error
}
func (e *InvalidVersionError) Error() string {
var errsStr strings.Builder
for i, err := range e.Errors {
if i > 0 {
errsStr.WriteString("; ")
}
errsStr.WriteString(err.Error())
}
return fmt.Sprintf("%s version %s does not satisfy constraints %s: %s", ThisModulePath, e.AttestVersion, e.VersionConstraint, errsStr.String())
}
func NewVersionChecker() *versionChecker {
return &versionChecker{}
}
type versionChecker struct{}
func (vc *versionChecker) CheckVersion(client TUFClient) error {
var attestMod *debug.Module
bi, ok := debug.ReadBuildInfo()
if !ok {
// if we can't read the build info, assume we're good. this should only happen if we're not running in a module
return nil
}
if bi.Main.Path == ThisModulePath {
attestMod = &bi.Main
} else {
for _, dep := range bi.Deps {
if dep.Path == ThisModulePath {
attestMod = dep
break
}
}
}
if attestMod == nil {
// if we can't find the attest dep, assume we're good. this should only happen in a test
return nil
}
attestVersion, err := semver.NewVersion(attestMod.Version)
if err != nil {
return fmt.Errorf("failed to parse version %s: %w", attestMod.Version, err)
}
// see https://github.com/Masterminds/semver/blob/v3.2.1/README.md#checking-version-constraints
// for more information on the expected format of the version constraints in the TUF repo
_, versionConstraintsBytes, err := client.DownloadTarget("version-constraints", "")
if err != nil {
return fmt.Errorf("failed to download version-constraints: %w", err)
}
versionConstraints, err := semver.NewConstraint(string(versionConstraintsBytes))
if err != nil {
return fmt.Errorf("failed to parse minimum version: %w", err)
}
ok, errs := versionConstraints.Validate(attestVersion)
if !ok {
return &InvalidVersionError{
AttestVersion: attestVersion.String(),
VersionConstraint: versionConstraints.String(),
Errors: errs,
}
}
return nil
}

View File

@@ -0,0 +1,58 @@
package attest
import rego.v1
keys := [{
"id": "6b241993defaba26558c64f94a94303ce860e7ad9163d801495c91cf57197c75",
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZmicqYSY38DprGr42jU0V3ND0ROj\nzSRH1+yjsxhh0bi52Hh/DuOhrSq2KJ5a09lW3ybnDjljowbkof0Y1i9Oow==\n-----END PUBLIC KEY-----",
"from": "2023-12-15T14:00:00Z",
"to": null,
# this key is still active
"status": "active",
"signing-format": "dssev1",
}]
provs(pred) := p if {
res := attest.fetch(pred)
not res.error
p := res.value
}
atts := union({
provs("https://slsa.dev/provenance/v0.2"),
provs("https://spdx.dev/Document"),
})
opts := {"keys": keys}
statements contains s if {
some att in atts
res := attest.verify(att, opts)
not res.error
s := res.value
}
subjects contains subject if {
some statement in statements
some subject in statement.subject
}
violations contains v if {
v := {
"type": "missing_attestation",
"description": "Attestation missing for subject",
"attestation": null,
"details": {},
}
}
result := {
"success": false,
"violations": violations,
"summary": {
"subjects": subjects,
"slsa_levels": ["SLSA_BUILD_LEVEL_3"],
"verifier": "docker-official-images",
"policy_uri": "https://docker.com/official/policy/v0.1",
},
}

View File

@@ -0,0 +1,11 @@
# map repos to policies
version: v1
kind: policy-mapping
policies:
- origin:
domain: docker.io
prefix: library/
id: test-images
description: Local test images
files:
- path: doi/policy.rego

View File

@@ -0,0 +1,49 @@
package attest
import rego.v1
keys := [{
"id": "6b241993defaba26558c64f94a94303ce860e7ad9163d801495c91cf57197c75",
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZmicqYSY38DprGr42jU0V3ND0ROj\nzSRH1+yjsxhh0bi52Hh/DuOhrSq2KJ5a09lW3ybnDjljowbkof0Y1i9Oow==\n-----END PUBLIC KEY-----",
"from": "2023-12-15T14:00:00Z",
"to": null,
# this key is still active
"status": "active",
"signing-format": "dssev1",
}]
provs(pred) := p if {
res := attest.fetch(pred)
not res.error
p := res.value
}
atts := union({
provs("https://slsa.dev/provenance/v0.2"),
provs("https://spdx.dev/Document"),
})
opts := {"keys": keys, "skip_tl": true}
statements contains s if {
some att in atts
res := attest.verify(att, opts)
not res.error
s := res.value
}
subjects contains subject if {
some statement in statements
some subject in statement.subject
}
result := {
"success": true,
"violations": set(),
"summary": {
"subjects": subjects,
"slsa_levels": ["SLSA_BUILD_LEVEL_3"],
"verifier": "docker-official-images",
"policy_uri": "https://docker.com/official/policy/v0.1",
},
}

View File

@@ -0,0 +1,11 @@
# map repos to policies
version: v1
kind: policy-mapping
policies:
- origin:
domain: docker.io
prefix: library/
id: test-images
description: Local test images
files:
- path: doi/policy.rego

View File

@@ -0,0 +1,49 @@
package attest
import rego.v1
keys := [{
"id": "6b241993defaba26558c64f94a94303ce860e7ad9163d801495c91cf57197c75",
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZmicqYSY38DprGr42jU0V3ND0ROj\nzSRH1+yjsxhh0bi52Hh/DuOhrSq2KJ5a09lW3ybnDjljowbkof0Y1i9Oow==\n-----END PUBLIC KEY-----",
"from": "2023-12-15T14:00:00Z",
"to": null,
# this key is still active
"status": "active",
"signing-format": "dssev1",
}]
provs(pred) := p if {
res := attest.fetch(pred)
not res.error
p := res.value
}
atts := union({
provs("https://slsa.dev/provenance/v0.2"),
provs("https://spdx.dev/Document"),
})
opts := {"keys": keys}
statements contains s if {
some att in atts
res := attest.verify(att, opts)
not res.error
s := res.value
}
subjects contains subject if {
some statement in statements
some subject in statement.subject
}
result := {
"success": true,
"violations": set(),
"summary": {
"subjects": subjects,
"slsa_levels": ["SLSA_BUILD_LEVEL_3"],
"verifier": "docker-official-images",
"policy_uri": "https://docker.com/official/policy/v0.1",
},
}

View File

@@ -0,0 +1,16 @@
# map repos to policies
version: v1
kind: policy-mapping
policies:
- origin:
domain: docker.io
prefix: library/
id: test-images
description: Local test images
files:
- path: doi/policy.rego
mirrors:
- policy-id: test-images
mirror:
domains: ["*"]
prefix: ""

View File

@@ -1,58 +0,0 @@
config:
doi:
keys:
- id: "f6a29392b1c08891ff456100aa448b4f6bf9c315850e11cc0883fe9c3c4412db"
key: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE+XOm2uWjLJhpsJtHCFdGic26suOy
mCl2pBgCof+AHGFZFca40JL833OT+nRSZJRMPKBGibWqsjFrLdRCkOB7bA==
-----END PUBLIC KEY-----
from: "2024-01-01T00:00:00Z"
to: "2024-01-15T12:00:00Z"
# this key was rotated at a planned time
status: "rotated"
signing-format: "dssev1"
- id: "e6f4c70fbba21cbcac44915fff53fd2fdf90dd8849445795fe58014c2b5f8c64"
key: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZSkTE3si/JkRbuLjaYraS3//YBnX
8KtEcgdYKZQPl2DnSl4gPsu3KiVeEBWp5GK06IoZlcBAL3NF0OsUUP+yVg==
-----END PUBLIC KEY-----
from: "2024-01-15T12:00:00Z"
to: "2024-01-15T14:00:00Z"
# this key was leaked at a known time, so it revoked from that time
# this behaves the same way as "rotated" but might give another failure message
status: "revoked"
signing-format: "dssev1"
- id: "d45980c5cf39a5e1bab9febe3f16c1c0820b97a8fd061b0064e54b0826e856e4"
key: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEafssq2x1EDQcKDZhuSrCOxWWl5D4
JBa9iDJYDnLZp9kPKvv4RnD4rz7Ucfmd0l/zzM45qT29fSBTlguKmnOA8A==
-----END PUBLIC KEY-----
# this key was leaked at an unknown time, so it's completely distrusted
distrust: true
status: "revoked"
signing-format: "dssev1"
- id: "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4"
key: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgH23D1i2+ZIOtVjmfB7iFvX8AhVN
9CPJ4ie9axw+WRHozGnRy99U2dRge3zueBBg2MweF0zrToXGig2v3YOrdw==
-----END PUBLIC KEY-----
from: "2023-12-15T14:00:00Z"
to: null
# this key is still active
status: "active"
signing-format: "dssev1"
- id: "b281835e00059de24fb06bd6db06eb0e4a33d7bd7210d7027c209f14b19e812a"
key: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgE4Jz6FrLc3lp/YRlbuwOjK4n6ac
jVkSDAmFhi3Ir2Jy+cKeEB7iRPcLvBy9qoMZ9E93m1NdWY6KtDo+Qi52Rg==
-----END PUBLIC KEY-----
from: "2024-01-15T14:00:00Z"
to: null
# this key is still active
status: "active"
signing-format: "dssev1"

View File

@@ -1,49 +1,49 @@
package docker
package attest
import rego.v1
import data.config
keys := [{
"id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4",
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgH23D1i2+ZIOtVjmfB7iFvX8AhVN\n9CPJ4ie9axw+WRHozGnRy99U2dRge3zueBBg2MweF0zrToXGig2v3YOrdw==\n-----END PUBLIC KEY-----",
"from": "2023-12-15T14:00:00Z",
"to": null,
"status": "active",
"signing-format": "dssev1",
}]
splitDigest := split(input.digest, ":")
digestType := splitDigest[0]
digest := splitDigest[1]
allow if {
some env in attestations.attestation("https://slsa.dev/verification_summary/v0.1")
some statement in verified_statements(config.doi.keys, env)
provs(pred) := p if {
res := attest.fetch(pred)
not res.error
p := res.value
}
atts := union({
provs("https://slsa.dev/provenance/v0.2"),
provs("https://spdx.dev/Document"),
})
verified_statements(keys, env) := statements if {
statements := {statement |
statement := attestations.verify_envelope(env, keys)
some subject in statement.subject
valid_subject(subject)
}
opts := {"keys": keys}
statements contains s if {
some att in atts
res := attest.verify(att, opts)
not res.error
s := res.value
}
valid_subject(sub) if {
print("valid_subject")
print("sub.digest[digestType]:", sub.digest[digestType])
print("digest", digest)
sub.digest[digestType] == digest
print("digest matches")
valid_subject_name(sub.name)
subjects contains subject if {
some statement in statements
some subject in statement.subject
}
valid_subject_name(name) if {
input.canonical
print("is canonical, ignoring name")
}
valid_subject_name(name) if {
not input.canonical
print("valid_subject_name...")
print("name:", name)
print("input.purl:", input.purl)
name == input.purl
print("name match")
result := {
"success": count(atts) > 0,
"violations": set(),
"attestations": statements,
"summary": {
"subjects": subjects,
"slsa_level": "SLSA_BUILD_LEVEL_3",
"verifier": "docker-official-images",
"policy_uri": "https://docker.com/official/policy/v0.1",
},
}

View File

@@ -1,25 +0,0 @@
package docker
import rego.v1
config := {"keys": []}
envs := [{"env": "test"}]
purl := "pkg:docker/library/alpine:1.2.3"
statement := {"subject": [{"name": purl, "digest": {"sha256": "dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0"}}]}
input_digest := "sha256:dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0"
test_with_mock_data if {
allow with attestations.attestation as envs
with attestations.verify_envelope as statement
with input.digest as input_digest
with input.purl as purl
with input.canonical as false
}
layout_digest := "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620"
outout_purl := "pkg:docker/test-image@test?platform=linux%2Famd64"
test_with_signed_oci_layout if {
allow with input.digest as layout_digest
with input.purl as outout_purl
with input.canonical as false
}

View File

@@ -5,12 +5,25 @@ policies:
- origin:
domain: docker.io
prefix: library/
name: test-images
id: test-images
description: Local test images
location: doi
files:
- path: "doi/policy.rego"
mirrors:
- name: test-images
- policy-id: test-images
mirror:
domains: [localhost:5001]
prefix: ""
domains: ["*"]
prefix: "repo"
- policy-id: test-images
mirror:
domains: ["*"]
prefix: "library/"
- policy-id: test-images
mirror:
domains: ["*"]
prefix: "test-image"
- policy-id: test-images
mirror:
domains: ["*"]
prefix: "image-signer-verifier-test"

5
test/testdata/test-signing-key.pem vendored Normal file
View File

@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIKZEqmmd++eAY3bmPoBdY6nC2wLy4da2yeVZNKCp6Oj2oAoGCCqGSM49
AwEHoUQDQgAEZmicqYSY38DprGr42jU0V3ND0ROjzSRH1+yjsxhh0bi52Hh/DuOh
rSq2KJ5a09lW3ybnDjljowbkof0Y1i9Oow==
-----END EC PRIVATE KEY-----

View File

@@ -1 +1,14 @@
{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:db8f2a6e112ea6396f57d073269ecfac61e8dcdad3a4a643dcb577522492f898","size":1607,"annotations":{"org.opencontainers.image.created":"2024-04-29T10:23:46Z","org.opencontainers.image.ref.name":"docker.io/library/test-image:test"}}]}
{
"schemaVersion": 2,
"manifests": [
{
"mediaType": "application/vnd.oci.image.index.v1+json",
"digest": "sha256:db8f2a6e112ea6396f57d073269ecfac61e8dcdad3a4a643dcb577522492f898",
"size": 1607,
"annotations": {
"org.opencontainers.image.created": "2024-04-29T10:23:46Z",
"org.opencontainers.image.ref.name": "docker.io/library/test-image:test"
}
}
]
}