Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23849c1c2e | ||
|
|
bada1df262 | ||
|
|
4778d3de6a | ||
|
|
a4ac09e7da | ||
|
|
9250552c5b | ||
|
|
2acc30693f | ||
|
|
5db1b5c4c1 | ||
|
|
6f94d59a96 | ||
|
|
95319494b5 | ||
|
|
64046df6f8 | ||
|
|
57b6df0ab5 | ||
|
|
857be568b5 | ||
|
|
9d39c5ae3d | ||
|
|
aed959f858 | ||
|
|
802725caf0 | ||
|
|
9c3f267870 | ||
|
|
6cc9191e1e | ||
|
|
7ce2817111 | ||
|
|
a60aab9338 | ||
|
|
2ef3a158ae | ||
|
|
4f163f4283 | ||
|
|
74e8d8beb3 | ||
|
|
a4a0bf3cbe | ||
|
|
52499053d2 | ||
|
|
5f17f97229 | ||
|
|
8d8f09661f | ||
|
|
059ee8926c | ||
|
|
cb47507650 | ||
|
|
7c0966de81 | ||
|
|
2bf7dec72e | ||
|
|
6de792c1b5 | ||
|
|
d2a8348ae8 | ||
|
|
881e9d9582 | ||
|
|
8c6df28540 | ||
|
|
5162cfa404 | ||
|
|
72f6517b2c | ||
|
|
84cadeb97e | ||
|
|
57a61cc266 | ||
|
|
5a772633b0 | ||
|
|
1febc55a19 | ||
|
|
0db96d56aa | ||
|
|
d97d20eb93 | ||
|
|
42390b5fc2 | ||
|
|
70e6345942 | ||
|
|
f853875eea | ||
|
|
050497e5a7 | ||
|
|
d69334a1e6 | ||
|
|
a84268b133 | ||
|
|
2cd2e2da96 | ||
|
|
f1ece6893f | ||
|
|
116b9ea770 | ||
|
|
d291912208 | ||
|
|
9cad88a687 | ||
|
|
77ccbc097b | ||
|
|
45927967c8 | ||
|
|
9aa56e564d | ||
|
|
6d0a6de520 | ||
|
|
8767951fa2 | ||
|
|
f18b5877d3 | ||
|
|
93fd9daeb9 | ||
|
|
5df79de1c7 | ||
|
|
5b5e43b07a | ||
|
|
4c5135eb1b | ||
|
|
0133423f0d | ||
|
|
501b9b442d | ||
|
|
d84ed4821c | ||
|
|
c9e2ddd448 | ||
|
|
165241de42 | ||
|
|
c7d17faf05 | ||
|
|
58021646e3 | ||
|
|
3e7a85e9b8 | ||
|
|
bb7a9a257e | ||
|
|
c690d1090c | ||
|
|
1d1c258f9c | ||
|
|
5d096e226f | ||
|
|
7fc7ceaba0 | ||
|
|
78ec0b7666 | ||
|
|
053f764b8f | ||
|
|
ad3b8b9e49 | ||
|
|
9582e69968 | ||
|
|
b0b37f73f3 | ||
|
|
d21fc7853c | ||
|
|
008c14e3f3 |
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.21.x]
|
||||
go-version: [1.22.x]
|
||||
# temp disable windows tests see https://github.com/docker/image-signer-verifier/pull/154
|
||||
# os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
token: ${{ secrets.TC_CLOUD_TOKEN }}
|
||||
- name: go test including e2e
|
||||
if: matrix.os == 'ubuntu-latest' && github.actor != 'dependabot[bot]'
|
||||
run: go test -tags=e2e -v ./... -coverprofile=coverage.out -covermode=atomic
|
||||
run: go test -tags=e2e -v ./... -coverpkg=./... -coverprofile=coverage.out -covermode=atomic
|
||||
- name: go test excluding e2e
|
||||
if: matrix.os == 'macos-latest' || github.actor == 'dependabot[bot]'
|
||||
run: go test -v ./...
|
||||
|
||||
11
README.md
11
README.md
@@ -128,7 +128,10 @@ The input to the policy is an object with the following fields:
|
||||
|
||||
- `digest` (string): the digest of the image being verified
|
||||
- `purl` (string): the package URL of the image being verified
|
||||
- `is_canonical` (bool): whether the image being verified was referenced by a 'canonical' name, i.e. one that contains a digest
|
||||
- `platform` (string): the platform of the image being verified
|
||||
- `normalized_name` (string): defaults are filled out. e.g. if the image is `alpine`, this would be `library/alpine`
|
||||
- `familiar_name` (string): short version of above (e.g. `alpine`)
|
||||
- `tag`: (string): tag of the image being verified (if present)
|
||||
|
||||
### Builtin Functions
|
||||
|
||||
@@ -345,7 +348,11 @@ The VSA can be signed and published to the registry using the signing functions
|
||||
"timeVerified": "2024-04-19T08:00:00.01Z",
|
||||
"resourceUri": "pkg:docker/example.org/example-image@1.0?platform=linux%2Famd64&digest=sha256%3A49f717386e5462e945232569a97a05831cb83bef8c3369be3bb7ea1793686960",
|
||||
"policy": {
|
||||
"uri": "https://example.org/internal-policy/v1"
|
||||
"uri": "https://example.org/internal-policy/v1",
|
||||
"downloadLocation": "https://docker.github.io/tuf-staging/targets/docker/d71d6b8f49fcba1295b16f5394dd5863a14e4277eb663d66d8c48e392509afe0.policy.rego",
|
||||
"digest": {
|
||||
"sha256": "d71d6b8f49fcba1295b16f5394dd5863a14e4277eb663d66d8c48e392509afe0"
|
||||
}
|
||||
},
|
||||
"verificationResult": "PASSED",
|
||||
"verifiedLevels": ["SLSA_BUILD_LEVEL_3"]
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
ignore:
|
||||
- "internal/test"
|
||||
coverage:
|
||||
status:
|
||||
patch: false
|
||||
|
||||
97
go.mod
97
go.mod
@@ -3,8 +3,8 @@ module github.com/docker/attest
|
||||
go 1.22.5
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.2.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27
|
||||
github.com/Masterminds/semver/v3 v3.3.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.31
|
||||
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8
|
||||
github.com/containerd/platforms v0.2.1
|
||||
github.com/distribution/reference v0.6.0
|
||||
@@ -13,55 +13,54 @@ require (
|
||||
github.com/google/go-containerregistry v0.20.1
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2
|
||||
github.com/in-toto/in-toto-golang v0.9.0
|
||||
github.com/open-policy-agent/opa v0.67.0
|
||||
github.com/open-policy-agent/opa v0.67.1
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
github.com/package-url/packageurl-go v0.1.3
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.8.0
|
||||
github.com/sigstore/cosign/v2 v2.3.0
|
||||
github.com/sigstore/cosign/v2 v2.4.0
|
||||
github.com/sigstore/rekor v1.3.6
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.7
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.7
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.8
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/testcontainers/testcontainers-go/modules/registry v0.32.0
|
||||
github.com/testcontainers/testcontainers-go/modules/registry v0.33.0
|
||||
github.com/theupdateframework/go-tuf/v2 v2.0.0
|
||||
google.golang.org/api v0.189.0
|
||||
google.golang.org/api v0.195.0
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
)
|
||||
|
||||
// fork of a fork (in case it goes away) with changes to support ArtifactType (https://github.com/google/go-containerregistry/pull/1931)
|
||||
replace github.com/google/go-containerregistry => github.com/kipz/go-containerregistry v0.0.0-20240722163910-ebe90246535d
|
||||
// fork with changes to support ArtifactType (https://github.com/google/go-containerregistry/pull/1931)
|
||||
replace github.com/google/go-containerregistry => github.com/docker/go-containerregistry v0.0.0-20240808132857-c8bfc44af7c8
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.115.0 // indirect
|
||||
cloud.google.com/go/auth v0.7.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
|
||||
cloud.google.com/go v0.115.1 // indirect
|
||||
cloud.google.com/go/auth v0.9.1 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.5.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.10 // indirect
|
||||
cloud.google.com/go/kms v1.18.2 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.9 // indirect
|
||||
cloud.google.com/go/iam v1.1.13 // indirect
|
||||
cloud.google.com/go/kms v1.18.5 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.12 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.12.3 // indirect
|
||||
github.com/OneOfOne/xxhash v1.2.8 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||
github.com/agnivade/levenshtein v1.1.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.29.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.24.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.35.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
|
||||
github.com/aws/smithy-go v1.20.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.5 // indirect
|
||||
github.com/aws/smithy-go v1.20.4 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
@@ -75,9 +74,9 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect
|
||||
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect
|
||||
github.com/docker/cli v26.1.3+incompatible // indirect
|
||||
github.com/docker/cli v27.1.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v27.0.3+incompatible // indirect
|
||||
github.com/docker/docker v27.1.1+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.1 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
@@ -101,13 +100,12 @@ require (
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/certificate-transparency-go v1.2.1 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.1-vault-5 // indirect
|
||||
@@ -149,7 +147,8 @@ require (
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.24.4 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/sigstore/sigstore v1.8.7 // indirect
|
||||
github.com/sigstore/protobuf-specs v0.3.2 // indirect
|
||||
github.com/sigstore/sigstore v1.8.8 // indirect
|
||||
github.com/sigstore/timestamp-authority v1.2.2 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
@@ -161,7 +160,7 @@ require (
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
|
||||
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
|
||||
github.com/testcontainers/testcontainers-go v0.32.0 // indirect
|
||||
github.com/testcontainers/testcontainers-go v0.33.0 // indirect
|
||||
github.com/theupdateframework/go-tuf v0.7.0 // indirect
|
||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
@@ -182,19 +181,19 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/term v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240722135656-d784300faade // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect
|
||||
golang.org/x/mod v0.19.0 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/oauth2 v0.22.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/term v0.23.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240823204242-4ba0660f739c // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c // indirect
|
||||
google.golang.org/grpc v1.65.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
||||
226
go.sum
226
go.sum
@@ -1,18 +1,18 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
|
||||
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
|
||||
cloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE=
|
||||
cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
|
||||
cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=
|
||||
cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
|
||||
cloud.google.com/go/auth v0.9.1 h1:+pMtLEV2k0AXKvs/tGZojuj6QaioxfUjOpMsG5Gtx+w=
|
||||
cloud.google.com/go/auth v0.9.1/go.mod h1:Sw8ocT5mhhXxFklyhT12Eiy0ed6tTrPMCJjSI8KhYLk=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
|
||||
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
|
||||
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
||||
cloud.google.com/go/iam v1.1.10 h1:ZSAr64oEhQSClwBL670MsJAW5/RLiC6kfw3Bqmd5ZDI=
|
||||
cloud.google.com/go/iam v1.1.10/go.mod h1:iEgMq62sg8zx446GCaijmA2Miwg5o3UbO+nI47WHJps=
|
||||
cloud.google.com/go/kms v1.18.2 h1:EGgD0B9k9tOOkbPhYW1PHo2W0teamAUYMOUIcDRMfPk=
|
||||
cloud.google.com/go/kms v1.18.2/go.mod h1:YFz1LYrnGsXARuRePL729oINmN5J/5e7nYijgvfiIeY=
|
||||
cloud.google.com/go/longrunning v0.5.9 h1:haH9pAuXdPAMqHvzX0zlWQigXT7B0+CL4/2nXXdBo5k=
|
||||
cloud.google.com/go/longrunning v0.5.9/go.mod h1:HD+0l9/OOW0za6UWdKJtXoFAX/BGg/3Wj8p10NeWF7c=
|
||||
cloud.google.com/go/iam v1.1.13 h1:7zWBXG9ERbMLrzQBRhFliAV+kjcRToDTgQT3CTwYyv4=
|
||||
cloud.google.com/go/iam v1.1.13/go.mod h1:K8mY0uSXwEXS30KrnVb+j54LB/ntfZu1dr+4zFMNbus=
|
||||
cloud.google.com/go/kms v1.18.5 h1:75LSlVs60hyHK3ubs2OHd4sE63OAMcM2BdSJc2bkuM4=
|
||||
cloud.google.com/go/kms v1.18.5/go.mod h1:yXunGUGzabH8rjUPImp2ndHiGolHeWJJ0LODLedicIY=
|
||||
cloud.google.com/go/longrunning v0.5.12 h1:5LqSIdERr71CqfUsFlJdBpOkBH8FBCFD7P1nTWy3TYE=
|
||||
cloud.google.com/go/longrunning v0.5.12/go.mod h1:S5hMV8CDJ6r50t2ubVJSKQVv5u0rmik5//KgLO3k4lU=
|
||||
cuelabs.dev/go/oci/ociregistry v0.0.0-20240404174027-a39bec0462d2 h1:BnG6pr9TTr6CYlrJznYUDj6V7xldD1W+1iXPum0wT/w=
|
||||
cuelabs.dev/go/oci/ociregistry v0.0.0-20240404174027-a39bec0462d2/go.mod h1:pK23AUVXuNzzTpfMCA06sxZGeVQ/75FdVtW249de9Uo=
|
||||
cuelang.org/go v0.9.2 h1:pfNiry2PdRBr02G/aKm5k2vhzmqbAOoaB4WurmEbWvs=
|
||||
@@ -29,12 +29,12 @@ github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo
|
||||
github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo/helper v0.2.0/go.mod h1:GgeIE+1be8Ivm7Sh4RgwI42aTtC9qrcj+Y9Y6CjJhJs=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 h1:1nGuui+4POelzDwI7RG56yfQJHCnKvwfMoU7VsEp+Zg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0/go.mod h1:99EvauvlcJ1U06amZiksfYz/3aFGyIhWGHVyiZXtBAI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0 h1:GJHeeA2N7xrG3q30L2UXDyuWRzDM900/65j70wcM4Ww=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 h1:H+U3Gk9zY56G3u872L82bk4thcsy2Gghb9ExT4Zvm1o=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0/go.mod h1:mgrmMSgaLp9hmax62XQTd0N4aAqSE5E0DulSpVYK7vc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 h1:DRiANoJTiW6obBQe3SqZizkuV1PEgfiiGivmVocDy64=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0/go.mod h1:qLIye2hwb/ZouqhpSD9Zn3SJipvpEnz1Ywl3VUk9Y0s=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
|
||||
@@ -60,12 +60,10 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/Microsoft/hcsshim v0.12.3 h1:LS9NXqXhMoqNCplK1ApmVSfB4UnVLRDWRapB6EIlxE0=
|
||||
github.com/Microsoft/hcsshim v0.12.3/go.mod h1:Iyl1WVpZzr+UkzjekHZbV8o5Z9ZkxNGx6CtY2Qg/JVQ=
|
||||
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
||||
@@ -102,48 +100,48 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/aws/aws-sdk-go v1.54.19 h1:tyWV+07jagrNiCcGRzRhdtVjQs7Vy41NwsuOcl0IbVI=
|
||||
github.com/aws/aws-sdk-go v1.54.19/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
|
||||
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
|
||||
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.31 h1:kxBoRsjhT3pq0cKthgj6RU6bXTm/2SgdoUMyrVw0rAI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.31/go.mod h1:z04nZdSWFPaDwK3DdJOG2r+scLQzMYuJeW0CujEm9FM=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.30 h1:aau/oYFtibVovr2rDt8FHlU17BTicFEMAi29V1U+L5Q=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.30/go.mod h1:BPJ/yXV92ZVq6G8uYvbU0gSl8q94UB63nMT5ctNO38g=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 h1:yjwoSyDZF8Jth+mUk5lSPJCkMC0lMy6FaCD51jm6ayE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12/go.mod h1:fuR57fAgMk7ot3WcNQfb6rSEn+SUffl7ri+aa8uKysI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16/go.mod h1:2DwJF39FlNAUiX5pAc0UNeiz16lK2t7IaFcm0LFHEgc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 h1:jYfy8UPmd+6kJW5YhY0L1/KftReOGxI/4NtVSTh9O/I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16/go.mod h1:7ZfEPZxkW42Afq4uQB8H2E2e6ebh6mXTueEpYzjCzcs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.29.1 h1:ywNLJrn/Qn4enDsz/XnKlvpnLqvJxFGQV2BltWltbis=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.29.1/go.mod h1:WadVIk+UrTvWuAsCp6BKGX4i2snurpz8mPWhJQnS7Dg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.24.1 h1:Eq9i/mvOlGghiKe9NtsmeD9Wlwg8p4fbsqrMb3nWirM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.24.1/go.mod h1:VtOgEoLEPV1YADuq+Z2XOK6/wKkGW2YK6DjChZ/GvDs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.35.1 h1:0gP2OJJT6HM2BYltZ9x+A87OE8LJL96DXeAAdLv3t1M=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.35.1/go.mod h1:hGONorZkQCfR5DW6l2xdy7zC8vfO0r9pJlwyg6gmGeo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
|
||||
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
|
||||
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18/go.mod h1:++NHzT+nAF7ZPrHPsA+ENvsXkOO8wEu+C6RXltAG4/c=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 h1:UPTdlTOwWUX49fVi7cymEN6hDqCwe3LNv1vi7TXUutk=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.35.3/go.mod h1:gjDP16zn+WWalyaUqwCCioQ8gU8lzttCCc9jYsiQI/8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 h1:zCsFCKvbj25i7p1u94imVoO447I/sFv8qq+lGJhRN0c=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.5/go.mod h1:ZeDX1SnKsVlejeuz41GiajjZpRSWR7/42q/EyA/QEiM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 h1:SKvPgvdvmiTWoi0GAJ7AsJfOz3ngVkD/ERbs5pUnHNI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5/go.mod h1:20sz31hv/WsPa3HhU3hfrIet2kxM4Pe0r20eBZ20Tac=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.5 h1:OMsEmCyz2i89XwRwPouAJvhj81wINh+4UK+k/0Yo/q8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.5/go.mod h1:vmSqFK+BVIwVpDAGZB3CoCXHzurt4qBE8lf+I/kRTh0=
|
||||
github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
|
||||
github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8 h1:SoFYaT9UyGkR0+nogNyD/Lj+bsixB+SNuAS4ABlEs6M=
|
||||
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8/go.mod h1:2JF49jcDOrLStIXN/j/K1EKRq8a8R2qRnlZA6/o/c7c=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/buildkite/agent/v3 v3.75.1 h1:FJg1gss9nUESExTsfx6yWqs/g20Vyd4pHSEB9iuT4pI=
|
||||
github.com/buildkite/agent/v3 v3.75.1/go.mod h1:UXCAEaqJnw5UsoZjJ9NxMvdSGc4oMHBnQFQqzGPDM0Y=
|
||||
github.com/buildkite/agent/v3 v3.76.2 h1:SweFq3e0N20RikWsVeOXzTjfr0AoOskxm9c0bcNyI0E=
|
||||
github.com/buildkite/agent/v3 v3.76.2/go.mod h1:9ffbmJD7d7C/nOcElj6Qm+uIj1QoYh3NNvka4rkKkss=
|
||||
github.com/buildkite/go-pipeline v0.10.0 h1:EDffu+LfMY2k5u+iEdo6Jn3obGKsrL5wicc1O/yFeRs=
|
||||
github.com/buildkite/go-pipeline v0.10.0/go.mod h1:eMH1kiav5VeiTiu0Mk2/M7nZhKyFeL4iGj7Y7rj4f3w=
|
||||
github.com/buildkite/interpolate v0.1.3 h1:OFEhqji1rNTRg0u9DsSodg63sjJQEb1uWbENq9fUOBM=
|
||||
@@ -218,16 +216,18 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi
|
||||
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v26.1.3+incompatible h1:bUpXT/N0kDE3VUHI2r5VMsYQgi38kYuoC0oL9yt3lqc=
|
||||
github.com/docker/cli v26.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE=
|
||||
github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE=
|
||||
github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
|
||||
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
|
||||
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-containerregistry v0.0.0-20240808132857-c8bfc44af7c8 h1:T/wutVfQ1Oj4H5tbP5IZL5l6PZqzvapVJ5cB4Wy4Ucc=
|
||||
github.com/docker/go-containerregistry v0.0.0-20240808132857-c8bfc44af7c8/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
@@ -353,8 +353,8 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
|
||||
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
|
||||
github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w=
|
||||
github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM=
|
||||
github.com/google/trillian v1.6.0 h1:jMBeDBIkINFvS2n6oV5maDqfRlxREAc6CW9QYWQ0qT4=
|
||||
@@ -364,8 +364,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA=
|
||||
github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
|
||||
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
@@ -417,8 +417,6 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kipz/go-containerregistry v0.0.0-20240722163910-ebe90246535d h1:5QaWAwKhslfqxEyMZY0ofvsbMJkMLcx5E30JFufMVj8=
|
||||
github.com/kipz/go-containerregistry v0.0.0-20240722163910-ebe90246535d/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
@@ -493,8 +491,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/open-policy-agent/opa v0.67.0 h1:FOdsO9yNhfmrh+72oVK7ImWmzruG+VSpfbr5IBqEWVs=
|
||||
github.com/open-policy-agent/opa v0.67.0/go.mod h1:aqKlHc8E2VAAylYE9x09zJYr/fYzGX+JKne89UGqFzk=
|
||||
github.com/open-policy-agent/opa v0.67.1 h1:rzy26J6g1X+CKknAcx0Vfbt41KqjuSzx4E0A8DAZf3E=
|
||||
github.com/open-policy-agent/opa v0.67.1/go.mod h1:aqKlHc8E2VAAylYE9x09zJYr/fYzGX+JKne89UGqFzk=
|
||||
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=
|
||||
@@ -555,22 +553,26 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/sigstore/cosign/v2 v2.3.0 h1:rBLVdKMYuER0blmaKMfMNkvawBdK1lTMz2L5PtTPrI8=
|
||||
github.com/sigstore/cosign/v2 v2.3.0/go.mod h1:tjACBZS6LoH3bap5hU8MxyGC4DIJiatqhZxuJWFcIJ0=
|
||||
github.com/sigstore/cosign/v2 v2.4.0 h1:2NdidNgClg+oXr/fDIr37E/BE6j00gqgUhSiBK2kjSQ=
|
||||
github.com/sigstore/cosign/v2 v2.4.0/go.mod h1:j+fH1DCUkcn92qp6ezDj4JbGMri6eG1nLJC+hs64rvc=
|
||||
github.com/sigstore/fulcio v1.5.1 h1:Iasy1zfNjaq8BV4S8o6pXspLDU28PQC2z07GmOu9zpM=
|
||||
github.com/sigstore/fulcio v1.5.1/go.mod h1:W1A/UHrTopy1IBZPMtHmxg7GPYAu+vt5dRXM3W6yjPo=
|
||||
github.com/sigstore/protobuf-specs v0.3.2 h1:nCVARCN+fHjlNCk3ThNXwrZRqIommIeNKWwQvORuRQo=
|
||||
github.com/sigstore/protobuf-specs v0.3.2/go.mod h1:RZ0uOdJR4OB3tLQeAyWoJFbNCBFrPQdcokntde4zRBA=
|
||||
github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8=
|
||||
github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc=
|
||||
github.com/sigstore/sigstore v1.8.7 h1:L7/zKauHTg0d0Hukx7qlR4nifh6T6O6UIt9JBwAmTIg=
|
||||
github.com/sigstore/sigstore v1.8.7/go.mod h1:MPiQ/NIV034Fc3Kk2IX9/XmBQdK60wfmpvgK9Z1UjRA=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.7 h1:SoahswHQm2JhO8h3KTAeX8IZeE7mSR2Lc53ay5choes=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.7/go.mod h1:TOVOPOqldrrz4dP7x4/0DFQTs9QSXZUoHu21+JHmixA=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.7 h1:jcdKxc5bvwfL7+ZbeCszaN/qsBd6180fGAHxeX5Ckm0=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.7/go.mod h1:gakVcpRiN+aFdLhPXXP8ubOCWiedM1YJ/gR74ez/tT0=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.7 h1:zYg1XlbKpQkmE7FpWTkLuUn7RttLAq4FcZ1G9JcqhoY=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.7/go.mod h1:VmUsO1R4OHuyHBEgI4bbSUn0z2nojszrDMvlDxyX/dE=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.7 h1:dbcB9VEddYrvK+y4rHeES5OZ/pMQuucfJ0qCNWQmnp0=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.7/go.mod h1:96MrPJBkHiAvqFyqviuYbwPAdbPCj8CR3V0RJ9bKjrE=
|
||||
github.com/sigstore/sigstore v1.8.8 h1:B6ZQPBKK7Z7tO3bjLNnlCMG+H66tO4E/+qAphX8T/hg=
|
||||
github.com/sigstore/sigstore v1.8.8/go.mod h1:GW0GgJSCTBJY3fUOuGDHeFWcD++c4G8Y9K015pwcpDI=
|
||||
github.com/sigstore/sigstore-go v0.5.1 h1:5IhKvtjlQBeLnjKkzMELNG4tIBf+xXQkDzhLV77+/8Y=
|
||||
github.com/sigstore/sigstore-go v0.5.1/go.mod h1:TuOfV7THHqiDaUHuJ5+QN23RP/YoKmsbwJpY+aaYPN0=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.8 h1:2zHmUvaYCwV6LVeTo+OAkTm8ykOGzA9uFlAjwDPAUWM=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.8/go.mod h1:OEhheBplZinUsm7W9BupafztVZV3ldkAxEHbpAeC0Pk=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.8 h1:RKk4Z+qMaLORUdT7zntwMqKiYAej1VQlCswg0S7xNSY=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.8/go.mod h1:dMJdlBWKHMu2xf0wIKpbo7+QfG+RzVkBB3nHP8EMM5o=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.8 h1:89Xtxj8oqZt3UlSpCP4wApFvnQ2Z/dgowW5QOVhQigI=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.8/go.mod h1:Wa4xn/H3pU/yW/6tHiMXTpObBtBSGC5q29KYFEPKN6o=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.8 h1:Zte3Oogkd8m+nu2oK3yHtGmN++TZWh2Lm6q2iSprT1M=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.8/go.mod h1:j00crVw6ki4/WViXflw0zWgNALrAzZT+GbIK8v7Xlz4=
|
||||
github.com/sigstore/timestamp-authority v1.2.2 h1:X4qyutnCQqJ0apMewFyx+3t7Tws00JQ/JonBiu3QvLE=
|
||||
github.com/sigstore/timestamp-authority v1.2.2/go.mod h1:nEah4Eq4wpliDjlY342rXclGSO7Kb9hoRrl9tqLW13A=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
@@ -611,10 +613,10 @@ github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDd
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
|
||||
github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes=
|
||||
github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
|
||||
github.com/testcontainers/testcontainers-go v0.32.0 h1:ug1aK08L3gCHdhknlTTwWjPHPS+/alvLJU/DRxTD/ME=
|
||||
github.com/testcontainers/testcontainers-go v0.32.0/go.mod h1:CRHrzHLQhlXUsa5gXjTOfqIEJcrK5+xMDmBr/WMI88E=
|
||||
github.com/testcontainers/testcontainers-go/modules/registry v0.32.0 h1:b4JSSEhbGXGtQA1WXJ3BlbkVjjdXoFTtBPvLRe+9Y9Y=
|
||||
github.com/testcontainers/testcontainers-go/modules/registry v0.32.0/go.mod h1:bX3JF8vQkv3D2frmrDyQd0GCQIQGl5nPG91xUvl7UhA=
|
||||
github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw=
|
||||
github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8=
|
||||
github.com/testcontainers/testcontainers-go/modules/registry v0.33.0 h1:rpQS5KcFpyRPM3xVKERuXDqUcE5xjwE8MQUgmKVkL0o=
|
||||
github.com/testcontainers/testcontainers-go/modules/registry v0.33.0/go.mod h1:qr3nJgBZ2ovQva6vadXchwi786/mBBDzhBPbrmWkYIE=
|
||||
github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg=
|
||||
github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU=
|
||||
github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
|
||||
@@ -676,8 +678,8 @@ go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+
|
||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.step.sm/crypto v0.50.0 h1:BqI9sEgocoHDLLHiZnFqdqXl5FjdMvOWKMm/fKL/lrw=
|
||||
go.step.sm/crypto v0.50.0/go.mod h1:NCFMhLS6FJXQ9sD9PP282oHtsBWLrI6wXZY0eOkq7t8=
|
||||
go.step.sm/crypto v0.51.1 h1:ktUg/2hetEMiBAqgz502ktZDGoDoGrcHFg3XpkmkvvA=
|
||||
go.step.sm/crypto v0.51.1/go.mod h1:PdrhttNU/tG9/YsVd4fdlysBN+UV503p0o2irFZQlAw=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
@@ -690,8 +692,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
@@ -702,8 +704,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -723,11 +725,11 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
|
||||
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -735,8 +737,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -767,15 +769,15 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
|
||||
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -783,10 +785,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -798,26 +800,26 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/api v0.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI=
|
||||
google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8=
|
||||
google.golang.org/api v0.195.0 h1:Ude4N8FvTKnnQJHU48RFI40jOBgIrL8Zqr3/QeST6yU=
|
||||
google.golang.org/api v0.195.0/go.mod h1:DOGRWuv3P8TU8Lnz7uQc4hyNqrBpMtD9ppW3wBJurgc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20240722135656-d784300faade h1:lKFsS7wpngDgSCeFn7MoLy+wBDQZ1UQIJD4UNM1Qvkg=
|
||||
google.golang.org/genproto v0.0.0-20240722135656-d784300faade/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade h1:oCRSWfwGXQsqlVdErcyTt4A93Y8fo0/9D4b1gnI++qo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/genproto v0.0.0-20240823204242-4ba0660f739c h1:TYOEhrQMrNDTAd2rX9m+WgGr8Ku6YNuj1D7OX6rWSok=
|
||||
google.golang.org/genproto v0.0.0-20240823204242-4ba0660f739c/go.mod h1:2rC5OendXvZ8wGEo/cSLheztrZDZaSoHanUcd1xtZnw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c h1:Kqjm4WpoWvwhMPcrAczoTyMySQmYa9Wy2iL6Con4zn8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
@@ -875,8 +877,8 @@ k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCI
|
||||
k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/release-utils v0.8.3 h1:KtOtA4qDmzJyeQ2zkDsFVI25+NViwms/o5eL2NftFdA=
|
||||
sigs.k8s.io/release-utils v0.8.3/go.mod h1:fp82Fma06OXBhEJ+GUJKqvcplDBomruK1R/1fWJnsrQ=
|
||||
sigs.k8s.io/release-utils v0.8.4 h1:4QVr3UgbyY/d9p74LBhg0njSVQofUsAZqYOzVZBhdBw=
|
||||
sigs.k8s.io/release-utils v0.8.4/go.mod h1:m1bHfscTemQp+z+pLCZnkXih9n0+WukIUU70n6nFnU0=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.3.0 h1:UZbZAZfX0wV2zr7YZorDz6GXROfDFj6LvqCRm4VUVKk=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.3.0/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
|
||||
@@ -16,11 +16,13 @@ var prodRoot []byte
|
||||
|
||||
var defaultRoot = prodRoot
|
||||
|
||||
type RootName string
|
||||
type EmbeddedRoot struct {
|
||||
Data []byte
|
||||
Name RootName
|
||||
}
|
||||
type (
|
||||
RootName string
|
||||
EmbeddedRoot struct {
|
||||
Data []byte
|
||||
Name RootName
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
RootDev = EmbeddedRoot{Data: devRoot, Name: "dev"}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/signerverifier"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||
)
|
||||
|
||||
type MockResolver struct {
|
||||
Envs []*attestation.Envelope
|
||||
}
|
||||
|
||||
func (r MockResolver) Attestations(ctx context.Context, mediaType string) ([]*attestation.Envelope, error) {
|
||||
return r.Envs, nil
|
||||
}
|
||||
|
||||
func (r MockResolver) ImageName(ctx context.Context) (string, error) {
|
||||
return "library/alpine:latest", nil
|
||||
}
|
||||
|
||||
func (r MockResolver) ImageDescriptor(ctx context.Context) (*v1.Descriptor, error) {
|
||||
digest, err := v1.NewHash("sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.Descriptor{
|
||||
Digest: digest,
|
||||
Size: 1234,
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (r MockResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
|
||||
return oci.ParsePlatform("linux/amd64")
|
||||
}
|
||||
|
||||
type MockRegistryResolver struct {
|
||||
Subject *v1.Descriptor
|
||||
ImageNameStr string
|
||||
*MockResolver
|
||||
}
|
||||
|
||||
func (r *MockRegistryResolver) ImageDescriptor(ctx context.Context) (*v1.Descriptor, error) {
|
||||
return r.Subject, nil
|
||||
}
|
||||
|
||||
func (r *MockRegistryResolver) ImageName(ctx context.Context) (string, error) {
|
||||
return r.ImageNameStr, nil
|
||||
}
|
||||
|
||||
func GetMockSigner(ctx context.Context) (dsse.SignerVerifier, error) {
|
||||
priv, err := os.ReadFile(filepath.Join("..", "..", "test", "testdata", "test-signing-key.pem"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return signerverifier.LoadKeyPair(priv)
|
||||
}
|
||||
@@ -2,33 +2,25 @@ package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/policy"
|
||||
"github.com/docker/attest/pkg/signerverifier"
|
||||
"github.com/docker/attest/pkg/tlog"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||
)
|
||||
|
||||
const (
|
||||
USE_MOCK_TL = true
|
||||
USE_MOCK_KMS = true
|
||||
USE_MOCK_POLICY = true
|
||||
UseMockTL = true
|
||||
UseMockKMS = true
|
||||
|
||||
AwsRegion = "us-east-1"
|
||||
AwsKmsKeyArn = "arn:aws:kms:us-east-1:175142243308:alias/doi-signing" // sandbox
|
||||
AWSRegion = "us-east-1"
|
||||
AWSKMSKeyARN = "arn:aws:kms:us-east-1:175142243308:alias/doi-signing" // sandbox
|
||||
)
|
||||
|
||||
var UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||
|
||||
func CreateTempDir(t *testing.T, dir, pattern string) string {
|
||||
// Create a temporary directory for output oci layout
|
||||
tempDir, err := os.MkdirTemp(dir, pattern)
|
||||
@@ -45,9 +37,17 @@ func CreateTempDir(t *testing.T, dir, pattern string) string {
|
||||
return tempDir
|
||||
}
|
||||
|
||||
func GetMockSigner(_ context.Context) (dsse.SignerVerifier, error) {
|
||||
priv, err := os.ReadFile(filepath.Join("..", "..", "test", "testdata", "test-signing-key.pem"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return signerverifier.LoadKeyPair(priv)
|
||||
}
|
||||
|
||||
func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) {
|
||||
var tl tlog.TL
|
||||
if USE_MOCK_TL {
|
||||
if UseMockTL {
|
||||
tl = tlog.GetMockTL()
|
||||
} else {
|
||||
tl = &tlog.RekorTL{}
|
||||
@@ -55,24 +55,15 @@ func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) {
|
||||
|
||||
ctx := tlog.WithTL(context.Background(), tl)
|
||||
|
||||
var policyEvaluator policy.PolicyEvaluator
|
||||
if USE_MOCK_POLICY {
|
||||
policyEvaluator = policy.GetMockPolicy()
|
||||
} else {
|
||||
policyEvaluator = policy.NewRegoEvaluator(true)
|
||||
}
|
||||
|
||||
ctx = policy.WithPolicyEvaluator(ctx, policyEvaluator)
|
||||
|
||||
var signer dsse.SignerVerifier
|
||||
var err error
|
||||
if USE_MOCK_KMS {
|
||||
if UseMockKMS {
|
||||
signer, err = GetMockSigner(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
signer, err = signerverifier.GetAWSSigner(ctx, AwsKmsKeyArn, AwsRegion)
|
||||
signer, err = signerverifier.GetAWSSigner(ctx, AWSKMSKeyARN, AWSRegion)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -80,107 +71,3 @@ func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) {
|
||||
|
||||
return ctx, signer
|
||||
}
|
||||
|
||||
type AnnotatedStatement struct {
|
||||
OCIDescriptor *v1.Descriptor
|
||||
InTotoStatement *intoto.Statement
|
||||
Annotations map[string]string
|
||||
}
|
||||
|
||||
func ExtractStatementsFromIndex(idx v1.ImageIndex, mediaType string) ([]*AnnotatedStatement, error) {
|
||||
mfs2, err := idx.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
||||
}
|
||||
|
||||
var statements []*AnnotatedStatement
|
||||
|
||||
for _, mf := range mfs2.Manifests {
|
||||
if mf.Annotations[attestation.DockerReferenceType] != "attestation-manifest" {
|
||||
continue
|
||||
}
|
||||
|
||||
attestationImage, err := idx.Image(mf.Digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
|
||||
}
|
||||
layers, err := attestationImage.Layers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err)
|
||||
}
|
||||
|
||||
for _, layer := range layers {
|
||||
// parse layer blob as json
|
||||
mt, err := layer.MediaType()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||
}
|
||||
|
||||
if string(mt) != mediaType {
|
||||
continue
|
||||
}
|
||||
r, err := layer.Uncompressed()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||
}
|
||||
defer r.Close()
|
||||
var intotoStatement = new(intoto.Statement)
|
||||
var desc *v1.Descriptor
|
||||
if strings.HasSuffix(string(mt), "+dsse") {
|
||||
var env = new(attestation.Envelope)
|
||||
err = json.NewDecoder(r).Decode(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode env: %w", err)
|
||||
}
|
||||
payload, err := base64.StdEncoding.Strict().DecodeString(env.Payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode payload: %w", err)
|
||||
}
|
||||
err = json.Unmarshal([]byte(payload), intotoStatement)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode %s statement: %w", mediaType, err)
|
||||
}
|
||||
} else {
|
||||
desc := new(v1.Descriptor)
|
||||
err = json.NewDecoder(r).Decode(desc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode statement: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
layerDesc, err := partial.Descriptor(layer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get descriptor for layer: %w", err)
|
||||
}
|
||||
annotations := make(map[string]string)
|
||||
for k, v := range layerDesc.Annotations {
|
||||
annotations[k] = v
|
||||
}
|
||||
statements = append(statements, &AnnotatedStatement{
|
||||
OCIDescriptor: desc,
|
||||
InTotoStatement: intotoStatement,
|
||||
Annotations: annotations,
|
||||
})
|
||||
}
|
||||
}
|
||||
return statements, nil
|
||||
}
|
||||
|
||||
func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStatement, error) {
|
||||
idx, err := layout.ImageIndexFromPath(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load image index: %w", err)
|
||||
}
|
||||
|
||||
idxm, err := idx.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get digest: %w", err)
|
||||
}
|
||||
idxDigest := idxm.Manifests[0].Digest
|
||||
|
||||
mfs, err := idx.ImageIndex(idxDigest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
|
||||
}
|
||||
return ExtractStatementsFromIndex(mfs, mediaType)
|
||||
}
|
||||
|
||||
2
pkg/attest/README.md
Normal file
2
pkg/attest/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## attest
|
||||
This package implements the top-level signing and verification methods.
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"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"
|
||||
@@ -55,7 +54,7 @@ func ExampleSignStatements_remote() {
|
||||
}
|
||||
|
||||
// push image index with signed attestation-manifests
|
||||
err = mirror.PushIndexToRegistry(signedIndex, ref)
|
||||
err = oci.PushIndexToRegistry(signedIndex, ref)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -66,11 +65,11 @@ func ExampleSignStatements_remote() {
|
||||
Add: signedIndex,
|
||||
Descriptor: v1.Descriptor{
|
||||
Annotations: map[string]string{
|
||||
oci.OciReferenceTarget: attIdx.Name,
|
||||
oci.OCIReferenceTarget: attIdx.Name,
|
||||
},
|
||||
},
|
||||
})
|
||||
err = mirror.SaveIndexAsOCILayout(idx, path)
|
||||
err = oci.SaveIndexAsOCILayout(idx, path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -6,24 +6,12 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/attest/internal/embed"
|
||||
"github.com/docker/attest/pkg/attest"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/policy"
|
||||
"github.com/docker/attest/pkg/tuf"
|
||||
)
|
||||
|
||||
func createTufClient(outputPath string) (*tuf.TufClient, error) {
|
||||
// using oci tuf metadata and targets
|
||||
metadataURI := "registry-1.docker.io/docker/tuf-metadata:latest"
|
||||
targetsURI := "registry-1.docker.io/docker/tuf-targets"
|
||||
// example using http tuf metadata and targets
|
||||
// metadataURI := "https://docker.github.io/tuf-staging/metadata"
|
||||
// targetsURI := "https://docker.github.io/tuf-staging/targets"
|
||||
|
||||
return tuf.NewTufClient(embed.RootStaging.Data, outputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker())
|
||||
}
|
||||
|
||||
func ExampleVerify_remote() {
|
||||
// create a tuf client
|
||||
home, err := os.UserHomeDir()
|
||||
@@ -31,21 +19,19 @@ func ExampleVerify_remote() {
|
||||
panic(err)
|
||||
}
|
||||
tufOutputPath := filepath.Join(home, ".docker", "tuf")
|
||||
tufClient, err := createTufClient(tufOutputPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tufClientOpts := tuf.NewDockerDefaultClientOptions(tufOutputPath)
|
||||
|
||||
// create a resolver for remote attestations
|
||||
image := "registry-1.docker.io/library/notary:server"
|
||||
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
|
||||
opts := &policy.Options{
|
||||
TUFClientOptions: tufClientOpts,
|
||||
LocalTargetsDir: filepath.Join(home, ".docker", "policy"), // location to store policy files downloaded from TUF
|
||||
LocalPolicyDir: "", // overrides TUF policy for local policy files if set
|
||||
PolicyID: "", // set to ignore policy mapping and select a policy by id
|
||||
DisableTUF: false, // set to disable TUF and rely on local policy files
|
||||
}
|
||||
|
||||
src, err := oci.ParseImageSpec(image, oci.WithPlatform(platform))
|
||||
|
||||
@@ -9,17 +9,17 @@ import (
|
||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||
)
|
||||
|
||||
// this is only relevant if there are (unsigned) in-toto statements
|
||||
func SignStatements(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) ([]*attestation.AttestationManifest, error) {
|
||||
// this is only relevant if there are (unsigned) in-toto statements.
|
||||
func SignStatements(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) ([]*attestation.Manifest, error) {
|
||||
// extract attestation manifests from index
|
||||
attestationManifests, err := attestation.GetAttestationManifestsFromIndex(idx)
|
||||
attestationManifests, err := attestation.ManifestsFromIndex(idx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load attestation manifests from index: %w", err)
|
||||
}
|
||||
// sign every attestation layer in each manifest
|
||||
for _, manifest := range attestationManifests {
|
||||
for _, layer := range manifest.OriginalLayers {
|
||||
err = manifest.AddAttestation(ctx, signer, layer.Statement, opts)
|
||||
err = manifest.Add(ctx, signer, layer.Statement, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign attestation layer %w", err)
|
||||
}
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
package attest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/mirror"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/policy"
|
||||
"github.com/google/go-containerregistry/pkg/registry"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
"github.com/google/go-containerregistry/pkg/v1/static"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -27,12 +15,13 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
|
||||
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
|
||||
PassMirrorPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-mirror")
|
||||
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
|
||||
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
|
||||
InputsPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-inputs")
|
||||
EmptyPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-policies")
|
||||
TestTempDir = "attest-sign-test"
|
||||
)
|
||||
|
||||
@@ -46,14 +35,15 @@ func TestSignVerifyOCILayout(t *testing.T) {
|
||||
expectedAttestations int
|
||||
replace bool
|
||||
}{
|
||||
{"signed replaced", UnsignedTestImage, 0, 4, true},
|
||||
{"without replace", UnsignedTestImage, 4, 4, false},
|
||||
{"signed replaced", test.UnsignedTestImage, 0, 4, true},
|
||||
{"without replace", test.UnsignedTestImage, 4, 4, false},
|
||||
// image without provenance doesn't fail
|
||||
{"no provenance (replace)", NoProvenanceImage, 0, 2, true},
|
||||
{"no provenance (no replace)", NoProvenanceImage, 2, 2, false},
|
||||
}
|
||||
policyOpts := &policy.PolicyOptions{
|
||||
policyOpts := &policy.Options{
|
||||
LocalPolicyDir: PassPolicyDir,
|
||||
DisableTUF: true,
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -66,28 +56,18 @@ func TestSignVerifyOCILayout(t *testing.T) {
|
||||
signedIndex := attIdx.Index
|
||||
signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests, attestation.WithReplacedLayers(tc.replace))
|
||||
require.NoError(t, err)
|
||||
// output signed attestations
|
||||
idx := v1.ImageIndex(empty.Index)
|
||||
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
||||
Add: signedIndex,
|
||||
Descriptor: v1.Descriptor{
|
||||
Annotations: map[string]string{
|
||||
oci.OciReferenceTarget: attIdx.Name,
|
||||
},
|
||||
},
|
||||
})
|
||||
_, err = layout.Write(outputLayout, idx)
|
||||
spec, err := oci.ParseImageSpec(oci.LocalPrefix + outputLayout)
|
||||
require.NoError(t, err)
|
||||
src, err := oci.ParseImageSpec("oci://" + outputLayout)
|
||||
err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name)
|
||||
require.NoError(t, err)
|
||||
policy, err := Verify(ctx, src, policyOpts)
|
||||
policy, err := Verify(ctx, spec, policyOpts)
|
||||
require.NoError(t, err)
|
||||
assert.Equalf(t, OutcomeSuccess, policy.Outcome, "Policy should have been found")
|
||||
|
||||
var allEnvelopes []*test.AnnotatedStatement
|
||||
var allEnvelopes []*attestation.AnnotatedStatement
|
||||
for _, predicate := range []string{intoto.PredicateSPDX, v02.PredicateSLSAProvenance, attestation.VSAPredicateType} {
|
||||
mt, _ := attestation.DSSEMediaType(predicate)
|
||||
statements, err := test.ExtractAnnotatedStatements(outputLayout, mt)
|
||||
statements, err := attestation.ExtractAnnotatedStatements(outputLayout, mt)
|
||||
require.NoError(t, err)
|
||||
allEnvelopes = append(allEnvelopes, statements...)
|
||||
|
||||
@@ -97,150 +77,9 @@ 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(outputLayout, intoto.PayloadType)
|
||||
statements, err := attestation.ExtractAnnotatedStatements(outputLayout, intoto.PayloadType)
|
||||
require.NoError(t, err)
|
||||
assert.Equalf(t, tc.expectedStatements, len(statements), "expected %d statement, got %d", tc.expectedStatements, len(statements))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddSignedLayerAnnotations(t *testing.T) {
|
||||
ctx, signer := test.Setup(t)
|
||||
testCases := []struct {
|
||||
name string
|
||||
replace bool
|
||||
}{
|
||||
{"replaced", true},
|
||||
{"not replaced", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
data := []byte("signed")
|
||||
testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType))
|
||||
mediaType := types.OCIManifestSchema1
|
||||
opts := &attestation.SigningOptions{}
|
||||
originalLayer := &attestation.AttestationLayer{
|
||||
Layer: testLayer,
|
||||
Statement: &intoto.Statement{
|
||||
StatementHeader: intoto.StatementHeader{
|
||||
PredicateType: attestation.VSAPredicateType,
|
||||
},
|
||||
},
|
||||
Annotations: map[string]string{"test": "test"},
|
||||
}
|
||||
|
||||
manifest := &attestation.AttestationManifest{
|
||||
OriginalDescriptor: &v1.Descriptor{
|
||||
MediaType: mediaType,
|
||||
},
|
||||
OriginalLayers: []*attestation.AttestationLayer{
|
||||
originalLayer,
|
||||
},
|
||||
SubjectDescriptor: &v1.Descriptor{},
|
||||
}
|
||||
err := manifest.AddAttestation(ctx, signer, originalLayer.Statement, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
newImg, err := manifest.BuildAttestationImage(attestation.WithReplacedLayers(tc.replace))
|
||||
require.NoError(t, err)
|
||||
mf, _ := newImg.RawManifest()
|
||||
type Annotations struct {
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
}
|
||||
type Layers struct {
|
||||
Layers []Annotations `json:"layers"`
|
||||
}
|
||||
l := &Layers{}
|
||||
err = json.Unmarshal(mf, l)
|
||||
require.NoError(t, err)
|
||||
_, ok := l.Layers[0].Annotations["test"]
|
||||
assert.Truef(t, ok, "missing annotations")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleStatementSigning(t *testing.T) {
|
||||
ctx, signer := test.Setup(t)
|
||||
empty := types.MediaType("application/vnd.oci.empty.v1+json")
|
||||
testCases := []struct {
|
||||
name string
|
||||
replace bool
|
||||
}{
|
||||
{"replaced", true},
|
||||
{"not replaced", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
opts := &attestation.SigningOptions{}
|
||||
statement := &intoto.Statement{
|
||||
StatementHeader: intoto.StatementHeader{
|
||||
PredicateType: attestation.VSAPredicateType,
|
||||
},
|
||||
}
|
||||
statement2 := &intoto.Statement{
|
||||
StatementHeader: intoto.StatementHeader{
|
||||
PredicateType: attestation.VSAPredicateType,
|
||||
},
|
||||
}
|
||||
digest, err := v1.NewHash("sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620")
|
||||
require.NoError(t, err)
|
||||
subject := &v1.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Digest: digest,
|
||||
}
|
||||
manifest, err := NewAttestationManifest(subject)
|
||||
require.NoError(t, err)
|
||||
err = manifest.AddAttestation(ctx, signer, statement, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = manifest.AddAttestation(ctx, signer, statement2, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
// fake that the manfifest was loaded from a real image
|
||||
manifest.OriginalLayers = manifest.SignedLayers
|
||||
envelopes, err := oci.ExtractEnvelopes(manifest, attestation.VSAPredicateType)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, envelopes, 2)
|
||||
|
||||
newImg, err := manifest.BuildAttestationImage(attestation.WithReplacedLayers(tc.replace))
|
||||
require.NoError(t, err)
|
||||
layers, err := newImg.Layers()
|
||||
require.NoError(t, err)
|
||||
if tc.replace {
|
||||
assert.Len(t, layers, 2)
|
||||
} else {
|
||||
assert.Len(t, layers, 4)
|
||||
}
|
||||
|
||||
newImgs, err := manifest.BuildReferringArtifacts()
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, newImgs, 2)
|
||||
for _, img := range newImgs {
|
||||
mf, err := img.Manifest()
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, mf.ArtifactType, "application/vnd.in-toto")
|
||||
assert.Contains(t, mf.ArtifactType, "+dsse")
|
||||
assert.Equal(t, subject.MediaType, mf.MediaType)
|
||||
assert.Equal(t, empty, mf.Config.MediaType)
|
||||
assert.Equal(t, int64(2), mf.Config.Size)
|
||||
assert.Equal(t, "{}", string(mf.Config.Data))
|
||||
layers, err := img.Layers()
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, layers, 1)
|
||||
}
|
||||
server := httptest.NewServer(registry.New(registry.WithReferrersSupport(true)))
|
||||
defer server.Close()
|
||||
|
||||
u, err := url.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||
output, err := oci.ParseImageSpecs(indexName)
|
||||
require.NoError(t, err)
|
||||
err = mirror.SaveReferrers(manifest, output)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func (o Outcome) StringForVSA() (string, error) {
|
||||
type VerificationResult struct {
|
||||
Outcome Outcome
|
||||
Policy *policy.Policy
|
||||
Input *policy.PolicyInput
|
||||
Input *policy.Input
|
||||
VSA *intoto.Statement
|
||||
Violations []policy.Violation
|
||||
SubjectDescriptor *v1.Descriptor
|
||||
|
||||
@@ -3,6 +3,8 @@ package attest
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -11,66 +13,134 @@ import (
|
||||
"github.com/docker/attest/pkg/config"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/policy"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/docker/attest/pkg/tuf"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
)
|
||||
|
||||
func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.PolicyOptions) (result *VerificationResult, err error) {
|
||||
type Verifier interface {
|
||||
Verify(ctx context.Context, src *oci.ImageSpec) (result *VerificationResult, err error)
|
||||
}
|
||||
|
||||
type tufVerifier struct {
|
||||
opts *policy.Options
|
||||
tufClient tuf.Downloader
|
||||
}
|
||||
|
||||
func NewVerifier(opts *policy.Options) (Verifier, error) {
|
||||
err := populateDefaultOptions(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tufClient tuf.Downloader
|
||||
if !opts.DisableTUF {
|
||||
tufClient, err = tuf.NewClient(opts.TUFClientOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create TUF client: %w", err)
|
||||
}
|
||||
}
|
||||
return &tufVerifier{
|
||||
opts: opts,
|
||||
tufClient: tufClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (verifier *tufVerifier) Verify(ctx context.Context, src *oci.ImageSpec) (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
|
||||
imageName, err := detailsResolver.ImageName(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve image name: %w", err)
|
||||
}
|
||||
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)
|
||||
policyResolver := policy.NewResolver(verifier.tufClient, verifier.opts)
|
||||
resolvedPolicy, err := policyResolver.ResolvePolicy(ctx, imageName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve policy: %w", err)
|
||||
}
|
||||
|
||||
if pctx == nil {
|
||||
if resolvedPolicy == nil {
|
||||
return &VerificationResult{
|
||||
Outcome: OutcomeNoPolicy,
|
||||
}, nil
|
||||
}
|
||||
// this is overriding the mapping with a referrers config. Useful for testing if nothing else
|
||||
if opts.ReferrersRepo != "" {
|
||||
pctx.Mapping.Attestations = &config.AttestationConfig{
|
||||
Repo: opts.ReferrersRepo,
|
||||
if verifier.opts.ReferrersRepo != "" {
|
||||
resolvedPolicy.Mapping.Attestations = &config.AttestationConfig{
|
||||
Repo: verifier.opts.ReferrersRepo,
|
||||
Style: config.AttestationStyleReferrers,
|
||||
}
|
||||
} else if opts.AttestationStyle == config.AttestationStyleAttached {
|
||||
pctx.Mapping.Attestations = &config.AttestationConfig{
|
||||
Repo: opts.ReferrersRepo,
|
||||
} else if verifier.opts.AttestationStyle == config.AttestationStyleAttached {
|
||||
resolvedPolicy.Mapping.Attestations = &config.AttestationConfig{
|
||||
Repo: verifier.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)
|
||||
resolver, err := policy.CreateAttestationResolver(detailsResolver, resolvedPolicy.Mapping)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create attestation resolver: %w", err)
|
||||
}
|
||||
result, err = VerifyAttestations(ctx, resolver, pctx)
|
||||
evaluator := policy.NewRegoEvaluator(verifier.opts.Debug)
|
||||
result, err = VerifyAttestations(ctx, resolver, evaluator, resolvedPolicy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to evaluate policy: %w", err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func toVerificationResult(p *policy.Policy, input *policy.PolicyInput, result *policy.Result) (*VerificationResult, error) {
|
||||
func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.Options) (result *VerificationResult, err error) {
|
||||
verifier, err := NewVerifier(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return verifier.Verify(ctx, src)
|
||||
}
|
||||
|
||||
func populateDefaultOptions(opts *policy.Options) (err error) {
|
||||
if opts.LocalPolicyDir == "" && opts.DisableTUF {
|
||||
return fmt.Errorf("local policy dir must be set if not using TUF")
|
||||
}
|
||||
if opts.LocalTargetsDir == "" {
|
||||
opts.LocalTargetsDir, err = defaultLocalTargetsDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if opts.DisableTUF && opts.TUFClientOptions != nil {
|
||||
return fmt.Errorf("TUF client options set but TUF disabled")
|
||||
} else if opts.TUFClientOptions == nil && !opts.DisableTUF {
|
||||
opts.TUFClientOptions = tuf.NewDockerDefaultClientOptions(opts.LocalTargetsDir)
|
||||
}
|
||||
|
||||
if opts.AttestationStyle == "" {
|
||||
opts.AttestationStyle = config.AttestationStyleReferrers
|
||||
}
|
||||
if opts.ReferrersRepo != "" && opts.AttestationStyle != config.AttestationStyleReferrers {
|
||||
return fmt.Errorf("referrers repo specified but attestation source not set to referrers")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func defaultLocalTargetsDir() (string, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
return filepath.Join(homeDir, ".docker", "tuf"), nil
|
||||
}
|
||||
|
||||
func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.Result) (*VerificationResult, error) {
|
||||
dgst, err := oci.SplitDigest(input.Digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to split digest: %w", err)
|
||||
}
|
||||
subject := intoto.Subject{
|
||||
Name: input.Purl,
|
||||
Name: input.PURL,
|
||||
Digest: dgst,
|
||||
}
|
||||
resourceUri, err := attestation.ToVSAResourceURI(subject)
|
||||
resourceURI, err := attestation.ToVSAResourceURI(subject)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create resource uri: %w", err)
|
||||
}
|
||||
@@ -87,6 +157,8 @@ func toVerificationResult(p *policy.Policy, input *policy.PolicyInput, result *p
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vsaPolicy := attestation.VSAPolicy{URI: result.Summary.PolicyURI, DownloadLocation: p.URI, Digest: p.Digest}
|
||||
|
||||
return &VerificationResult{
|
||||
Policy: p,
|
||||
Outcome: outcome,
|
||||
@@ -103,8 +175,8 @@ func toVerificationResult(p *policy.Policy, input *policy.PolicyInput, result *p
|
||||
ID: result.Summary.Verifier,
|
||||
},
|
||||
TimeVerified: time.Now().UTC().Format(time.RFC3339),
|
||||
ResourceUri: resourceUri,
|
||||
Policy: attestation.VSAPolicy{URI: result.Summary.PolicyURI},
|
||||
ResourceURI: resourceURI,
|
||||
Policy: vsaPolicy,
|
||||
VerificationResult: outcomeStr,
|
||||
VerifiedLevels: result.Summary.SLSALevels,
|
||||
},
|
||||
@@ -112,7 +184,7 @@ func toVerificationResult(p *policy.Policy, input *policy.PolicyInput, result *p
|
||||
}, nil
|
||||
}
|
||||
|
||||
func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, pctx *policy.Policy) (*VerificationResult, error) {
|
||||
func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, evaluator policy.Evaluator, resolvedPolicy *policy.Policy) (*VerificationResult, error) {
|
||||
desc, err := resolver.ImageDescriptor(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get image descriptor: %w", err)
|
||||
@@ -127,7 +199,7 @@ func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, p
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pctx.ResolvedName != "" {
|
||||
if resolvedPolicy.ResolvedName != "" {
|
||||
// this means the name we have is not the one we want to use for policy evaluation
|
||||
// so we need to replace it with the one we resolved during policy resolution.
|
||||
// this can happen if the name is an alias for another image, e.g. if it is a mirror
|
||||
@@ -136,41 +208,46 @@ func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, p
|
||||
return nil, fmt.Errorf("failed to parse image name: %w", err)
|
||||
}
|
||||
oldName := ref.Name()
|
||||
name = strings.Replace(name, oldName, pctx.ResolvedName, 1)
|
||||
name = strings.Replace(name, oldName, resolvedPolicy.ResolvedName, 1)
|
||||
}
|
||||
|
||||
purl, canonical, err := oci.RefToPURL(name, platform)
|
||||
ref, err := reference.ParseNormalizedNamed(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse ref %q: %w", ref, err)
|
||||
}
|
||||
purl, canonical, err := oci.RefToPURL(ref, platform)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert ref to purl: %w", err)
|
||||
}
|
||||
input := &policy.PolicyInput{
|
||||
Digest: digest,
|
||||
Purl: purl,
|
||||
IsCanonical: canonical,
|
||||
var tag string
|
||||
if !canonical {
|
||||
// unlike the function name indicates, this adds latest if no tag is present
|
||||
ref = reference.TagNameOnly(ref)
|
||||
}
|
||||
|
||||
evaluator, err := policy.GetPolicyEvaluator(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if tagged, ok := ref.(reference.Tagged); ok {
|
||||
tag = tagged.Tag()
|
||||
}
|
||||
result, err := evaluator.Evaluate(ctx, resolver, pctx, input)
|
||||
input := &policy.Input{
|
||||
Digest: digest,
|
||||
PURL: purl,
|
||||
Platform: platform.String(),
|
||||
Domain: reference.Domain(ref),
|
||||
NormalizedName: reference.Path(ref),
|
||||
FamiliarName: reference.FamiliarName(ref),
|
||||
}
|
||||
// rego has null strings
|
||||
if tag != "" {
|
||||
input.Tag = tag
|
||||
}
|
||||
result, err := evaluator.Evaluate(ctx, resolver, resolvedPolicy, input)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("policy evaluation failed: %w", err)
|
||||
}
|
||||
verificationResult, err := toVerificationResult(pctx, input, result)
|
||||
verificationResult, err := toVerificationResult(resolvedPolicy, input, result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert to policy result: %w", err)
|
||||
}
|
||||
verificationResult.SubjectDescriptor = desc
|
||||
return verificationResult, nil
|
||||
}
|
||||
|
||||
func NewAttestationManifest(subject *v1.Descriptor) (*attestation.AttestationManifest, error) {
|
||||
return &attestation.AttestationManifest{
|
||||
OriginalDescriptor: &v1.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
},
|
||||
OriginalLayers: []*attestation.AttestationLayer{},
|
||||
SubjectDescriptor: subject,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -8,22 +8,19 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/config"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/policy"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
"github.com/docker/attest/pkg/tuf"
|
||||
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")
|
||||
)
|
||||
var ExampleAttestation = filepath.Join("..", "..", "test", "testdata", "example_attestation.json")
|
||||
|
||||
const (
|
||||
LinuxAMD64 = "linux/amd64"
|
||||
@@ -33,10 +30,10 @@ func TestVerifyAttestations(t *testing.T) {
|
||||
ex, err := os.ReadFile(ExampleAttestation)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var env = new(attestation.Envelope)
|
||||
env := new(attestation.Envelope)
|
||||
err = json.Unmarshal(ex, env)
|
||||
assert.NoError(t, err)
|
||||
resolver := &test.MockResolver{
|
||||
resolver := &attestation.MockResolver{
|
||||
Envs: []*attestation.Envelope{env},
|
||||
}
|
||||
|
||||
@@ -48,18 +45,16 @@ func TestVerifyAttestations(t *testing.T) {
|
||||
{"policy ok", nil, nil},
|
||||
{"policy error", fmt.Errorf("policy error"), fmt.Errorf("policy evaluation failed: policy error")},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
mockPE := policy.MockPolicyEvaluator{
|
||||
EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, pctx *policy.Policy, input *policy.PolicyInput) (*policy.Result, error) {
|
||||
EvaluateFunc: func(_ context.Context, _ attestation.Resolver, _ *policy.Policy, _ *policy.Input) (*policy.Result, error) {
|
||||
return policy.AllowedResult(), tc.policyEvaluationError
|
||||
},
|
||||
}
|
||||
|
||||
ctx := policy.WithPolicyEvaluator(context.Background(), &mockPE)
|
||||
_, err := VerifyAttestations(ctx, resolver, &policy.Policy{ResolvedName: ""})
|
||||
_, err := VerifyAttestations(ctx, resolver, &mockPE, &policy.Policy{ResolvedName: ""})
|
||||
if tc.expectedError != nil {
|
||||
if assert.Error(t, err) {
|
||||
assert.Equal(t, tc.expectedError.Error(), err.Error())
|
||||
@@ -73,12 +68,11 @@ func TestVerifyAttestations(t *testing.T) {
|
||||
|
||||
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{}
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
assert.NoError(t, err)
|
||||
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
||||
require.NoError(t, err)
|
||||
@@ -87,32 +81,25 @@ func TestVSA(t *testing.T) {
|
||||
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)
|
||||
spec, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64))
|
||||
require.NoError(t, err)
|
||||
err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// mocked vsa query should pass
|
||||
policyOpts := &policy.PolicyOptions{
|
||||
LocalPolicyDir: PassPolicyDir,
|
||||
policyOpts := &policy.Options{
|
||||
LocalPolicyDir: PassPolicyDir,
|
||||
AttestationStyle: config.AttestationStyleAttached,
|
||||
DisableTUF: true,
|
||||
}
|
||||
src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64))
|
||||
require.NoError(t, err)
|
||||
results, err := Verify(ctx, src, policyOpts)
|
||||
results, err := Verify(ctx, spec, 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.NotNil(t, results.Input.Tag)
|
||||
}
|
||||
|
||||
assert.Equal(t, intoto.StatementInTotoV01, results.VSA.Type)
|
||||
@@ -120,22 +107,24 @@ func TestVSA(t *testing.T) {
|
||||
assert.Len(t, results.VSA.Subject, 1)
|
||||
|
||||
require.IsType(t, attestation.VSAPredicate{}, results.VSA.Predicate)
|
||||
attestationPredicate := results.VSA.Predicate.(attestation.VSAPredicate)
|
||||
attestationPredicate, ok := results.VSA.Predicate.(attestation.VSAPredicate)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, "PASSED", attestationPredicate.VerificationResult)
|
||||
assert.Equal(t, "docker-official-images", attestationPredicate.Verifier.ID)
|
||||
assert.Equal(t, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels)
|
||||
assert.Equal(t, PassPolicyDir+"/policy.rego", attestationPredicate.Policy.DownloadLocation)
|
||||
assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI)
|
||||
assert.Equal(t, map[string]string{"sha256": "d71d6b8f49fcba1295b16f5394dd5863a14e4277eb663d66d8c48e392509afe0"}, attestationPredicate.Policy.Digest)
|
||||
}
|
||||
|
||||
func TestVerificationFailure(t *testing.T) {
|
||||
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{}
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
assert.NoError(t, err)
|
||||
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
||||
require.NoError(t, err)
|
||||
@@ -144,25 +133,18 @@ func TestVerificationFailure(t *testing.T) {
|
||||
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)
|
||||
spec, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64))
|
||||
require.NoError(t, err)
|
||||
err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// mocked vsa query should fail
|
||||
policyOpts := &policy.PolicyOptions{
|
||||
LocalPolicyDir: FailPolicyDir,
|
||||
policyOpts := &policy.Options{
|
||||
LocalPolicyDir: FailPolicyDir,
|
||||
AttestationStyle: config.AttestationStyleAttached,
|
||||
DisableTUF: true,
|
||||
}
|
||||
src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64))
|
||||
require.NoError(t, err)
|
||||
results, err := Verify(ctx, src, policyOpts)
|
||||
results, err := Verify(ctx, spec, policyOpts)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, OutcomeFailure, results.Outcome)
|
||||
assert.Len(t, results.Violations, 1)
|
||||
@@ -177,35 +159,38 @@ func TestVerificationFailure(t *testing.T) {
|
||||
assert.Len(t, results.VSA.Subject, 1)
|
||||
|
||||
require.IsType(t, attestation.VSAPredicate{}, results.VSA.Predicate)
|
||||
attestationPredicate := results.VSA.Predicate.(attestation.VSAPredicate)
|
||||
attestationPredicate, ok := results.VSA.Predicate.(attestation.VSAPredicate)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, "FAILED", attestationPredicate.VerificationResult)
|
||||
assert.Equal(t, "docker-official-images", attestationPredicate.Verifier.ID)
|
||||
assert.Equal(t, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels)
|
||||
assert.Equal(t, FailPolicyDir+"/policy.rego", attestationPredicate.Policy.DownloadLocation)
|
||||
assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI)
|
||||
assert.Equal(t, map[string]string{"sha256": "ad045e1bd7cd602d90196acf68f2c57d7b51565d59e6e30e30d94ae86aa16201"}, attestationPredicate.Policy.Digest)
|
||||
}
|
||||
|
||||
func TestSignVerify(t *testing.T) {
|
||||
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
|
||||
imageName string
|
||||
expectError bool
|
||||
name string
|
||||
signTL bool
|
||||
policyDir string
|
||||
imageName string
|
||||
expectedNonSuccess Outcome
|
||||
}{
|
||||
{name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir},
|
||||
{name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir},
|
||||
{name: "no tl", signTL: false, policyDir: PassPolicyDir},
|
||||
{name: "mirror", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "mirror.org/library/test-image:test"},
|
||||
{name: "mirror no match", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectError: true},
|
||||
{name: "mirror no match", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectedNonSuccess: OutcomeNoPolicy},
|
||||
{name: "verify inputs", signTL: false, policyDir: InputsPolicyDir},
|
||||
}
|
||||
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -225,35 +210,100 @@ func TestSignVerify(t *testing.T) {
|
||||
imageName = attIdx.Name
|
||||
}
|
||||
// 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: imageName,
|
||||
},
|
||||
},
|
||||
})
|
||||
_, err = layout.Write(outputLayout, idx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
policyOpts := &policy.PolicyOptions{
|
||||
LocalPolicyDir: tc.policyDir,
|
||||
}
|
||||
src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64))
|
||||
spec, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64))
|
||||
require.NoError(t, err)
|
||||
results, err := Verify(ctx, src, policyOpts)
|
||||
if tc.expectError {
|
||||
require.Error(t, err)
|
||||
err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, imageName)
|
||||
require.NoError(t, err)
|
||||
|
||||
policyOpts := &policy.Options{
|
||||
LocalPolicyDir: tc.policyDir,
|
||||
DisableTUF: true,
|
||||
}
|
||||
results, err := Verify(ctx, spec, policyOpts)
|
||||
require.NoError(t, err)
|
||||
if tc.expectedNonSuccess != "" {
|
||||
assert.Equal(t, tc.expectedNonSuccess, results.Outcome)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, OutcomeSuccess, results.Outcome)
|
||||
platform, err := oci.ParsePlatform(LinuxAMD64)
|
||||
require.NoError(t, err)
|
||||
expectedPURL, _, err := oci.RefToPURL(attIdx.Name, platform)
|
||||
|
||||
ref, err := reference.ParseNormalizedNamed(attIdx.Name)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedPURL, results.Input.Purl)
|
||||
expectedPURL, _, err := oci.RefToPURL(ref, platform)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedPURL, results.Input.PURL)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultOptions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
tufOpts *tuf.ClientOptions
|
||||
localTargetsDir string
|
||||
attestationStyle config.AttestationStyle
|
||||
referrersRepo string
|
||||
expectedError string
|
||||
disableTuf bool
|
||||
localPolicyDir string
|
||||
}{
|
||||
{name: "empty"},
|
||||
{name: "tufClient provided", tufOpts: &tuf.ClientOptions{MetadataSource: "a", TargetsSource: "b"}},
|
||||
{name: "localTargetsDir provided", localTargetsDir: test.CreateTempDir(t, "", TestTempDir)},
|
||||
{name: "attestationStyle provided", attestationStyle: config.AttestationStyleAttached},
|
||||
{name: "referrersRepo provided", referrersRepo: "referrers"},
|
||||
{name: "referrersRepo provided with attached", referrersRepo: "referrers", attestationStyle: config.AttestationStyleAttached, expectedError: "referrers repo specified but attestation source not set to referrers"},
|
||||
{name: "tuf disabled and no local-policy-dir", disableTuf: true, expectedError: "local policy dir must be set if not using TUF"},
|
||||
{name: "tuf disabled but options set", disableTuf: true, tufOpts: &tuf.ClientOptions{MetadataSource: "a", TargetsSource: "b"}, localPolicyDir: "foo", expectedError: "TUF client options set but TUF disabled"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defaultTargets, err := defaultLocalTargetsDir()
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := &policy.Options{
|
||||
TUFClientOptions: tc.tufOpts,
|
||||
LocalTargetsDir: tc.localTargetsDir,
|
||||
AttestationStyle: tc.attestationStyle,
|
||||
ReferrersRepo: tc.referrersRepo,
|
||||
DisableTUF: tc.disableTuf,
|
||||
LocalPolicyDir: tc.localPolicyDir,
|
||||
}
|
||||
|
||||
err = populateDefaultOptions(opts)
|
||||
if tc.expectedError != "" {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, tc.expectedError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
if tc.localTargetsDir != "" {
|
||||
assert.Equal(t, tc.localTargetsDir, opts.LocalTargetsDir)
|
||||
} else {
|
||||
assert.Equal(t, defaultTargets, opts.LocalTargetsDir)
|
||||
}
|
||||
|
||||
if tc.attestationStyle != "" {
|
||||
assert.Equal(t, tc.attestationStyle, opts.AttestationStyle)
|
||||
} else {
|
||||
assert.Equal(t, config.AttestationStyleReferrers, opts.AttestationStyle)
|
||||
}
|
||||
|
||||
if tc.tufOpts != nil {
|
||||
assert.Equal(t, tc.tufOpts, opts.TUFClientOptions)
|
||||
} else {
|
||||
assert.NotNil(t, opts.TUFClientOptions)
|
||||
}
|
||||
|
||||
if tc.referrersRepo != "" {
|
||||
assert.Equal(t, tc.referrersRepo, opts.ReferrersRepo)
|
||||
} else {
|
||||
assert.Empty(t, opts.ReferrersRepo)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
4
pkg/attestation/README.md
Normal file
4
pkg/attestation/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## attestations
|
||||
This package is for components that deal with the creation, storage, and retrieval of signed attestions using OCI.
|
||||
|
||||
For more generic OCI components see the `oci` package.
|
||||
@@ -2,12 +2,16 @@ package attestation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
"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/layout"
|
||||
"github.com/google/go-containerregistry/pkg/v1/match"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
@@ -17,19 +21,32 @@ import (
|
||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||
)
|
||||
|
||||
// GetAttestationManifestsFromIndex extracts all attestation manifests from an index
|
||||
func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*AttestationManifest, error) {
|
||||
// NewManifest creates a new attestation manifest from a descriptor.
|
||||
func NewManifest(subject *v1.Descriptor) (*Manifest, error) {
|
||||
return &Manifest{
|
||||
OriginalDescriptor: &v1.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
},
|
||||
OriginalLayers: []*Layer{},
|
||||
SubjectDescriptor: subject,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ManifestsFromIndex extracts all attestation manifests from an index.
|
||||
func ManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, error) {
|
||||
idx, err := index.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
||||
}
|
||||
subjects := make(map[string]*v1.Descriptor)
|
||||
for _, subject := range idx.Manifests {
|
||||
subjects[subject.Digest.String()] = &subject
|
||||
for i := range idx.Manifests {
|
||||
subject := &idx.Manifests[i]
|
||||
subjects[subject.Digest.String()] = subject
|
||||
}
|
||||
|
||||
var attestationManifests []*AttestationManifest
|
||||
for _, desc := range idx.Manifests {
|
||||
var attestationManifests []*Manifest
|
||||
for i := range idx.Manifests {
|
||||
desc := idx.Manifests[i]
|
||||
if desc.Annotations[DockerReferenceType] == AttestationManifestType {
|
||||
subject := subjects[desc.Annotations[DockerReferenceDigest]]
|
||||
if subject == nil {
|
||||
@@ -39,27 +56,28 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*AttestationManife
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", desc.Digest.String(), err)
|
||||
}
|
||||
attestationLayers, err := GetAttestationsFromImage(attestationImage)
|
||||
attestationLayers, err := layersFromImage(attestationImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
|
||||
}
|
||||
attestationManifests = append(attestationManifests,
|
||||
&AttestationManifest{
|
||||
&Manifest{
|
||||
OriginalDescriptor: &desc,
|
||||
SubjectDescriptor: subject,
|
||||
OriginalLayers: attestationLayers})
|
||||
OriginalLayers: attestationLayers,
|
||||
})
|
||||
}
|
||||
}
|
||||
return attestationManifests, nil
|
||||
}
|
||||
|
||||
// GetAttestationsFromImage extracts all attestation layers from an image
|
||||
func GetAttestationsFromImage(image v1.Image) ([]*AttestationLayer, error) {
|
||||
// LayersFromImage extracts all attestation layers from an image.
|
||||
func layersFromImage(image v1.Image) ([]*Layer, error) {
|
||||
layers, err := image.Layers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract layers from image: %w", err)
|
||||
}
|
||||
var attestationLayers []*AttestationLayer
|
||||
var attestationLayers []*Layer
|
||||
for _, layer := range layers {
|
||||
// parse layer blob as json
|
||||
r, err := layer.Uncompressed()
|
||||
@@ -78,19 +96,19 @@ func GetAttestationsFromImage(image v1.Image) ([]*AttestationLayer, error) {
|
||||
// copy original annotations
|
||||
ann := maps.Clone(layerDesc.Annotations)
|
||||
// only decode intoto statements
|
||||
var stmt = new(intoto.Statement)
|
||||
stmt := new(intoto.Statement)
|
||||
if mt == types.MediaType(intoto.PayloadType) {
|
||||
err = json.NewDecoder(r).Decode(&stmt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode statement layer contents: %w", err)
|
||||
}
|
||||
}
|
||||
attestationLayers = append(attestationLayers, &AttestationLayer{Layer: layer, Statement: stmt, Annotations: ann})
|
||||
attestationLayers = append(attestationLayers, &Layer{Layer: layer, Statement: stmt, Annotations: ann})
|
||||
}
|
||||
return attestationLayers, nil
|
||||
}
|
||||
|
||||
func (manifest *AttestationManifest) AddAttestation(ctx context.Context, signer dsse.SignerVerifier, statement *intoto.Statement, opts *SigningOptions) error {
|
||||
func (manifest *Manifest) Add(ctx context.Context, signer dsse.SignerVerifier, statement *intoto.Statement, opts *SigningOptions) error {
|
||||
layer, err := createSignedImageLayer(ctx, statement, signer, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create signed layer: %w", err)
|
||||
@@ -99,9 +117,9 @@ func (manifest *AttestationManifest) AddAttestation(ctx context.Context, signer
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*AttestationLayer, error) {
|
||||
func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*Layer, error) {
|
||||
// sign the statement
|
||||
env, err := SignInTotoStatement(ctx, statement, signer, opts)
|
||||
env, err := signInTotoStatement(ctx, statement, signer, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign statement: %w", err)
|
||||
}
|
||||
@@ -114,7 +132,7 @@ func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, si
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal envelope: %w", err)
|
||||
}
|
||||
return &AttestationLayer{
|
||||
return &Layer{
|
||||
Statement: statement,
|
||||
Annotations: map[string]string{
|
||||
InTotoPredicateType: statement.PredicateType,
|
||||
@@ -124,7 +142,7 @@ func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, si
|
||||
}, nil
|
||||
}
|
||||
|
||||
func SignInTotoStatement(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) {
|
||||
func signInTotoStatement(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) {
|
||||
payload, err := json.Marshal(statement)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal statement: %w", err)
|
||||
@@ -136,12 +154,12 @@ func SignInTotoStatement(ctx context.Context, statement *intoto.Statement, signe
|
||||
return env, nil
|
||||
}
|
||||
|
||||
func UpdateIndexImage(
|
||||
func updateImageIndex(
|
||||
idx v1.ImageIndex,
|
||||
manifest *AttestationManifest,
|
||||
options ...func(*AttestationManifestImageOptions) error) (v1.ImageIndex, error) {
|
||||
image, err := manifest.BuildAttestationImage(options...)
|
||||
|
||||
manifest *Manifest,
|
||||
options ...func(*ManifestImageOptions) error,
|
||||
) (v1.ImageIndex, error) {
|
||||
image, err := manifest.BuildImage(options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build image: %w", err)
|
||||
}
|
||||
@@ -163,10 +181,10 @@ func UpdateIndexImage(
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
func UpdateIndexImages(idx v1.ImageIndex, manifest []*AttestationManifest, options ...func(*AttestationManifestImageOptions) error) (v1.ImageIndex, error) {
|
||||
func UpdateIndexImages(idx v1.ImageIndex, manifest []*Manifest, options ...func(*ManifestImageOptions) error) (v1.ImageIndex, error) {
|
||||
var err error
|
||||
for _, m := range manifest {
|
||||
idx, err = UpdateIndexImage(idx, m, options...)
|
||||
idx, err = updateImageIndex(idx, m, options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add image to index: %w", err)
|
||||
}
|
||||
@@ -174,8 +192,8 @@ func UpdateIndexImages(idx v1.ImageIndex, manifest []*AttestationManifest, optio
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
func newOptions(options ...func(*AttestationManifestImageOptions) error) (*AttestationManifestImageOptions, error) {
|
||||
opts := &AttestationManifestImageOptions{}
|
||||
func newOptions(options ...func(*ManifestImageOptions) error) (*ManifestImageOptions, error) {
|
||||
opts := &ManifestImageOptions{}
|
||||
for _, opt := range options {
|
||||
err := opt(opts)
|
||||
if err != nil {
|
||||
@@ -185,22 +203,22 @@ func newOptions(options ...func(*AttestationManifestImageOptions) error) (*Attes
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func WithoutSubject(skipSubject bool) func(*AttestationManifestImageOptions) error {
|
||||
return func(r *AttestationManifestImageOptions) error {
|
||||
func WithoutSubject(skipSubject bool) func(*ManifestImageOptions) error {
|
||||
return func(r *ManifestImageOptions) error {
|
||||
r.skipSubject = skipSubject
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithReplacedLayers(replaceLayers bool) func(*AttestationManifestImageOptions) error {
|
||||
return func(r *AttestationManifestImageOptions) error {
|
||||
func WithReplacedLayers(replaceLayers bool) func(*ManifestImageOptions) error {
|
||||
return func(r *ManifestImageOptions) error {
|
||||
r.replaceLayers = replaceLayers
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// build an image with signed attestations, optionally replacing existing layers with signed layers
|
||||
func (manifest *AttestationManifest) BuildAttestationImage(options ...func(*AttestationManifestImageOptions) error) (v1.Image, error) {
|
||||
// build an image with signed attestations, optionally replacing existing layers with signed layers.
|
||||
func (manifest *Manifest) BuildImage(options ...func(*ManifestImageOptions) error) (v1.Image, error) {
|
||||
opts, err := newOptions(options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create options: %w", err)
|
||||
@@ -218,26 +236,26 @@ func (manifest *AttestationManifest) BuildAttestationImage(options ...func(*Atte
|
||||
break
|
||||
}
|
||||
}
|
||||
//add existing layers if they've not been signed or we're not replacing them
|
||||
// add existing layers if they've not been signed or we're not replacing them
|
||||
if !found || !opts.replaceLayers {
|
||||
resultLayers = append(resultLayers, existingLayer)
|
||||
}
|
||||
}
|
||||
// so taht we attach all attestations to a single attestations image - as per current buildkit
|
||||
// so that we attach all attestations to a single attestations image - as per current buildkit
|
||||
opts.laxReferrers = true
|
||||
newImg, err := buildImage(resultLayers, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts)
|
||||
newImg, err := buildImageFromLayers(resultLayers, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build image: %w", err)
|
||||
}
|
||||
return newImg, nil
|
||||
}
|
||||
|
||||
// build an image per attestation (layer) suitable for use as Referrers
|
||||
func (manifest *AttestationManifest) BuildReferringArtifacts() ([]v1.Image, error) {
|
||||
// build an image per attestation (layer) suitable for use as Referrers.
|
||||
func (manifest *Manifest) BuildReferringArtifacts() ([]v1.Image, error) {
|
||||
var images []v1.Image
|
||||
for _, layer := range manifest.SignedLayers {
|
||||
opts := &AttestationManifestImageOptions{}
|
||||
newImg, err := buildImage([]*AttestationLayer{layer}, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts)
|
||||
opts := &ManifestImageOptions{}
|
||||
newImg, err := buildImageFromLayers([]*Layer{layer}, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build image: %w", err)
|
||||
}
|
||||
@@ -246,15 +264,15 @@ func (manifest *AttestationManifest) BuildReferringArtifacts() ([]v1.Image, erro
|
||||
return images, nil
|
||||
}
|
||||
|
||||
// build and image containing only layers
|
||||
func buildImage(layers []*AttestationLayer, manifest *v1.Descriptor, subject *v1.Descriptor, opts *AttestationManifestImageOptions) (v1.Image, error) {
|
||||
// build an image containing only layers provided.
|
||||
func buildImageFromLayers(layers []*Layer, manifest *v1.Descriptor, subject *v1.Descriptor, opts *ManifestImageOptions) (v1.Image, error) {
|
||||
newImg := empty.Image
|
||||
var err error
|
||||
if len(layers) == 0 {
|
||||
return nil, fmt.Errorf("no layers supplied to build image")
|
||||
}
|
||||
// NB: if we add the subject before the layers, it does not end up being computed/serialised in the output for some reason
|
||||
//TODO - recreate this bug and push upstream
|
||||
// NB: if we add the subject before the layers, it does not end up being computed/serialized in the output for some reason
|
||||
// TODO - recreate this bug and push upstream
|
||||
for _, layer := range layers {
|
||||
add := mutate.Addendum{
|
||||
Layer: layer.Layer,
|
||||
@@ -284,41 +302,143 @@ func buildImage(layers []*AttestationLayer, manifest *v1.Descriptor, subject *v1
|
||||
// see note above - must be added after the layers!
|
||||
if !opts.skipSubject {
|
||||
subject.Platform = nil
|
||||
newImg = mutate.Subject(newImg, *subject).(v1.Image)
|
||||
ok := false
|
||||
newImg, ok = mutate.Subject(newImg, *subject).(v1.Image)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to set subject: %w", err)
|
||||
}
|
||||
}
|
||||
if !opts.laxReferrers {
|
||||
// as per https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidance-for-an-empty-descriptor
|
||||
newImg = &EmptyConfigImage{newImg}
|
||||
newImg = &oci.EmptyConfigImage{Image: newImg}
|
||||
}
|
||||
return newImg, nil
|
||||
}
|
||||
|
||||
type EmptyConfigImage struct {
|
||||
v1.Image
|
||||
}
|
||||
|
||||
func (i *EmptyConfigImage) RawConfigFile() ([]byte, error) {
|
||||
return []byte("{}"), nil
|
||||
}
|
||||
|
||||
func (i *EmptyConfigImage) Manifest() (*v1.Manifest, error) {
|
||||
mf, err := i.Image.Manifest()
|
||||
func ExtractEnvelopes(manifest *Manifest, predicateType string) ([]*Envelope, error) {
|
||||
var envs []*Envelope
|
||||
dsseMediaType, err := DSSEMediaType(predicateType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
||||
return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err)
|
||||
}
|
||||
mf.Config = v1.Descriptor{
|
||||
MediaType: "application/vnd.oci.empty.v1+json",
|
||||
Size: 2,
|
||||
Digest: v1.Hash{Algorithm: "sha256", Hex: "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"},
|
||||
Data: []byte("{}"),
|
||||
for _, attestationLayer := range manifest.OriginalLayers {
|
||||
mt, err := attestationLayer.Layer.MediaType()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||
}
|
||||
if string(mt) == dsseMediaType {
|
||||
reader, err := attestationLayer.Layer.Uncompressed()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
env := new(Envelope)
|
||||
err = json.NewDecoder(reader).Decode(&env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode envelope: %w", err)
|
||||
}
|
||||
envs = append(envs, env)
|
||||
}
|
||||
}
|
||||
return mf, nil
|
||||
|
||||
return envs, nil
|
||||
}
|
||||
|
||||
func (i *EmptyConfigImage) RawManifest() ([]byte, error) {
|
||||
mf, err := i.Manifest()
|
||||
func ExtractStatementsFromIndex(idx v1.ImageIndex, mediaType string) ([]*AnnotatedStatement, error) {
|
||||
mfs2, err := idx.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
||||
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
||||
}
|
||||
return json.Marshal(mf)
|
||||
|
||||
var statements []*AnnotatedStatement
|
||||
|
||||
for i := range mfs2.Manifests {
|
||||
mf := &mfs2.Manifests[i]
|
||||
if mf.Annotations[DockerReferenceType] != "attestation-manifest" {
|
||||
continue
|
||||
}
|
||||
|
||||
attestationImage, err := idx.Image(mf.Digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
|
||||
}
|
||||
layers, err := attestationImage.Layers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err)
|
||||
}
|
||||
|
||||
for _, layer := range layers {
|
||||
// parse layer blob as json
|
||||
mt, err := layer.MediaType()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||
}
|
||||
|
||||
if string(mt) != mediaType {
|
||||
continue
|
||||
}
|
||||
r, err := layer.Uncompressed()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||
}
|
||||
defer r.Close()
|
||||
inTotoStatement := new(intoto.Statement)
|
||||
var desc *v1.Descriptor
|
||||
if strings.HasSuffix(string(mt), "+dsse") {
|
||||
env := new(Envelope)
|
||||
err = json.NewDecoder(r).Decode(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode env: %w", err)
|
||||
}
|
||||
payload, err := base64.StdEncoding.Strict().DecodeString(env.Payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode payload: %w", err)
|
||||
}
|
||||
err = json.Unmarshal([]byte(payload), inTotoStatement)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode %s statement: %w", mediaType, err)
|
||||
}
|
||||
} else {
|
||||
desc := new(v1.Descriptor)
|
||||
err = json.NewDecoder(r).Decode(desc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode statement: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
layerDesc, err := partial.Descriptor(layer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get descriptor for layer: %w", err)
|
||||
}
|
||||
annotations := make(map[string]string)
|
||||
for k, v := range layerDesc.Annotations {
|
||||
annotations[k] = v
|
||||
}
|
||||
statements = append(statements, &AnnotatedStatement{
|
||||
OCIDescriptor: desc,
|
||||
InTotoStatement: inTotoStatement,
|
||||
Annotations: annotations,
|
||||
})
|
||||
}
|
||||
}
|
||||
return statements, nil
|
||||
}
|
||||
|
||||
func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStatement, error) {
|
||||
idx, err := layout.ImageIndexFromPath(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load image index: %w", err)
|
||||
}
|
||||
|
||||
idxm, err := idx.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get digest: %w", err)
|
||||
}
|
||||
idxDigest := idxm.Manifests[0].Digest
|
||||
|
||||
mfs, err := idx.ImageIndex(idxDigest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
|
||||
}
|
||||
return ExtractStatementsFromIndex(mfs, mediaType)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
package test
|
||||
package attestation_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||
)
|
||||
|
||||
const (
|
||||
ExpectedStatements = 4
|
||||
)
|
||||
const ExpectedStatements = 4
|
||||
|
||||
func TestExtractAnnotatedStatements(t *testing.T) {
|
||||
statements, err := ExtractAnnotatedStatements(UnsignedTestImage, intoto.PayloadType)
|
||||
statements, err := attestation.ExtractAnnotatedStatements(test.UnsignedTestImage, intoto.PayloadType)
|
||||
assert.NoError(t, err)
|
||||
assert.Equalf(t, len(statements), ExpectedStatements, "expected %d statement, got %d", ExpectedStatements, len(statements))
|
||||
}
|
||||
@@ -4,9 +4,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/docker/attest/pkg/attest"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/mirror"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/signerverifier"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
@@ -14,7 +12,7 @@ import (
|
||||
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
|
||||
)
|
||||
|
||||
func ExampleAttestationManifest() {
|
||||
func ExampleManifest() {
|
||||
// configure signerverifier
|
||||
// local signer (unsafe for production)
|
||||
signer, err := signerverifier.GenKeyPair()
|
||||
@@ -55,7 +53,7 @@ func ExampleAttestationManifest() {
|
||||
ID: "test-verifier",
|
||||
},
|
||||
TimeVerified: time.Now().UTC().Format(time.RFC3339),
|
||||
ResourceUri: "some-uri",
|
||||
ResourceURI: "some-uri",
|
||||
Policy: attestation.VSAPolicy{URI: "some-uri"},
|
||||
VerificationResult: "PASSED",
|
||||
VerifiedLevels: []string{"SLSA_BUILD_LEVEL_1"},
|
||||
@@ -63,13 +61,13 @@ func ExampleAttestationManifest() {
|
||||
}
|
||||
|
||||
// create a new manifest to hold the attestation
|
||||
manifest, err := attest.NewAttestationManifest(desc)
|
||||
manifest, err := attestation.NewManifest(desc)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// sign and add the attestation to the manifest
|
||||
err = manifest.AddAttestation(context.Background(), signer, statement, opts)
|
||||
err = manifest.Add(context.Background(), signer, statement, opts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -80,7 +78,11 @@ func ExampleAttestationManifest() {
|
||||
}
|
||||
|
||||
// save the manifest to the registry as a referrers artifact
|
||||
err = mirror.SaveReferrers(manifest, output)
|
||||
artifacts, err := manifest.BuildReferringArtifacts()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = oci.SaveImagesNoTag(artifacts, output)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -1,52 +1,51 @@
|
||||
package oci
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
att "github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
)
|
||||
|
||||
// implementation of AttestationResolver that closes over attestations from an oci layout
|
||||
type OCILayoutResolver struct {
|
||||
*attestation.AttestationManifest
|
||||
*ImageSpec
|
||||
// implementation of Resolver that closes over attestations from an oci layout.
|
||||
type LayoutResolver struct {
|
||||
*Manifest
|
||||
*oci.ImageSpec
|
||||
}
|
||||
|
||||
func NewOCILayoutAttestationResolver(src *ImageSpec) (*OCILayoutResolver, error) {
|
||||
r := &OCILayoutResolver{
|
||||
func NewOCILayoutResolver(src *oci.ImageSpec) (*LayoutResolver, error) {
|
||||
r := &LayoutResolver{
|
||||
ImageSpec: src,
|
||||
}
|
||||
_, err := r.fetchAttestationManifest()
|
||||
_, err := r.fetchManifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *OCILayoutResolver) fetchAttestationManifest() (*attestation.AttestationManifest, error) {
|
||||
if r.AttestationManifest == nil {
|
||||
m, err := attestationManifestFromOCILayout(r.Identifier, r.ImageSpec.Platform)
|
||||
func (r *LayoutResolver) fetchManifest() (*Manifest, error) {
|
||||
if r.Manifest == nil {
|
||||
m, err := manifestFromOCILayout(r.Identifier, r.ImageSpec.Platform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.AttestationManifest = m
|
||||
r.Manifest = m
|
||||
}
|
||||
|
||||
return r.AttestationManifest, nil
|
||||
return r.Manifest, nil
|
||||
}
|
||||
|
||||
func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
||||
var envs []*att.Envelope
|
||||
dsseMediaType, err := attestation.DSSEMediaType(predicateType)
|
||||
func (r *LayoutResolver) Attestations(_ context.Context, predicateType string) ([]*Envelope, error) {
|
||||
var envs []*Envelope
|
||||
dsseMediaType, err := DSSEMediaType(predicateType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err)
|
||||
}
|
||||
for _, attestationLayer := range r.AttestationManifest.OriginalLayers {
|
||||
for _, attestationLayer := range r.Manifest.OriginalLayers {
|
||||
mt, err := attestationLayer.Layer.MediaType()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||
@@ -55,10 +54,9 @@ func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType stri
|
||||
if mts != dsseMediaType {
|
||||
continue
|
||||
}
|
||||
var env = new(att.Envelope)
|
||||
env := new(Envelope)
|
||||
// parse layer blob as json
|
||||
r, err := attestationLayer.Layer.Uncompressed()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||
}
|
||||
@@ -72,19 +70,19 @@ func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType stri
|
||||
return envs, nil
|
||||
}
|
||||
|
||||
func (r *OCILayoutResolver) ImageName(ctx context.Context) (string, error) {
|
||||
func (r *LayoutResolver) ImageName(_ context.Context) (string, error) {
|
||||
return r.SubjectName, nil
|
||||
}
|
||||
|
||||
func (r *OCILayoutResolver) ImageDescriptor(ctx context.Context) (*v1.Descriptor, error) {
|
||||
func (r *LayoutResolver) ImageDescriptor(_ context.Context) (*v1.Descriptor, error) {
|
||||
return r.SubjectDescriptor, nil
|
||||
}
|
||||
|
||||
func (r *OCILayoutResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
|
||||
func (r *LayoutResolver) ImagePlatform(_ context.Context) (*v1.Platform, error) {
|
||||
return r.ImageSpec.Platform, nil
|
||||
}
|
||||
|
||||
func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*attestation.AttestationManifest, error) {
|
||||
func manifestFromOCILayout(path string, platform *v1.Platform) (*Manifest, error) {
|
||||
idx, err := layout.ImageIndexFromPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -96,7 +94,6 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*atte
|
||||
}
|
||||
|
||||
idxDescriptor := idxm.Manifests[0]
|
||||
name := idxDescriptor.Annotations["org.opencontainers.image.ref.name"]
|
||||
idxDigest := idxDescriptor.Digest
|
||||
|
||||
mfs, err := idx.ImageIndex(idxDigest)
|
||||
@@ -108,18 +105,25 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*atte
|
||||
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
||||
}
|
||||
var subjectDescriptor *v1.Descriptor
|
||||
for _, mf := range mfs2.Manifests {
|
||||
if mf.Platform.Equals(*platform) {
|
||||
subjectDescriptor = &mf
|
||||
break
|
||||
for i := range mfs2.Manifests {
|
||||
manifest := &mfs2.Manifests[i]
|
||||
if manifest.Platform != nil {
|
||||
if manifest.Platform.Equals(*platform) {
|
||||
subjectDescriptor = manifest
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, mf := range mfs2.Manifests {
|
||||
if mf.Annotations[att.DockerReferenceType] != attestation.AttestationManifestType {
|
||||
if subjectDescriptor == nil {
|
||||
return nil, fmt.Errorf("platform not found in index")
|
||||
}
|
||||
for i := range mfs2.Manifests {
|
||||
mf := &mfs2.Manifests[i]
|
||||
if mf.Annotations[DockerReferenceType] != AttestationManifestType {
|
||||
continue
|
||||
}
|
||||
|
||||
if mf.Annotations[att.DockerReferenceDigest] != subjectDescriptor.Digest.String() {
|
||||
if mf.Annotations[DockerReferenceDigest] != subjectDescriptor.Digest.String() {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -127,14 +131,14 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*atte
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
|
||||
}
|
||||
layers, err := attestation.GetAttestationsFromImage(attestationImage)
|
||||
layers, err := layersFromImage(attestationImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
|
||||
}
|
||||
attest := &attestation.AttestationManifest{
|
||||
attest := &Manifest{
|
||||
OriginalLayers: layers,
|
||||
OriginalDescriptor: &mf,
|
||||
SubjectName: name,
|
||||
OriginalDescriptor: mf,
|
||||
SubjectName: idxDescriptor.Annotations["org.opencontainers.image.ref.name"],
|
||||
SubjectDescriptor: subjectDescriptor,
|
||||
}
|
||||
return attest, nil
|
||||
68
pkg/attestation/layout_test.go
Normal file
68
pkg/attestation/layout_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package attestation_test
|
||||
|
||||
import (
|
||||
"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/oci"
|
||||
"github.com/docker/attest/pkg/policy"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAttestationFromOCILayout(t *testing.T) {
|
||||
ctx, signer := test.Setup(t)
|
||||
outputLayout := test.CreateTempDir(t, "", "attest-oci-layout")
|
||||
|
||||
invalidPlatform := &v1.Platform{
|
||||
Architecture: "invalid",
|
||||
OS: "invalid",
|
||||
}
|
||||
|
||||
opts := &attestation.SigningOptions{}
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
||||
require.NoError(t, err)
|
||||
signedIndex := attIdx.Index
|
||||
signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests)
|
||||
require.NoError(t, err)
|
||||
spec, err := oci.ParseImageSpec(oci.LocalPrefix + outputLayout)
|
||||
require.NoError(t, err)
|
||||
err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, outputLayout)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
platform *v1.Platform
|
||||
errorStr string
|
||||
}{
|
||||
{name: "nominal", platform: spec.Platform},
|
||||
{name: "invalid platform", platform: invalidPlatform, errorStr: "platform not found in index"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
spec := &oci.ImageSpec{
|
||||
Type: oci.OCI,
|
||||
Identifier: outputLayout,
|
||||
Platform: tc.platform,
|
||||
}
|
||||
resolver, err := policy.CreateImageDetailsResolver(spec)
|
||||
if tc.errorStr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.errorStr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
desc, err := resolver.ImageDescriptor(ctx)
|
||||
require.NoError(t, err)
|
||||
digest := desc.Digest.String()
|
||||
assert.True(t, strings.Contains(digest, "sha256:"))
|
||||
})
|
||||
}
|
||||
}
|
||||
50
pkg/attestation/mock.go
Normal file
50
pkg/attestation/mock.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
)
|
||||
|
||||
type MockResolver struct {
|
||||
Envs []*Envelope
|
||||
}
|
||||
|
||||
func (r MockResolver) Attestations(_ context.Context, _ string) ([]*Envelope, error) {
|
||||
return r.Envs, nil
|
||||
}
|
||||
|
||||
func (r MockResolver) ImageName(_ context.Context) (string, error) {
|
||||
return "library/alpine:latest", nil
|
||||
}
|
||||
|
||||
func (r MockResolver) ImageDescriptor(_ context.Context) (*v1.Descriptor, error) {
|
||||
digest, err := v1.NewHash("sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.Descriptor{
|
||||
Digest: digest,
|
||||
Size: 1234,
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r MockResolver) ImagePlatform(_ context.Context) (*v1.Platform, error) {
|
||||
return oci.ParsePlatform("linux/amd64")
|
||||
}
|
||||
|
||||
type MockRegistryResolver struct {
|
||||
Subject *v1.Descriptor
|
||||
ImageNameStr string
|
||||
*MockResolver
|
||||
}
|
||||
|
||||
func (r *MockRegistryResolver) ImageDescriptor(_ context.Context) (*v1.Descriptor, error) {
|
||||
return r.Subject, nil
|
||||
}
|
||||
|
||||
func (r *MockRegistryResolver) ImageName(_ context.Context) (string, error) {
|
||||
return r.ImageNameStr, nil
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
package oci
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
att "github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
type ReferrersResolver struct {
|
||||
referrersRepo string
|
||||
ImageDetailsResolver
|
||||
oci.ImageDetailsResolver
|
||||
}
|
||||
|
||||
func NewReferrersAttestationResolver(src ImageDetailsResolver, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) {
|
||||
func NewReferrersResolver(src oci.ImageDetailsResolver, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) {
|
||||
res := &ReferrersResolver{
|
||||
ImageDetailsResolver: src,
|
||||
}
|
||||
@@ -35,9 +35,8 @@ func WithReferrersRepo(repo string) func(*ReferrersResolver) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateType string) ([]*attestation.AttestationManifest,
|
||||
error) {
|
||||
dsseMediaType, err := attestation.DSSEMediaType(predicateType)
|
||||
func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateType string) ([]*Manifest, error) {
|
||||
dsseMediaType, err := DSSEMediaType(predicateType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err)
|
||||
}
|
||||
@@ -54,19 +53,16 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy
|
||||
return nil, fmt.Errorf("failed to get descriptor: %w", err)
|
||||
}
|
||||
subjectDigest := desc.Digest.String()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get digest: %w", err)
|
||||
}
|
||||
var referrersSubjectRef name.Digest
|
||||
if r.referrersRepo != "" {
|
||||
referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", r.referrersRepo, subjectDigest))
|
||||
referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", strings.TrimPrefix(r.referrersRepo, oci.RegistryPrefix), subjectDigest))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create referrers reference: %w", err)
|
||||
}
|
||||
} else {
|
||||
referrersSubjectRef = subjectRef.Context().Digest(subjectDigest)
|
||||
}
|
||||
options := WithOptions(ctx, nil)
|
||||
options := oci.WithOptions(ctx, nil)
|
||||
options = append(options, remote.WithFilter("artifactType", dsseMediaType))
|
||||
referrersIndex, err := remote.Referrers(referrersSubjectRef, options...)
|
||||
if err != nil {
|
||||
@@ -76,15 +72,16 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get index manifest: %w", err)
|
||||
}
|
||||
aManifests := make([]*attestation.AttestationManifest, 0)
|
||||
for _, m := range referrersIndexManifest.Manifests {
|
||||
aManifests := make([]*Manifest, 0)
|
||||
for i := range referrersIndexManifest.Manifests {
|
||||
m := referrersIndexManifest.Manifests[i]
|
||||
remoteRef := referrersSubjectRef.Context().Digest(m.Digest.String())
|
||||
options = WithOptions(ctx, nil)
|
||||
options = oci.WithOptions(ctx, nil)
|
||||
attestationImage, err := remote.Image(remoteRef, options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get referred image: %w", err)
|
||||
}
|
||||
layers, err := attestation.GetAttestationsFromImage(attestationImage)
|
||||
layers, err := layersFromImage(attestationImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
|
||||
}
|
||||
@@ -98,7 +95,7 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy
|
||||
if string(mt) != dsseMediaType {
|
||||
return nil, fmt.Errorf("expected layer media type %s, got %s", dsseMediaType, mt)
|
||||
}
|
||||
attest := &attestation.AttestationManifest{
|
||||
attest := &Manifest{
|
||||
SubjectName: imageName,
|
||||
OriginalLayers: layers,
|
||||
OriginalDescriptor: &m,
|
||||
@@ -109,12 +106,12 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy
|
||||
return aManifests, nil
|
||||
}
|
||||
|
||||
func (r *ReferrersResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
||||
func (r *ReferrersResolver) Attestations(ctx context.Context, predicateType string) ([]*Envelope, error) {
|
||||
manifests, err := r.resolveAttestations(ctx, predicateType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve attestations: %w", err)
|
||||
}
|
||||
var envs []*att.Envelope
|
||||
var envs []*Envelope
|
||||
for _, attest := range manifests {
|
||||
es, err := ExtractEnvelopes(attest, predicateType)
|
||||
if err != nil {
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"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"
|
||||
@@ -22,7 +21,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
|
||||
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
|
||||
LocalPolicy = filepath.Join("..", "..", "test", "testdata", "local-policy")
|
||||
@@ -34,7 +32,6 @@ var (
|
||||
|
||||
func TestAttestationReferenceTypes(t *testing.T) {
|
||||
ctx, signer := test.Setup(t)
|
||||
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
|
||||
platforms := []string{"linux/amd64", "linux/arm64"}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
@@ -57,7 +54,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
|
||||
{
|
||||
name: "attached attestations, referrers repo (mismatched args)",
|
||||
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||
expectFailure: true, //mismatched args
|
||||
expectFailure: true, // mismatched args
|
||||
attestationSource: config.AttestationStyleAttached,
|
||||
referrersRepo: "referrers",
|
||||
},
|
||||
@@ -93,7 +90,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
|
||||
opts := &attestation.SigningOptions{
|
||||
SkipTL: true,
|
||||
}
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
|
||||
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||
@@ -112,14 +109,16 @@ func TestAttestationReferenceTypes(t *testing.T) {
|
||||
|
||||
// push subject image so that it can be resolved
|
||||
require.NoError(t, err)
|
||||
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
|
||||
err = oci.PushIndexToRegistry(attIdx.Index, indexName)
|
||||
require.NoError(t, err)
|
||||
|
||||
// upload referrers
|
||||
output, err := oci.ParseImageSpec(outputRepo)
|
||||
require.NoError(t, err)
|
||||
for _, attIdx := range signedManifests {
|
||||
err = mirror.SaveReferrers(attIdx, []*oci.ImageSpec{output})
|
||||
images, err := attIdx.BuildReferringArtifacts()
|
||||
require.NoError(t, err)
|
||||
err = oci.SaveImagesNoTag(images, []*oci.ImageSpec{output})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -137,8 +136,9 @@ func TestAttestationReferenceTypes(t *testing.T) {
|
||||
ref = fmt.Sprintf("%s/repo@%s", u.Host, idxDigest.String())
|
||||
}
|
||||
|
||||
policyOpts := &policy.PolicyOptions{
|
||||
policyOpts := &policy.Options{
|
||||
LocalPolicyDir: LocalPolicy,
|
||||
DisableTUF: true,
|
||||
}
|
||||
|
||||
if tc.referrersRepo != "" {
|
||||
@@ -201,22 +201,22 @@ func TestReferencesInDifferentRepo(t *testing.T) {
|
||||
} {
|
||||
server := tc.server
|
||||
defer server.Close()
|
||||
serverUrl, err := url.Parse(server.URL)
|
||||
serverURL, err := url.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
refServer := tc.refServer
|
||||
defer refServer.Close()
|
||||
refServerUrl, err := url.Parse(refServer.URL)
|
||||
refServerURL, err := url.Parse(refServer.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := &attestation.SigningOptions{
|
||||
SkipTL: true,
|
||||
}
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
|
||||
indexName := fmt.Sprintf("%s/%s:latest", serverUrl.Host, repoName)
|
||||
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
|
||||
indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName)
|
||||
err = oci.PushIndexToRegistry(attIdx.Index, indexName)
|
||||
require.NoError(t, err)
|
||||
|
||||
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
||||
@@ -225,24 +225,24 @@ func TestReferencesInDifferentRepo(t *testing.T) {
|
||||
// push signed attestation image to the ref server
|
||||
for _, signedManifest := range signedManifests {
|
||||
// push references using subject-digest.att convention
|
||||
image, err := signedManifest.BuildAttestationImage()
|
||||
image, err := signedManifest.BuildImage()
|
||||
require.NoError(t, err)
|
||||
err = mirror.PushImageToRegistry(image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerUrl.Host, repoName))
|
||||
err = oci.PushImageToRegistry(image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerURL.Host, repoName))
|
||||
require.NoError(t, err)
|
||||
|
||||
refServer := tc.refServer
|
||||
defer refServer.Close()
|
||||
refServerUrl, err := url.Parse(refServer.URL)
|
||||
refServerURL, err := url.Parse(refServer.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := &attestation.SigningOptions{
|
||||
SkipTL: true,
|
||||
}
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
|
||||
indexName := fmt.Sprintf("%s/%s:latest", serverUrl.Host, repoName)
|
||||
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
|
||||
indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName)
|
||||
err = oci.PushIndexToRegistry(attIdx.Index, indexName)
|
||||
require.NoError(t, err)
|
||||
|
||||
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
||||
@@ -254,21 +254,22 @@ func TestReferencesInDifferentRepo(t *testing.T) {
|
||||
imgs, err := mf.BuildReferringArtifacts()
|
||||
require.NoError(t, err)
|
||||
for _, img := range imgs {
|
||||
err = mirror.PushImageToRegistry(img, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerUrl.Host, repoName))
|
||||
err = oci.PushImageToRegistry(img, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerURL.Host, repoName))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
mfs2, err := attIdx.Index.IndexManifest()
|
||||
require.NoError(t, err)
|
||||
for _, mf := range mfs2.Manifests {
|
||||
//skip signed/unsigned attestations
|
||||
// skip signed/unsigned attestations
|
||||
if mf.Annotations[attestation.DockerReferenceType] == attestation.AttestationManifestType {
|
||||
continue
|
||||
}
|
||||
// can evaluate policy using referrers in a different repo
|
||||
referencedImage := fmt.Sprintf("%s@%s", indexName, mf.Digest.String())
|
||||
policyOpts := &policy.PolicyOptions{
|
||||
policyOpts := &policy.Options{
|
||||
LocalPolicyDir: PassPolicyDir,
|
||||
DisableTUF: true,
|
||||
}
|
||||
src, err := oci.ParseImageSpec(referencedImage)
|
||||
require.NoError(t, err)
|
||||
@@ -285,7 +286,7 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) {
|
||||
server := httptest.NewServer(registry.New())
|
||||
|
||||
defer server.Close()
|
||||
serverUrl, err := url.Parse(server.URL)
|
||||
serverURL, err := url.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
repoName := "repo"
|
||||
@@ -293,11 +294,11 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) {
|
||||
opts := &attestation.SigningOptions{
|
||||
SkipTL: true,
|
||||
}
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
|
||||
indexName := fmt.Sprintf("%s/%s:latest", serverUrl.Host, repoName)
|
||||
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
|
||||
indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName)
|
||||
err = oci.PushIndexToRegistry(attIdx.Index, indexName)
|
||||
require.NoError(t, err)
|
||||
|
||||
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
||||
@@ -308,12 +309,12 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) {
|
||||
imgs, err := mf.BuildReferringArtifacts()
|
||||
require.NoError(t, err)
|
||||
for _, img := range imgs {
|
||||
err = mirror.PushImageToRegistry(img, fmt.Sprintf("%s/%s:tag-does-not-matter", serverUrl.Host, repoName))
|
||||
err = oci.PushImageToRegistry(img, fmt.Sprintf("%s/%s:tag-does-not-matter", serverURL.Host, repoName))
|
||||
require.NoError(t, err)
|
||||
mf, err := img.Manifest()
|
||||
require.NoError(t, err)
|
||||
subject := mf.Subject
|
||||
subjectRef, err := name.ParseReference(fmt.Sprintf("%s/%s:sha256-%s", serverUrl.Host, repoName, subject.Digest.Hex))
|
||||
subjectRef, err := name.ParseReference(fmt.Sprintf("%s/%s:sha256-%s", serverURL.Host, repoName, subject.Digest.Hex))
|
||||
require.NoError(t, err)
|
||||
idx, err := remote.Index(subjectRef)
|
||||
require.NoError(t, err)
|
||||
|
||||
101
pkg/attestation/registry.go
Normal file
101
pkg/attestation/registry.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
type RegistryResolver struct {
|
||||
*oci.RegistryImageDetailsResolver
|
||||
*Manifest
|
||||
}
|
||||
|
||||
func NewRegistryResolver(src *oci.RegistryImageDetailsResolver) (*RegistryResolver, error) {
|
||||
return &RegistryResolver{
|
||||
RegistryImageDetailsResolver: src,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *RegistryResolver) Attestations(ctx context.Context, predicateType string) ([]*Envelope, error) {
|
||||
if r.Manifest == nil {
|
||||
attest, err := FetchManifest(ctx, r.Identifier, r.ImageSpec.Platform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Manifest = attest
|
||||
}
|
||||
return ExtractEnvelopes(r.Manifest, predicateType)
|
||||
}
|
||||
|
||||
func attestationDigestForImage(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) {
|
||||
for i := range ix.Manifests {
|
||||
m := &ix.Manifests[i]
|
||||
if v, ok := m.Annotations[DockerReferenceType]; ok && v == attestType {
|
||||
if d, ok := m.Annotations[DockerReferenceDigest]; ok && d == imageDigest {
|
||||
return m.Digest.String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no attestation found for image %s", imageDigest)
|
||||
}
|
||||
|
||||
func FetchManifest(ctx context.Context, image string, platform *v1.Platform) (*Manifest, error) {
|
||||
// we want to get to the image index, so ignoring platform for now
|
||||
options := oci.WithOptions(ctx, nil)
|
||||
ref, err := name.ParseReference(image)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse reference: %w", err)
|
||||
}
|
||||
index, err := remote.Index(ref, options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get index: %w", err)
|
||||
}
|
||||
indexManifest, err := index.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get index manifest: %w", err)
|
||||
}
|
||||
subjectDescriptor, err := oci.ImageDescriptor(indexManifest, platform)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to obtain image for platform: %w", err)
|
||||
}
|
||||
|
||||
digest := subjectDescriptor.Digest.String()
|
||||
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), digest))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
||||
}
|
||||
|
||||
attestationDigest, err := attestationDigestForImage(indexManifest, digest, "attestation-manifest")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to obtain attestation for image: %w", err)
|
||||
}
|
||||
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), attestationDigest))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
||||
}
|
||||
remoteDescriptor, err := remote.Get(ref, options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestation: %w", err)
|
||||
}
|
||||
attestationImage, err := remoteDescriptor.Image()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestation image: %w", err)
|
||||
}
|
||||
|
||||
layers, err := layersFromImage(attestationImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
|
||||
}
|
||||
attest := &Manifest{
|
||||
OriginalLayers: layers,
|
||||
OriginalDescriptor: &remoteDescriptor.Descriptor,
|
||||
SubjectName: image,
|
||||
SubjectDescriptor: subjectDescriptor,
|
||||
}
|
||||
return attest, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package oci_test
|
||||
package attestation_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"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"
|
||||
@@ -26,7 +25,7 @@ func TestRegistry(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := &attestation.SigningOptions{}
|
||||
attIdx, err := oci.IndexFromPath(oci.UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
||||
require.NoError(t, err)
|
||||
@@ -36,7 +35,7 @@ func TestRegistry(t *testing.T) {
|
||||
|
||||
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||
require.NoError(t, err)
|
||||
err = mirror.PushIndexToRegistry(signedIndex, indexName)
|
||||
err = oci.PushIndexToRegistry(signedIndex, indexName)
|
||||
require.NoError(t, err)
|
||||
|
||||
spec, err := oci.ParseImageSpec(indexName)
|
||||
12
pkg/attestation/resolver.go
Normal file
12
pkg/attestation/resolver.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
)
|
||||
|
||||
type Resolver interface {
|
||||
oci.ImageDetailsResolver
|
||||
Attestations(ctx context.Context, mediaType string) ([]*Envelope, error)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||
)
|
||||
|
||||
// SignDSSE signs a payload with a given signer and uploads the signature to the transparency log
|
||||
// SignDSSE signs a payload with a given signer and uploads the signature to the transparency log.
|
||||
func SignDSSE(ctx context.Context, payload []byte, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) {
|
||||
payloadType := intoto.PayloadType
|
||||
env := new(Envelope)
|
||||
@@ -28,13 +28,13 @@ func SignDSSE(ctx context.Context, payload []byte, signer dsse.SignerVerifier, o
|
||||
}
|
||||
|
||||
// get Key ID from signer
|
||||
keyId, err := signer.KeyID()
|
||||
keyID, err := signer.KeyID()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting public key ID: %w", err)
|
||||
}
|
||||
|
||||
dsseSig := Signature{
|
||||
KeyID: keyId,
|
||||
dsseSig := &Signature{
|
||||
KeyID: keyID,
|
||||
Sig: base64Encoding.EncodeToString(sig),
|
||||
}
|
||||
if !opts.SkipTL {
|
||||
@@ -42,22 +42,22 @@ func SignDSSE(ctx context.Context, payload []byte, signer dsse.SignerVerifier, o
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to log to rekor: %w", err)
|
||||
}
|
||||
dsseSig.Extension = *ext
|
||||
dsseSig.Extension = ext
|
||||
}
|
||||
// add signature to dsse envelope
|
||||
env.Signatures = []Signature{dsseSig}
|
||||
env.Signatures = []*Signature{dsseSig}
|
||||
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// returns a new envelope with the transparency log entry added to the signature extension
|
||||
// returns a new envelope with the transparency log entry added to the signature extension.
|
||||
func logSignature(ctx context.Context, t tlog.TL, sig *[]byte, encPayload *[]byte, signer dsse.SignerVerifier) (*Extension, error) {
|
||||
// get Key ID from signer
|
||||
keyId, err := signer.KeyID()
|
||||
keyID, err := signer.KeyID()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting public key ID: %w", err)
|
||||
}
|
||||
entry, err := t.UploadLogEntry(ctx, keyId, *encPayload, *sig, signer)
|
||||
entry, err := t.UploadLogEntry(ctx, keyID, *encPayload, *sig, signer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error uploading TL entry: %w", err)
|
||||
}
|
||||
@@ -66,10 +66,10 @@ func logSignature(ctx context.Context, t tlog.TL, sig *[]byte, encPayload *[]byt
|
||||
return nil, fmt.Errorf("error unmarshaling tl entry: %w", err)
|
||||
}
|
||||
return &Extension{
|
||||
Kind: DockerDsseExtKind,
|
||||
Ext: DockerDsseExtension{
|
||||
Tl: DockerTlExtension{
|
||||
Kind: RekorTlExtKind,
|
||||
Kind: DockerDSSEExtKind,
|
||||
Ext: &DockerDSSEExtension{
|
||||
TL: &DockerTLExtension{
|
||||
Kind: RekorTLExtKind,
|
||||
Data: entryObj, // transparency log entry metadata
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,12 +6,19 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/signerverifier"
|
||||
"github.com/google/go-containerregistry/pkg/registry"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/static"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -44,20 +51,20 @@ func TestSignVerifyAttestation(t *testing.T) {
|
||||
// signer.Public() used here for test purposes
|
||||
ecPub, ok := signer.Public().(*ecdsa.PublicKey)
|
||||
assert.True(t, ok)
|
||||
pem, err := signerverifier.ToPEM(ecPub)
|
||||
pem, err := signerverifier.ConvertToPEM(ecPub)
|
||||
assert.NoError(t, err)
|
||||
keyId, err := signerverifier.KeyID(ecPub)
|
||||
keyID, err := signerverifier.KeyID(ecPub)
|
||||
assert.NoError(t, err)
|
||||
|
||||
badKeyPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
badKey := &badKeyPriv.PublicKey
|
||||
badPEM, err := signerverifier.ToPEM(badKey)
|
||||
badPEM, err := signerverifier.ConvertToPEM(badKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
keyId string
|
||||
keyID string
|
||||
pem []byte
|
||||
distrust bool
|
||||
from time.Time
|
||||
@@ -67,7 +74,7 @@ func TestSignVerifyAttestation(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "all OK",
|
||||
keyId: keyId,
|
||||
keyID: keyID,
|
||||
pem: pem,
|
||||
distrust: false,
|
||||
from: time.Time{},
|
||||
@@ -77,17 +84,17 @@ func TestSignVerifyAttestation(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "key not found",
|
||||
keyId: "someotherkey",
|
||||
keyID: "someotherkey",
|
||||
pem: pem,
|
||||
distrust: false,
|
||||
from: time.Time{},
|
||||
to: nil,
|
||||
status: "active",
|
||||
expectedError: fmt.Sprintf("key not found: %s", keyId),
|
||||
expectedError: fmt.Sprintf("key not found: %s", keyID),
|
||||
},
|
||||
{
|
||||
name: "key distrusted",
|
||||
keyId: keyId,
|
||||
keyID: keyID,
|
||||
pem: pem,
|
||||
distrust: true,
|
||||
from: time.Time{},
|
||||
@@ -97,7 +104,7 @@ func TestSignVerifyAttestation(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "key not yet valid",
|
||||
keyId: keyId,
|
||||
keyID: keyID,
|
||||
pem: pem,
|
||||
distrust: false,
|
||||
from: time.Now().Add(time.Hour),
|
||||
@@ -107,7 +114,7 @@ func TestSignVerifyAttestation(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "key already revoked",
|
||||
keyId: keyId,
|
||||
keyID: keyID,
|
||||
pem: pem,
|
||||
distrust: false,
|
||||
from: time.Time{},
|
||||
@@ -117,7 +124,7 @@ func TestSignVerifyAttestation(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "bad key",
|
||||
keyId: keyId,
|
||||
keyID: keyID,
|
||||
pem: badPEM,
|
||||
distrust: false,
|
||||
from: time.Time{},
|
||||
@@ -129,8 +136,8 @@ func TestSignVerifyAttestation(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
keyMeta := attestation.KeyMetadata{
|
||||
ID: tc.keyId,
|
||||
keyMeta := &attestation.KeyMetadata{
|
||||
ID: tc.keyID,
|
||||
PEM: string(tc.pem),
|
||||
Distrust: tc.distrust,
|
||||
From: tc.from,
|
||||
@@ -149,3 +156,146 @@ func TestSignVerifyAttestation(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddSignedLayerAnnotations(t *testing.T) {
|
||||
ctx, signer := test.Setup(t)
|
||||
testCases := []struct {
|
||||
name string
|
||||
replace bool
|
||||
}{
|
||||
{"replaced", true},
|
||||
{"not replaced", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
data := []byte("signed")
|
||||
testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType))
|
||||
mediaType := types.OCIManifestSchema1
|
||||
opts := &attestation.SigningOptions{}
|
||||
originalLayer := &attestation.Layer{
|
||||
Layer: testLayer,
|
||||
Statement: &intoto.Statement{
|
||||
StatementHeader: intoto.StatementHeader{
|
||||
PredicateType: attestation.VSAPredicateType,
|
||||
},
|
||||
},
|
||||
Annotations: map[string]string{"test": "test"},
|
||||
}
|
||||
|
||||
manifest := &attestation.Manifest{
|
||||
OriginalDescriptor: &v1.Descriptor{
|
||||
MediaType: mediaType,
|
||||
},
|
||||
OriginalLayers: []*attestation.Layer{
|
||||
originalLayer,
|
||||
},
|
||||
SubjectDescriptor: &v1.Descriptor{},
|
||||
}
|
||||
err := manifest.Add(ctx, signer, originalLayer.Statement, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
newImg, err := manifest.BuildImage(attestation.WithReplacedLayers(tc.replace))
|
||||
require.NoError(t, err)
|
||||
mf, _ := newImg.RawManifest()
|
||||
type Annotations struct {
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
}
|
||||
type Layers struct {
|
||||
Layers []Annotations `json:"layers"`
|
||||
}
|
||||
l := &Layers{}
|
||||
err = json.Unmarshal(mf, l)
|
||||
require.NoError(t, err)
|
||||
_, ok := l.Layers[0].Annotations["test"]
|
||||
assert.Truef(t, ok, "missing annotations")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleStatementSigning(t *testing.T) {
|
||||
ctx, signer := test.Setup(t)
|
||||
empty := types.MediaType("application/vnd.oci.empty.v1+json")
|
||||
testCases := []struct {
|
||||
name string
|
||||
replace bool
|
||||
}{
|
||||
{"replaced", true},
|
||||
{"not replaced", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
opts := &attestation.SigningOptions{}
|
||||
statement := &intoto.Statement{
|
||||
StatementHeader: intoto.StatementHeader{
|
||||
PredicateType: attestation.VSAPredicateType,
|
||||
},
|
||||
}
|
||||
statement2 := &intoto.Statement{
|
||||
StatementHeader: intoto.StatementHeader{
|
||||
PredicateType: attestation.VSAPredicateType,
|
||||
},
|
||||
}
|
||||
digest, err := v1.NewHash("sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620")
|
||||
require.NoError(t, err)
|
||||
subject := &v1.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Digest: digest,
|
||||
}
|
||||
manifest, err := attestation.NewManifest(subject)
|
||||
require.NoError(t, err)
|
||||
err = manifest.Add(ctx, signer, statement, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = manifest.Add(ctx, signer, statement2, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
// fake that the manfifest was loaded from a real image
|
||||
manifest.OriginalLayers = manifest.SignedLayers
|
||||
envelopes, err := attestation.ExtractEnvelopes(manifest, attestation.VSAPredicateType)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, envelopes, 2)
|
||||
|
||||
newImg, err := manifest.BuildImage(attestation.WithReplacedLayers(tc.replace))
|
||||
require.NoError(t, err)
|
||||
layers, err := newImg.Layers()
|
||||
require.NoError(t, err)
|
||||
if tc.replace {
|
||||
assert.Len(t, layers, 2)
|
||||
} else {
|
||||
assert.Len(t, layers, 4)
|
||||
}
|
||||
|
||||
newImgs, err := manifest.BuildReferringArtifacts()
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, newImgs, 2)
|
||||
for _, img := range newImgs {
|
||||
mf, err := img.Manifest()
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, mf.ArtifactType, "application/vnd.in-toto")
|
||||
assert.Contains(t, mf.ArtifactType, "+dsse")
|
||||
assert.Equal(t, subject.MediaType, mf.MediaType)
|
||||
assert.Equal(t, empty, mf.Config.MediaType)
|
||||
assert.Equal(t, int64(2), mf.Config.Size)
|
||||
assert.Equal(t, "{}", string(mf.Config.Data))
|
||||
layers, err := img.Layers()
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, layers, 1)
|
||||
}
|
||||
server := httptest.NewServer(registry.New(registry.WithReferrersSupport(true)))
|
||||
defer server.Close()
|
||||
|
||||
u, err := url.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||
output, err := oci.ParseImageSpecs(indexName)
|
||||
require.NoError(t, err)
|
||||
artifacts, err := manifest.BuildReferringArtifacts()
|
||||
require.NoError(t, err)
|
||||
err = oci.SaveImagesNoTag(artifacts, output)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ const (
|
||||
AttestationManifestType = "attestation-manifest"
|
||||
InTotoPredicateType = "in-toto.io/predicate-type"
|
||||
DockerReferenceDigest = "vnd.docker.reference.digest"
|
||||
DockerDsseExtKind = "application/vnd.docker.attestation-verification.v1+json"
|
||||
RekorTlExtKind = "Rekor"
|
||||
DockerDSSEExtKind = "application/vnd.docker.attestation-verification.v1+json"
|
||||
RekorTLExtKind = "Rekor"
|
||||
OCIDescriptorDSSEMediaType = ociv1.MediaTypeDescriptor + "+dsse"
|
||||
InTotoReferenceLifecycleStage = "vnd.docker.lifecycle-stage"
|
||||
LifecycleStageExperimental = "experimental"
|
||||
@@ -24,58 +24,64 @@ const (
|
||||
|
||||
var base64Encoding = base64.StdEncoding.Strict()
|
||||
|
||||
type AttestationLayer struct {
|
||||
type Layer struct {
|
||||
Statement *intoto.Statement
|
||||
Layer v1.Layer
|
||||
Annotations map[string]string
|
||||
}
|
||||
|
||||
type AttestationManifest struct {
|
||||
type Manifest struct {
|
||||
OriginalDescriptor *v1.Descriptor
|
||||
OriginalLayers []*AttestationLayer
|
||||
OriginalLayers []*Layer
|
||||
|
||||
// accumulated during signing
|
||||
SignedLayers []*AttestationLayer
|
||||
// details of subect image
|
||||
SignedLayers []*Layer
|
||||
// details of subject image
|
||||
SubjectName string
|
||||
SubjectDescriptor *v1.Descriptor
|
||||
}
|
||||
|
||||
type AttestationManifestImageOptions struct {
|
||||
type ManifestImageOptions struct {
|
||||
// how to output the image
|
||||
skipSubject bool
|
||||
replaceLayers bool
|
||||
laxReferrers bool
|
||||
}
|
||||
|
||||
// the following types are needed until https://github.com/secure-systems-lab/dsse/pull/61 is merged
|
||||
// the following types are needed until https://github.com/secure-systems-lab/dsse/pull/61 is merged.
|
||||
type Envelope struct {
|
||||
PayloadType string `json:"payloadType"`
|
||||
Payload string `json:"payload"`
|
||||
Signatures []Signature `json:"signatures"`
|
||||
PayloadType string `json:"payloadType"`
|
||||
Payload string `json:"payload"`
|
||||
Signatures []*Signature `json:"signatures"`
|
||||
}
|
||||
type Signature struct {
|
||||
KeyID string `json:"keyid"`
|
||||
Sig string `json:"sig"`
|
||||
Extension Extension `json:"extension,omitempty"`
|
||||
KeyID string `json:"keyid"`
|
||||
Sig string `json:"sig"`
|
||||
Extension *Extension `json:"extension,omitempty"`
|
||||
}
|
||||
type Extension struct {
|
||||
Kind string `json:"kind"`
|
||||
Ext DockerDsseExtension `json:"ext"`
|
||||
Kind string `json:"kind"`
|
||||
Ext *DockerDSSEExtension `json:"ext"`
|
||||
}
|
||||
|
||||
type DockerDsseExtension struct {
|
||||
Tl DockerTlExtension `json:"tl"`
|
||||
type AnnotatedStatement struct {
|
||||
OCIDescriptor *v1.Descriptor
|
||||
InTotoStatement *intoto.Statement
|
||||
Annotations map[string]string
|
||||
}
|
||||
|
||||
type DockerTlExtension struct {
|
||||
type DockerDSSEExtension struct {
|
||||
TL *DockerTLExtension `json:"tl"`
|
||||
}
|
||||
|
||||
type DockerTLExtension struct {
|
||||
Kind string `json:"kind"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
type VerifyOptions struct {
|
||||
Keys []KeyMetadata `json:"keys"`
|
||||
SkipTL bool `json:"skip_tl"`
|
||||
Keys []*KeyMetadata `json:"keys"`
|
||||
SkipTL bool `json:"skip_tl"`
|
||||
}
|
||||
|
||||
type SigningOptions struct {
|
||||
@@ -83,6 +89,12 @@ type SigningOptions struct {
|
||||
SkipTL bool
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
NoReferrers bool
|
||||
Attach bool
|
||||
ReferrersRepo string
|
||||
}
|
||||
|
||||
func DSSEMediaType(predicateType string) (string, error) {
|
||||
var predicateName string
|
||||
switch predicateType {
|
||||
|
||||
@@ -27,8 +27,10 @@ type KeyMetadata struct {
|
||||
Distrust bool `json:"distrust,omitempty"`
|
||||
}
|
||||
|
||||
type Keys []KeyMetadata
|
||||
type KeysMap map[string]KeyMetadata
|
||||
type (
|
||||
Keys []*KeyMetadata
|
||||
KeysMap map[string]*KeyMetadata
|
||||
)
|
||||
|
||||
func VerifyDSSE(ctx context.Context, env *Envelope, opts *VerifyOptions) ([]byte, error) {
|
||||
// enforce payload type
|
||||
@@ -58,8 +60,8 @@ func VerifyDSSE(ctx context.Context, env *Envelope, opts *VerifyOptions) ([]byte
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func verifySignature(ctx context.Context, sig Signature, payload []byte, opts *VerifyOptions) error {
|
||||
keys := make(map[string]KeyMetadata, len(opts.Keys))
|
||||
func verifySignature(ctx context.Context, sig *Signature, payload []byte, opts *VerifyOptions) error {
|
||||
keys := make(map[string]*KeyMetadata, len(opts.Keys))
|
||||
for _, key := range opts.Keys {
|
||||
keys[key.ID] = key
|
||||
}
|
||||
@@ -72,26 +74,25 @@ func verifySignature(ctx context.Context, sig Signature, payload []byte, opts *V
|
||||
return fmt.Errorf("key %s is distrusted", keyMeta.ID)
|
||||
}
|
||||
// TODO: this is unmarshalling with MarshalPKIXPublicKey only for us to marshal it again
|
||||
publicKey, err := signerverifier.Parse([]byte(keyMeta.PEM))
|
||||
publicKey, err := signerverifier.ParsePublicKey([]byte(keyMeta.PEM))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse public key: %w", err)
|
||||
}
|
||||
|
||||
if !opts.SkipTL {
|
||||
t := tlog.GetTL(ctx)
|
||||
|
||||
if sig.Extension.Kind == "" {
|
||||
return fmt.Errorf("error missing signature extension kind")
|
||||
if sig.Extension == nil || sig.Extension.Kind == "" {
|
||||
return fmt.Errorf("error missing signature extension")
|
||||
}
|
||||
if sig.Extension.Kind != DockerDsseExtKind {
|
||||
if sig.Extension.Kind != DockerDSSEExtKind {
|
||||
return fmt.Errorf("error unsupported signature extension kind: %s", sig.Extension.Kind)
|
||||
}
|
||||
|
||||
// verify TL entry
|
||||
if sig.Extension.Ext.Tl.Kind != RekorTlExtKind {
|
||||
return fmt.Errorf("error unsupported TL extension kind: %s", sig.Extension.Ext.Tl.Kind)
|
||||
if sig.Extension.Ext.TL.Kind != RekorTLExtKind {
|
||||
return fmt.Errorf("error unsupported TL extension kind: %s", sig.Extension.Ext.TL.Kind)
|
||||
}
|
||||
entry := sig.Extension.Ext.Tl.Data
|
||||
entry := sig.Extension.Ext.TL.Data
|
||||
entryBytes, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal TL entry: %w", err)
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestVerifyUnsignedAttestation(t *testing.T) {
|
||||
payload := []byte("payload")
|
||||
env := &attestation.Envelope{
|
||||
// no signatures
|
||||
Signatures: []attestation.Signature{},
|
||||
Signatures: []*attestation.Signature{},
|
||||
Payload: base64.StdEncoding.EncodeToString(payload),
|
||||
PayloadType: intoto.PayloadType,
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ const (
|
||||
type VSAPredicate struct {
|
||||
Verifier VSAVerifier `json:"verifier"`
|
||||
TimeVerified string `json:"timeVerified"`
|
||||
ResourceUri string `json:"resourceUri"`
|
||||
ResourceURI string `json:"resourceUri"`
|
||||
Policy VSAPolicy `json:"policy"`
|
||||
InputAttestations []VSAInputAttestation `json:"inputAttestations"`
|
||||
InputAttestations []VSAInputAttestation `json:"inputAttestations,omitempty"`
|
||||
VerificationResult string `json:"verificationResult"`
|
||||
VerifiedLevels []string `json:"verifiedLevels"`
|
||||
}
|
||||
@@ -26,7 +26,9 @@ type VSAVerifier struct {
|
||||
}
|
||||
|
||||
type VSAPolicy struct {
|
||||
URI string `json:"uri"`
|
||||
URI string `json:"uri,omitempty"`
|
||||
Digest map[string]string `json:"digest"`
|
||||
DownloadLocation string `json:"downloadLocation,omitempty"`
|
||||
}
|
||||
|
||||
type VSAInputAttestation struct {
|
||||
@@ -35,7 +37,7 @@ type VSAInputAttestation struct {
|
||||
}
|
||||
|
||||
func ToVSAResourceURI(sub intoto.Subject) (string, error) {
|
||||
//parse purl
|
||||
// parse purl
|
||||
purl, err := packageurl.FromString(sub.Name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse package url: %w", err)
|
||||
|
||||
@@ -31,18 +31,18 @@ func LoadLocalMappings(configDir string) (*PolicyMappings, error) {
|
||||
return expandMappingFile(mappings)
|
||||
}
|
||||
|
||||
func LoadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMappings, error) {
|
||||
func LoadTUFMappings(tufClient tuf.Downloader, 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))
|
||||
file, err := tufClient.DownloadTarget(filename, filepath.Join(localTargetsDir, filename))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download policy mapping file %s: %w", filename, err)
|
||||
}
|
||||
mappings := &policyMappingsFile{}
|
||||
|
||||
err = yaml.Unmarshal(fileContents, mappings)
|
||||
err = yaml.Unmarshal(file.Data, mappings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", filename, err)
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func LoadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMa
|
||||
func expandMappingFile(mappingFile *policyMappingsFile) (*PolicyMappings, error) {
|
||||
policies := make(map[string]*PolicyMapping)
|
||||
for _, policy := range mappingFile.Policies {
|
||||
policies[policy.Id] = policy
|
||||
policies[policy.ID] = policy
|
||||
}
|
||||
|
||||
var rules []*PolicyRule
|
||||
@@ -63,7 +63,7 @@ func expandMappingFile(mappingFile *policyMappingsFile) (*PolicyMappings, error)
|
||||
}
|
||||
rules = append(rules, &PolicyRule{
|
||||
Pattern: r,
|
||||
PolicyId: rule.PolicyId,
|
||||
PolicyID: rule.PolicyID,
|
||||
Replacement: rule.Replacement,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ type policyMappingsFile struct {
|
||||
|
||||
type policyRuleFile struct {
|
||||
Pattern string `json:"pattern"`
|
||||
PolicyId string `json:"policy-id"`
|
||||
PolicyID string `json:"policy-id"`
|
||||
Replacement string `json:"rewrite"`
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ const (
|
||||
)
|
||||
|
||||
type PolicyMapping struct {
|
||||
Id string `json:"id"`
|
||||
ID string `json:"id"`
|
||||
Description string `json:"description"`
|
||||
Files []PolicyMappingFile `json:"files"`
|
||||
Attestations *AttestationConfig `json:"attestations"`
|
||||
@@ -49,6 +49,6 @@ type PolicyMappingFile struct {
|
||||
|
||||
type PolicyRule struct {
|
||||
Pattern *regexp.Regexp
|
||||
PolicyId string
|
||||
PolicyID string
|
||||
Replacement string
|
||||
}
|
||||
|
||||
2
pkg/mirror/README.md
Normal file
2
pkg/mirror/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## mirror
|
||||
This package contains components to mirror TUF metadata and targets to OCI.
|
||||
@@ -6,20 +6,20 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/attest/internal/embed"
|
||||
"github.com/docker/attest/pkg/mirror"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"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
|
||||
delegatedMetadata []*mirror.Image
|
||||
targets []*mirror.Image
|
||||
delegatedTargets []*mirror.Index
|
||||
}
|
||||
|
||||
func ExampleNewTufMirror() {
|
||||
func ExampleNewTUFMirror() {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -29,7 +29,7 @@ func ExampleNewTufMirror() {
|
||||
// configure TUF mirror
|
||||
metadataURI := "https://docker.github.io/tuf-staging/metadata"
|
||||
targetsURI := "https://docker.github.io/tuf-staging/targets"
|
||||
m, err := mirror.NewTufMirror(embed.RootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker())
|
||||
m, err := mirror.NewTUFMirror(tuf.DockerTUFRootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func ExampleNewTufMirror() {
|
||||
}
|
||||
|
||||
// create targets manifest
|
||||
targets, err := m.GetTufTargetMirrors()
|
||||
targets, err := m.GetTUFTargetMirrors()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -80,7 +80,7 @@ func ExampleNewTufMirror() {
|
||||
func mirrorToRegistry(o *TufMirrorOutput) error {
|
||||
// push metadata to registry
|
||||
metadataRepo := "registry-1.docker.io/docker/tuf-metadata:latest"
|
||||
err := mirror.PushImageToRegistry(o.metadata, metadataRepo)
|
||||
err := oci.PushImageToRegistry(o.metadata, metadataRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func mirrorToRegistry(o *TufMirrorOutput) error {
|
||||
return fmt.Errorf("failed to get repo without tag: %s", metadataRepo)
|
||||
}
|
||||
imageName := fmt.Sprintf("%s:%s", repo, metadata.Tag)
|
||||
err = mirror.PushImageToRegistry(metadata.Image, imageName)
|
||||
err = oci.PushImageToRegistry(metadata.Image, imageName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -101,7 +101,7 @@ func mirrorToRegistry(o *TufMirrorOutput) error {
|
||||
targetsRepo := "registry-1.docker.io/docker/tuf-targets"
|
||||
for _, target := range o.targets {
|
||||
imageName := fmt.Sprintf("%s:%s", targetsRepo, target.Tag)
|
||||
err = mirror.PushImageToRegistry(target.Image, imageName)
|
||||
err = oci.PushImageToRegistry(target.Image, imageName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -109,7 +109,7 @@ func mirrorToRegistry(o *TufMirrorOutput) error {
|
||||
// push delegated targets to registry
|
||||
for _, target := range o.delegatedTargets {
|
||||
imageName := fmt.Sprintf("%s:%s", targetsRepo, target.Tag)
|
||||
err = mirror.PushIndexToRegistry(target.Index, imageName)
|
||||
err = oci.PushIndexToRegistry(target.Index, imageName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -119,14 +119,14 @@ func mirrorToRegistry(o *TufMirrorOutput) error {
|
||||
|
||||
func mirrorToLocal(o *TufMirrorOutput, outputPath string) error {
|
||||
// output metadata to local directory
|
||||
err := mirror.SaveImageAsOCILayout(o.metadata, outputPath)
|
||||
err := oci.SaveImageAsOCILayout(o.metadata, outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// output delegated metadata to local directory
|
||||
for _, metadata := range o.delegatedMetadata {
|
||||
path := filepath.Join(outputPath, metadata.Tag)
|
||||
err = mirror.SaveImageAsOCILayout(metadata.Image, path)
|
||||
err = oci.SaveImageAsOCILayout(metadata.Image, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -135,7 +135,7 @@ func mirrorToLocal(o *TufMirrorOutput, outputPath string) error {
|
||||
// output top-level targets to local directory
|
||||
for _, target := range o.targets {
|
||||
path := filepath.Join(outputPath, target.Tag)
|
||||
err = mirror.SaveImageAsOCILayout(target.Image, path)
|
||||
err = oci.SaveImageAsOCILayout(target.Image, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -143,7 +143,7 @@ func mirrorToLocal(o *TufMirrorOutput, outputPath string) error {
|
||||
// output delegated targets to local directory
|
||||
for _, target := range o.delegatedTargets {
|
||||
path := filepath.Join(outputPath, target.Tag)
|
||||
err = mirror.SaveIndexAsOCILayout(target.Index, path)
|
||||
err = oci.SaveIndexAsOCILayout(target.Index, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"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/mutate"
|
||||
@@ -16,9 +17,9 @@ import (
|
||||
// TUF root metadata
|
||||
// -----------------
|
||||
|
||||
// GetMetadataManifest returns an image with TUF root metadata as layers
|
||||
func (m *TufMirror) GetMetadataManifest(metadataURL string) (v1.Image, error) {
|
||||
metadata, err := m.getTufMetadataMirror(metadataURL)
|
||||
// GetMetadataManifest returns an image with TUF root metadata as layers.
|
||||
func (m *TUFMirror) GetMetadataManifest(metadataURL string) (*oci.EmptyConfigImage, error) {
|
||||
metadata, err := m.getMetadataMirror(metadataURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get metadata: %w", err)
|
||||
}
|
||||
@@ -26,19 +27,19 @@ func (m *TufMirror) GetMetadataManifest(metadataURL string) (v1.Image, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build metadata manifest: %w", err)
|
||||
}
|
||||
return manifest, nil
|
||||
return &oci.EmptyConfigImage{Image: manifest}, nil
|
||||
}
|
||||
|
||||
// getTufMetadataMirror returns a TufMetadata struct with TUF metadata as map of file names to bytes
|
||||
func (m *TufMirror) getTufMetadataMirror(metadataURL string) (*TufMetadata, error) {
|
||||
trustedMetadata := m.TufClient.GetMetadata()
|
||||
// getMetadataMirror returns a TufMetadata struct with TUF metadata as map of file names to bytes.
|
||||
func (m *TUFMirror) getMetadataMirror(metadataURL string) (*TUFMetadata, error) {
|
||||
trustedMetadata := m.TUFClient.GetMetadata()
|
||||
|
||||
rootMetadata := map[string][]byte{}
|
||||
rootVersion := trustedMetadata.Root.Signed.Version
|
||||
// get the previous versions of root metadata if any
|
||||
if rootVersion != 1 {
|
||||
var err error
|
||||
rootMetadata, err = m.TufClient.GetPriorRoots(metadataURL)
|
||||
rootMetadata, err = m.TUFClient.GetPriorRoots(metadataURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get prior root metadata: %w", err)
|
||||
}
|
||||
@@ -69,7 +70,7 @@ func (m *TufMirror) getTufMetadataMirror(metadataURL string) (*TufMetadata, erro
|
||||
snapshotVersion = strconv.FormatInt(trustedMetadata.Snapshot.Signed.Version, 10)
|
||||
targetsVersion = strconv.FormatInt(trustedMetadata.Targets[metadata.TARGETS].Signed.Version, 10)
|
||||
}
|
||||
return &TufMetadata{
|
||||
return &TUFMetadata{
|
||||
Root: rootMetadata,
|
||||
Snapshot: map[string][]byte{nameFromRole(metadata.SNAPSHOT, snapshotVersion): snapshotBytes},
|
||||
Targets: map[string][]byte{nameFromRole(metadata.TARGETS, targetsVersion): targetsBytes},
|
||||
@@ -77,12 +78,12 @@ func (m *TufMirror) getTufMetadataMirror(metadataURL string) (*TufMetadata, erro
|
||||
}, nil
|
||||
}
|
||||
|
||||
// buildMetadataManifest returns an OCI image with TUF metadata as layers with annotations
|
||||
func (m *TufMirror) buildMetadataManifest(metadata *TufMetadata) (v1.Image, error) {
|
||||
// buildMetadataManifest returns an OCI image with TUF metadata as layers with annotations.
|
||||
func (m *TUFMirror) buildMetadataManifest(metadata *TUFMetadata) (v1.Image, error) {
|
||||
img := empty.Image
|
||||
img = mutate.MediaType(img, types.OCIManifestSchema1)
|
||||
img = mutate.ConfigMediaType(img, types.OCIConfigJSON)
|
||||
for _, role := range TufRoles {
|
||||
for _, role := range TUFRoles {
|
||||
layers, err := m.makeRoleLayers(role, metadata)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make role layer: %w", err)
|
||||
@@ -95,8 +96,8 @@ func (m *TufMirror) buildMetadataManifest(metadata *TufMetadata) (v1.Image, erro
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// makeRoleLayers returns a list of layers for a given TUF role
|
||||
func (m *TufMirror) makeRoleLayers(role TufRole, tufMetadata *TufMetadata) ([]mutate.Addendum, error) {
|
||||
// makeRoleLayers returns a list of layers for a given TUF role.
|
||||
func (m *TUFMirror) makeRoleLayers(role TUFRole, tufMetadata *TUFMetadata) ([]mutate.Addendum, error) {
|
||||
var layers []mutate.Addendum
|
||||
ann := map[string]string{tufFileAnnotation: ""}
|
||||
switch role {
|
||||
@@ -115,8 +116,8 @@ func (m *TufMirror) makeRoleLayers(role TufRole, tufMetadata *TufMetadata) ([]mu
|
||||
return layers, nil
|
||||
}
|
||||
|
||||
// annotatedMetaLayers returns a list of layers with annotations for each TUF metadata file
|
||||
func (m *TufMirror) annotatedMetaLayers(meta map[string][]byte) []mutate.Addendum {
|
||||
// annotatedMetaLayers returns a list of layers with annotations for each TUF metadata file.
|
||||
func (m *TUFMirror) annotatedMetaLayers(meta map[string][]byte) []mutate.Addendum {
|
||||
var layers []mutate.Addendum
|
||||
for name, data := range meta {
|
||||
ann := map[string]string{tufFileAnnotation: name}
|
||||
@@ -129,8 +130,8 @@ func (m *TufMirror) annotatedMetaLayers(meta map[string][]byte) []mutate.Addendu
|
||||
// TUF delegated targets metadata
|
||||
// ------------------------------
|
||||
|
||||
// GetDelegatedMetadataMirrors returns a list of mirrors (image/tag pairs) for each delegated targets role metadata
|
||||
func (m *TufMirror) GetDelegatedMetadataMirrors() ([]*MirrorImage, error) {
|
||||
// GetDelegatedMetadataMirrors returns a list of mirrors (image/tag pairs) for each delegated targets role metadata.
|
||||
func (m *TUFMirror) GetDelegatedMetadataMirrors() ([]*Image, error) {
|
||||
// get current delegated targets metadata
|
||||
delegatedTargets, err := m.getDelegatedTargetsMetadata()
|
||||
if err != nil {
|
||||
@@ -143,12 +144,12 @@ func (m *TufMirror) GetDelegatedMetadataMirrors() ([]*MirrorImage, error) {
|
||||
return mirror, nil
|
||||
}
|
||||
|
||||
// getDelegatedTargetsMetadata returns delegated targets metadata as a list of DelegatedTargetMetadata (role name and data)
|
||||
func (m *TufMirror) getDelegatedTargetsMetadata() ([]DelegatedTargetMetadata, error) {
|
||||
// getDelegatedTargetsMetadata returns delegated targets metadata as a list of DelegatedTargetMetadata (role name and data).
|
||||
func (m *TUFMirror) getDelegatedTargetsMetadata() ([]DelegatedTargetMetadata, error) {
|
||||
var delegatedTargets []DelegatedTargetMetadata
|
||||
md := m.TufClient.GetMetadata()
|
||||
md := m.TUFClient.GetMetadata()
|
||||
for _, role := range md.Targets[metadata.TARGETS].Signed.Delegations.Roles {
|
||||
roleMetadata, err := m.TufClient.LoadDelegatedTargets(role.Name, metadata.TARGETS)
|
||||
roleMetadata, err := m.TUFClient.LoadDelegatedTargets(role.Name, metadata.TARGETS)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get delegated role metadata: %w", err)
|
||||
}
|
||||
@@ -170,9 +171,9 @@ func (m *TufMirror) getDelegatedTargetsMetadata() ([]DelegatedTargetMetadata, er
|
||||
return delegatedTargets, nil
|
||||
}
|
||||
|
||||
// buildDelegatedMetadataManifests returns a list of mirrors (image/tag pairs) for each delegated target role metadata
|
||||
func (m *TufMirror) buildDelegatedMetadataManifests(delegated []DelegatedTargetMetadata) ([]*MirrorImage, error) {
|
||||
manifests := []*MirrorImage{}
|
||||
// buildDelegatedMetadataManifests returns a list of mirrors (image/tag pairs) for each delegated target role metadata.
|
||||
func (m *TUFMirror) buildDelegatedMetadataManifests(delegated []DelegatedTargetMetadata) ([]*Image, error) {
|
||||
manifests := []*Image{}
|
||||
for _, role := range delegated {
|
||||
img := empty.Image
|
||||
img = mutate.MediaType(img, types.OCIManifestSchema1)
|
||||
@@ -183,7 +184,7 @@ func (m *TufMirror) buildDelegatedMetadataManifests(delegated []DelegatedTargetM
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to append delegated targets layer to image: %w", err)
|
||||
}
|
||||
manifests = append(manifests, &MirrorImage{Image: img, Tag: role.Name})
|
||||
manifests = append(manifests, &Image{Image: &oci.EmptyConfigImage{Image: img}, Tag: role.Name})
|
||||
}
|
||||
return manifests, nil
|
||||
}
|
||||
|
||||
@@ -9,22 +9,26 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/embed"
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/pkg/tuf"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/theupdateframework/go-tuf/v2/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
metadataPath = "/metadata"
|
||||
targetsPath = "/targets"
|
||||
)
|
||||
|
||||
func TestGetTufMetadataMirror(t *testing.T) {
|
||||
server := httptest.NewServer(http.FileServer(http.Dir(filepath.Join("..", "..", "test", "testdata", "tuf", "test-repo"))))
|
||||
defer server.Close()
|
||||
|
||||
path := test.CreateTempDir(t, "", "tuf_temp")
|
||||
m, err := NewTufMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
|
||||
m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
|
||||
assert.NoError(t, err)
|
||||
|
||||
tufMetadata, err := m.getTufMetadataMirror(server.URL + "/metadata")
|
||||
tufMetadata, err := m.getMetadataMirror(server.URL + metadataPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check that all roles are not empty
|
||||
@@ -39,10 +43,10 @@ func TestGetMetadataManifest(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
path := test.CreateTempDir(t, "", "tuf_temp")
|
||||
m, err := NewTufMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
|
||||
m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
|
||||
assert.NoError(t, err)
|
||||
|
||||
img, err := m.GetMetadataManifest(server.URL + "/metadata")
|
||||
img, err := m.GetMetadataManifest(server.URL + metadataPath)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, img)
|
||||
|
||||
@@ -78,7 +82,7 @@ func TestGetDelegatedMetadataMirrors(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
path := test.CreateTempDir(t, "", "tuf_temp")
|
||||
m, err := NewTufMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
|
||||
m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
|
||||
assert.NoError(t, err)
|
||||
|
||||
delegations, err := m.GetDelegatedMetadataMirrors()
|
||||
|
||||
@@ -2,156 +2,17 @@ package mirror
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/docker/attest/internal/embed"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/tuf"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
func NewTufMirror(root []byte, tufPath, metadataURL, targetsURL string, versionChecker tuf.VersionChecker) (*TufMirror, error) {
|
||||
func NewTUFMirror(root []byte, tufPath, metadataURL, targetsURL string, versionChecker tuf.VersionChecker) (*TUFMirror, error) {
|
||||
if root == nil {
|
||||
root = embed.RootDefault.Data
|
||||
root = tuf.DockerTUFRootDefault.Data
|
||||
}
|
||||
tufClient, err := tuf.NewTufClient(root, tufPath, metadataURL, targetsURL, versionChecker)
|
||||
tufClient, err := tuf.NewClient(&tuf.ClientOptions{InitialRoot: root, Path: tufPath, MetadataSource: metadataURL, TargetsSource: targetsURL, VersionChecker: versionChecker})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create TUF client: %w", err)
|
||||
}
|
||||
return &TufMirror{TufClient: tufClient, tufPath: tufPath, metadataURL: metadataURL, targetsURL: targetsURL}, nil
|
||||
}
|
||||
|
||||
func PushImageToRegistry(image v1.Image, imageName string) error {
|
||||
ref, err := name.ParseReference(imageName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse image name '%s': %w", imageName, err)
|
||||
}
|
||||
|
||||
// Push the image to the registry
|
||||
return remote.Write(ref, image, oci.MultiKeychainOption())
|
||||
}
|
||||
|
||||
func PushIndexToRegistry(index 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)
|
||||
}
|
||||
|
||||
// Push the index to the registry
|
||||
return remote.WriteIndex(ref, index, oci.MultiKeychainOption())
|
||||
}
|
||||
|
||||
func SaveImageAsOCILayout(image v1.Image, path string) error {
|
||||
// Save the image to the local filesystem
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create directory: %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.ModePerm)
|
||||
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
|
||||
}
|
||||
|
||||
func SaveIndex(outputs []*oci.ImageSpec, index v1.ImageIndex, indexName string) error {
|
||||
// split output by comma and write or push each one
|
||||
for _, output := range outputs {
|
||||
if output.Type == oci.OCI {
|
||||
idx := v1.ImageIndex(empty.Index)
|
||||
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
||||
Add: index,
|
||||
Descriptor: v1.Descriptor{
|
||||
Annotations: map[string]string{
|
||||
oci.OciReferenceTarget: indexName,
|
||||
},
|
||||
},
|
||||
})
|
||||
err := SaveIndexAsOCILayout(idx, output.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write signed image: %w", err)
|
||||
}
|
||||
} else {
|
||||
err := PushIndexToRegistry(index, output.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to push signed image: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SaveImage(output *oci.ImageSpec, image v1.Image, imageName string) error {
|
||||
if output.Type == oci.OCI {
|
||||
idx := v1.ImageIndex(empty.Index)
|
||||
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
||||
Add: image,
|
||||
Descriptor: v1.Descriptor{
|
||||
Annotations: map[string]string{
|
||||
oci.OciReferenceTarget: imageName,
|
||||
},
|
||||
},
|
||||
})
|
||||
err := SaveIndexAsOCILayout(idx, output.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write signed image: %w", err)
|
||||
}
|
||||
} else {
|
||||
err := PushImageToRegistry(image, output.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to push signed image: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SaveReferrers(manifest *attestation.AttestationManifest, outputs []*oci.ImageSpec) error {
|
||||
for _, output := range outputs {
|
||||
if output.Type == oci.OCI {
|
||||
continue
|
||||
}
|
||||
// so that we use the same tag each time to reduce number of tags (tags aren't needed for referrers but we must push one)
|
||||
attOut, err := oci.ReplaceTagInSpec(output, manifest.SubjectDescriptor.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//otherwise we end up with the detected platform, though I'm not sure it matters
|
||||
attOut.Platform = &v1.Platform{
|
||||
OS: "unknown",
|
||||
Architecture: "unknown",
|
||||
}
|
||||
images, err := manifest.BuildReferringArtifacts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build image: %w", err)
|
||||
}
|
||||
for _, image := range images {
|
||||
err = SaveImage(attOut, image, "")
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to push image: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return &TUFMirror{TUFClient: tufClient, tufPath: tufPath, metadataURL: metadataURL, targetsURL: targetsURL}, nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"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/mutate"
|
||||
@@ -13,16 +14,16 @@ import (
|
||||
"github.com/theupdateframework/go-tuf/v2/metadata"
|
||||
)
|
||||
|
||||
// GetTufTargetMirrors returns a list of top-level target files as MirrorImages (image with tag)
|
||||
func (m *TufMirror) GetTufTargetMirrors() ([]*MirrorImage, error) {
|
||||
targetMirrors := []*MirrorImage{}
|
||||
md := m.TufClient.GetMetadata()
|
||||
// GetTUFTargetMirrors returns a list of top-level target files as MirrorImages (image with tag).
|
||||
func (m *TUFMirror) GetTUFTargetMirrors() ([]*Image, error) {
|
||||
targetMirrors := []*Image{}
|
||||
md := m.TUFClient.GetMetadata()
|
||||
|
||||
// for each top-level target file, create an image with the target file as a layer
|
||||
targets := md.Targets[metadata.TARGETS].Signed.Targets
|
||||
for _, t := range targets {
|
||||
// download target file
|
||||
_, data, err := m.TufClient.DownloadTarget(t.Path, filepath.Join(m.tufPath, "download"))
|
||||
file, err := m.TUFClient.DownloadTarget(t.Path, filepath.Join(m.tufPath, "download"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download target %s: %w", t.Path, err)
|
||||
}
|
||||
@@ -37,21 +38,21 @@ func (m *TufMirror) GetTufTargetMirrors() ([]*MirrorImage, error) {
|
||||
}
|
||||
name := hash.String() + "." + t.Path
|
||||
ann := map[string]string{tufFileAnnotation: name}
|
||||
layer := mutate.Addendum{Layer: static.NewLayer(data, tufTargetMediaType), Annotations: ann}
|
||||
layer := mutate.Addendum{Layer: static.NewLayer(file.Data, tufTargetMediaType), Annotations: ann}
|
||||
img, err = mutate.Append(img, layer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to append role layer to image: %w", err)
|
||||
}
|
||||
targetMirrors = append(targetMirrors, &MirrorImage{Image: img, Tag: name})
|
||||
targetMirrors = append(targetMirrors, &Image{Image: &oci.EmptyConfigImage{Image: img}, Tag: name})
|
||||
}
|
||||
return targetMirrors, nil
|
||||
}
|
||||
|
||||
// GetDelegatedTargetMirrors returns a list of delegated target files as MirrorIndexes (image index with tag)
|
||||
// each image in the index contains a delegated target file
|
||||
func (m *TufMirror) GetDelegatedTargetMirrors() ([]*MirrorIndex, error) {
|
||||
mirror := []*MirrorIndex{}
|
||||
md := m.TufClient.GetMetadata()
|
||||
// each image in the index contains a delegated target file.
|
||||
func (m *TUFMirror) GetDelegatedTargetMirrors() ([]*Index, error) {
|
||||
mirror := []*Index{}
|
||||
md := m.TUFClient.GetMetadata()
|
||||
|
||||
// for each delegated role, create an image index with target files as images
|
||||
roles := md.Targets[metadata.TARGETS].Signed.Delegations.Roles
|
||||
@@ -60,7 +61,7 @@ func (m *TufMirror) GetDelegatedTargetMirrors() ([]*MirrorIndex, error) {
|
||||
index := v1.ImageIndex(empty.Index)
|
||||
|
||||
// get delegated targets metadata for role
|
||||
roleMeta, err := m.TufClient.LoadDelegatedTargets(role.Name, metadata.TARGETS)
|
||||
roleMeta, err := m.TUFClient.LoadDelegatedTargets(role.Name, metadata.TARGETS)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load delegated targets metadata: %w", err)
|
||||
}
|
||||
@@ -68,7 +69,7 @@ func (m *TufMirror) GetDelegatedTargetMirrors() ([]*MirrorIndex, error) {
|
||||
// for each target file, create an image with the target file as a layer
|
||||
for _, target := range roleMeta.Signed.Targets {
|
||||
// download target file
|
||||
_, data, err := m.TufClient.DownloadTarget(target.Path, filepath.Join(m.tufPath, "download"))
|
||||
file, err := m.TUFClient.DownloadTarget(target.Path, filepath.Join(m.tufPath, "download"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download target %s: %w", target.Path, err)
|
||||
}
|
||||
@@ -88,14 +89,15 @@ func (m *TufMirror) GetDelegatedTargetMirrors() ([]*MirrorIndex, error) {
|
||||
}
|
||||
name := hash.String() + "." + filename
|
||||
ann := map[string]string{tufFileAnnotation: name}
|
||||
layer := mutate.Addendum{Layer: static.NewLayer(data, tufTargetMediaType), Annotations: ann}
|
||||
layer := mutate.Addendum{Layer: static.NewLayer(file.Data, tufTargetMediaType), Annotations: ann}
|
||||
img, err = mutate.Append(img, layer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to append role layer to image: %w", err)
|
||||
}
|
||||
emptyConfigImage := &oci.EmptyConfigImage{Image: img}
|
||||
// append image to index with annotation
|
||||
index = mutate.AppendManifests(index, mutate.IndexAddendum{
|
||||
Add: img,
|
||||
Add: emptyConfigImage,
|
||||
Descriptor: v1.Descriptor{
|
||||
Annotations: map[string]string{
|
||||
tufFileAnnotation: fmt.Sprintf("%s/%s", subdir, name),
|
||||
@@ -103,7 +105,7 @@ func (m *TufMirror) GetDelegatedTargetMirrors() ([]*MirrorIndex, error) {
|
||||
},
|
||||
})
|
||||
}
|
||||
mirror = append(mirror, &MirrorIndex{Index: index, Tag: role.Name})
|
||||
mirror = append(mirror, &Index{Index: index, Tag: role.Name})
|
||||
}
|
||||
return mirror, nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/embed"
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/pkg/tuf"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -27,10 +26,10 @@ func TestGetTufTargetsMirror(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
path := test.CreateTempDir(t, "", "tuf_temp")
|
||||
m, err := NewTufMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
|
||||
m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
|
||||
assert.NoError(t, err)
|
||||
|
||||
targets, err := m.GetTufTargetMirrors()
|
||||
targets, err := m.GetTUFTargetMirrors()
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, len(targets), 0)
|
||||
|
||||
@@ -61,10 +60,10 @@ func TestTargetDelegationMetadata(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
path := test.CreateTempDir(t, "", "tuf_temp")
|
||||
tm, err := NewTufMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
|
||||
tm, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
|
||||
assert.NoError(t, err)
|
||||
|
||||
targets, err := tm.TufClient.LoadDelegatedTargets("test-role", "targets")
|
||||
targets, err := tm.TUFClient.LoadDelegatedTargets("test-role", "targets")
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, len(targets.Signed.Targets), 0)
|
||||
}
|
||||
@@ -74,7 +73,7 @@ func TestGetDelegatedTargetMirrors(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
path := test.CreateTempDir(t, "", "tuf_temp")
|
||||
m, err := NewTufMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
|
||||
m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
|
||||
assert.NoError(t, err)
|
||||
|
||||
mirrors, err := m.GetDelegatedTargetMirrors()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package mirror
|
||||
|
||||
import (
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/tuf"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/theupdateframework/go-tuf/v2/metadata"
|
||||
@@ -14,11 +15,11 @@ const (
|
||||
tufFileAnnotation = "tuf.io/filename"
|
||||
)
|
||||
|
||||
type TufRole string
|
||||
type TUFRole string
|
||||
|
||||
var TufRoles = []TufRole{metadata.ROOT, metadata.SNAPSHOT, metadata.TARGETS, metadata.TIMESTAMP}
|
||||
var TUFRoles = []TUFRole{metadata.ROOT, metadata.SNAPSHOT, metadata.TARGETS, metadata.TIMESTAMP}
|
||||
|
||||
type TufMetadata struct {
|
||||
type TUFMetadata struct {
|
||||
Root map[string][]byte
|
||||
Snapshot map[string][]byte
|
||||
Targets map[string][]byte
|
||||
@@ -31,18 +32,18 @@ type DelegatedTargetMetadata struct {
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type MirrorImage struct {
|
||||
Image v1.Image
|
||||
type Image struct {
|
||||
Image *oci.EmptyConfigImage
|
||||
Tag string
|
||||
}
|
||||
|
||||
type MirrorIndex struct {
|
||||
type Index struct {
|
||||
Index v1.ImageIndex
|
||||
Tag string
|
||||
}
|
||||
|
||||
type TufMirror struct {
|
||||
TufClient *tuf.TufClient
|
||||
type TUFMirror struct {
|
||||
TUFClient *tuf.Client
|
||||
tufPath string
|
||||
metadataURL string
|
||||
targetsURL string
|
||||
|
||||
2
pkg/oci/README.md
Normal file
2
pkg/oci/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## oci
|
||||
This package is for generic OCI components. For attestation specific components see the `attestation` package.
|
||||
@@ -1,20 +1,17 @@
|
||||
//go:build e2e
|
||||
|
||||
package mirror_test
|
||||
package oci_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/pkg/mirror"
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRegistryAuth(t *testing.T) {
|
||||
UnsignedTestImage := filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
// test cases for ecr, gcr and dockerhub
|
||||
testCases := []struct {
|
||||
@@ -25,7 +22,7 @@ func TestRegistryAuth(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.Image, func(t *testing.T) {
|
||||
err := mirror.PushIndexToRegistry(attIdx.Index, tc.Image)
|
||||
err := oci.PushIndexToRegistry(attIdx.Index, tc.Image)
|
||||
require.NoError(t, err)
|
||||
_, err = oci.IndexFromRemote(tc.Image)
|
||||
require.NoError(t, err)
|
||||
@@ -7,21 +7,21 @@ import (
|
||||
)
|
||||
|
||||
type userAgentTransporter struct {
|
||||
ua string
|
||||
rt http.RoundTripper
|
||||
userAgent string
|
||||
roundTripper http.RoundTripper
|
||||
}
|
||||
|
||||
type Option = func(*http.Client)
|
||||
|
||||
func (u *userAgentTransporter) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Set("User-Agent", u.ua)
|
||||
req.Header.Set("User-Agent", u.userAgent)
|
||||
|
||||
return u.rt.RoundTrip(req)
|
||||
return u.roundTripper.RoundTrip(req)
|
||||
}
|
||||
|
||||
func HttpTransport() http.RoundTripper {
|
||||
func HTTPTransport() http.RoundTripper {
|
||||
return &userAgentTransporter{
|
||||
ua: "Docker-Client",
|
||||
rt: cleanhttp.DefaultTransport(),
|
||||
userAgent: "Docker-Client",
|
||||
roundTripper: cleanhttp.DefaultTransport(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,11 @@ package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
att "github.com/docker/attest/pkg/attestation"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
|
||||
@@ -18,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
// ParsePlatform parses the provided platform string or attempts to obtain
|
||||
// the platform of the current host system
|
||||
// the platform of the current host system.
|
||||
func ParsePlatform(platformStr string) (*v1.Platform, error) {
|
||||
if platformStr == "" {
|
||||
cdp := platforms.Normalize(platforms.DefaultSpec())
|
||||
@@ -30,14 +27,13 @@ func ParsePlatform(platformStr string) (*v1.Platform, error) {
|
||||
Architecture: cdp.Architecture,
|
||||
Variant: cdp.Variant,
|
||||
}, nil
|
||||
} else {
|
||||
return v1.ParsePlatform(platformStr)
|
||||
}
|
||||
return v1.ParsePlatform(platformStr)
|
||||
}
|
||||
|
||||
func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
|
||||
// prepare options
|
||||
options := []remote.Option{MultiKeychainOption(), remote.WithTransport(HttpTransport()), remote.WithContext(ctx)}
|
||||
options := []remote.Option{MultiKeychainOption(), remote.WithTransport(HTTPTransport()), remote.WithContext(ctx)}
|
||||
|
||||
// add in platform into remote Get operation; this might conflict with an explicit digest, but we are trying anyway
|
||||
if platform != nil {
|
||||
@@ -46,61 +42,18 @@ func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
|
||||
return options
|
||||
}
|
||||
|
||||
func ExtractEnvelopes(manifest *attestation.AttestationManifest, predicateType string) ([]*att.Envelope, error) {
|
||||
var envs []*att.Envelope
|
||||
dsseMediaType, err := attestation.DSSEMediaType(predicateType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err)
|
||||
}
|
||||
for _, attestationLayer := range manifest.OriginalLayers {
|
||||
mt, err := attestationLayer.Layer.MediaType()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||
}
|
||||
if string(mt) == dsseMediaType {
|
||||
reader, err := attestationLayer.Layer.Uncompressed()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
var env = new(att.Envelope)
|
||||
err = json.NewDecoder(reader).Decode(&env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode envelope: %w", err)
|
||||
}
|
||||
envs = append(envs, env)
|
||||
}
|
||||
}
|
||||
|
||||
return envs, nil
|
||||
}
|
||||
|
||||
func imageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descriptor, error) {
|
||||
for _, m := range ix.Manifests {
|
||||
func ImageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descriptor, error) {
|
||||
for i := range ix.Manifests {
|
||||
m := &ix.Manifests[i]
|
||||
if (m.MediaType == ocispec.MediaTypeImageManifest || m.MediaType == "application/vnd.docker.distribution.manifest.v2+json") && m.Platform.Equals(*platform) {
|
||||
return &m, nil
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no image found for platform %v", platform)
|
||||
}
|
||||
|
||||
func attestationDigestForDigest(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) {
|
||||
for _, m := range ix.Manifests {
|
||||
if v, ok := m.Annotations[att.DockerReferenceType]; ok && v == attestType {
|
||||
if d, ok := m.Annotations[att.DockerReferenceDigest]; ok && d == imageDigest {
|
||||
return m.Digest.String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no attestation found for image %s", imageDigest)
|
||||
}
|
||||
|
||||
func RefToPURL(ref string, platform *v1.Platform) (string, bool, error) {
|
||||
func RefToPURL(named reference.Named, platform *v1.Platform) (string, bool, error) {
|
||||
var isCanonical bool
|
||||
named, err := reference.ParseNormalizedNamed(ref)
|
||||
if err != nil {
|
||||
return "", false, fmt.Errorf("failed to parse ref %q: %w", ref, err)
|
||||
}
|
||||
var qualifiers []packageurl.Qualifier
|
||||
|
||||
if canonical, ok := named.(reference.Canonical); ok {
|
||||
@@ -149,7 +102,7 @@ func SplitDigest(digest string) (common.DigestSet, error) {
|
||||
}
|
||||
|
||||
func ReplaceTagInSpec(src *ImageSpec, digest v1.Hash) (*ImageSpec, error) {
|
||||
newName, err := replaceTag(src.Identifier, digest)
|
||||
newName, err := ReplaceTag(src.Identifier, digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse repo name: %w", err)
|
||||
}
|
||||
@@ -160,8 +113,8 @@ func ReplaceTagInSpec(src *ImageSpec, digest v1.Hash) (*ImageSpec, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// so that the index tag is replaced with a tag unique to the image digest and doesn't overwrite it
|
||||
func replaceTag(image string, digest v1.Hash) (string, error) {
|
||||
// so that the index tag is replaced with a tag unique to the image digest and doesn't overwrite it.
|
||||
func ReplaceTag(image string, digest v1.Hash) (string, error) {
|
||||
if strings.HasPrefix(image, LocalPrefix) {
|
||||
return image, nil
|
||||
}
|
||||
@@ -171,3 +124,26 @@ func replaceTag(image string, digest v1.Hash) (string, error) {
|
||||
}
|
||||
return fmt.Sprintf("%s:%s-%s.att", notag, digest.Algorithm, digest.Hex), nil
|
||||
}
|
||||
|
||||
func ReplaceDigestInSpec(src *ImageSpec, digest v1.Hash) (*ImageSpec, error) {
|
||||
newName, err := replaceDigest(src.Identifier, digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse repo name: %w", err)
|
||||
}
|
||||
return &ImageSpec{
|
||||
Identifier: newName,
|
||||
Type: src.Type,
|
||||
Platform: src.Platform,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func replaceDigest(image string, digest v1.Hash) (string, error) {
|
||||
if strings.HasPrefix(image, LocalPrefix) {
|
||||
return image, nil
|
||||
}
|
||||
notag, err := WithoutTag(image)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
return fmt.Sprintf("%s@%s:%s", notag, digest.Algorithm, digest.Hex), nil
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package oci
|
||||
package oci_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -11,56 +13,61 @@ import (
|
||||
)
|
||||
|
||||
func TestRefToPurl(t *testing.T) {
|
||||
arm, err := ParsePlatform("arm64/linux")
|
||||
arm, err := oci.ParsePlatform("arm64/linux")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err := RefToPURL("alpine", arm)
|
||||
ref, err := reference.ParseNormalizedNamed("alpine")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err := oci.RefToPURL(ref, arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/alpine@latest?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
|
||||
purl, canonical, err = RefToPURL("alpine:123", arm)
|
||||
ref, err = reference.ParseNormalizedNamed("alpine:123")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
|
||||
purl, canonical, err = RefToPURL("google/alpine:123", arm)
|
||||
ref, err = reference.ParseNormalizedNamed("google/alpine:123")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/google/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
|
||||
purl, canonical, err = RefToPURL("library/alpine:123", arm)
|
||||
ref, err = reference.ParseNormalizedNamed("library/alpine:123")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
|
||||
purl, canonical, err = RefToPURL("docker.io/library/alpine:123", arm)
|
||||
ref, err = reference.ParseNormalizedNamed("docker.io/library/alpine:123")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
|
||||
purl, canonical, err = RefToPURL("localhost:5001/library/alpine:123", arm)
|
||||
ref, err = reference.ParseNormalizedNamed("localhost:5001/library/alpine:123")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/localhost%3A5001/library/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
|
||||
purl, canonical, err = RefToPURL("localhost:5001/alpine:123", arm)
|
||||
ref, err = reference.ParseNormalizedNamed("localhost:5001/alpine:123")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
|
||||
purl, canonical, err = RefToPURL("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b", arm)
|
||||
ref, err = reference.ParseNormalizedNamed("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine?digest=sha256%3Ac5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b&platform=arm64%2Flinux", purl)
|
||||
assert.True(t, canonical)
|
||||
}
|
||||
|
||||
var (
|
||||
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||
)
|
||||
|
||||
// Test fix for https://github.com/docker/secure-artifacts-team-issues/issues/202
|
||||
func TestImageDigestForPlatform(t *testing.T) {
|
||||
idx, err := layout.ImageIndexFromPath(UnsignedTestImage)
|
||||
idx, err := layout.ImageIndexFromPath(test.UnsignedTestImage)
|
||||
assert.NoError(t, err)
|
||||
|
||||
idxm, err := idx.IndexManifest()
|
||||
@@ -74,16 +81,16 @@ func TestImageDigestForPlatform(t *testing.T) {
|
||||
mfs2, err := mfs.IndexManifest()
|
||||
assert.NoError(t, err)
|
||||
|
||||
p, err := ParsePlatform("linux/amd64")
|
||||
p, err := oci.ParsePlatform("linux/amd64")
|
||||
assert.NoError(t, err)
|
||||
desc, err := imageDescriptor(mfs2, p)
|
||||
desc, err := oci.ImageDescriptor(mfs2, p)
|
||||
assert.NoError(t, err)
|
||||
digest := desc.Digest.String()
|
||||
assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", digest)
|
||||
|
||||
p, err = ParsePlatform("linux/arm64")
|
||||
p, err = oci.ParsePlatform("linux/arm64")
|
||||
assert.NoError(t, err)
|
||||
desc, err = imageDescriptor(mfs2, p)
|
||||
desc, err = oci.ImageDescriptor(mfs2, p)
|
||||
assert.NoError(t, err)
|
||||
digest = desc.Digest.String()
|
||||
assert.Equal(t, "sha256:7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e", digest)
|
||||
@@ -97,14 +104,14 @@ func TestWithoutTag(t *testing.T) {
|
||||
{name: "image:tag", expected: "index.docker.io/library/image"},
|
||||
{name: "image", expected: "index.docker.io/library/image"},
|
||||
{name: "image:sha256-digest.att", expected: "index.docker.io/library/image"},
|
||||
{name: "docker://image:tag", expected: "docker://index.docker.io/library/image"},
|
||||
{name: oci.RegistryPrefix + "image:tag", expected: oci.RegistryPrefix + "index.docker.io/library/image"},
|
||||
{name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image"},
|
||||
{name: "docker://image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "docker://index.docker.io/library/image"},
|
||||
{name: "docker://127.0.0.1:36555/repo:latest", expected: "docker://127.0.0.1:36555/repo"},
|
||||
{name: oci.RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: oci.RegistryPrefix + "index.docker.io/library/image"},
|
||||
{name: oci.RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: oci.RegistryPrefix + "127.0.0.1:36555/repo"},
|
||||
}
|
||||
for _, c := range tc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
notag, _ := WithoutTag(c.name)
|
||||
notag, _ := oci.WithoutTag(c.name)
|
||||
assert.Equal(t, c.expected, notag)
|
||||
})
|
||||
}
|
||||
@@ -118,11 +125,11 @@ func TestReplaceTag(t *testing.T) {
|
||||
{name: "image:tag", expected: "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: "image", expected: "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: "image:sha256-digest.att", expected: "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: "docker://image:tag", expected: "docker://index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: oci.RegistryPrefix + "image:tag", expected: oci.RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: "oci://foobar", expected: "oci://foobar"},
|
||||
{name: "docker://image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "docker://index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: "docker://127.0.0.1:36555/repo:latest", expected: "docker://127.0.0.1:36555/repo:sha256-digest.att"},
|
||||
{name: oci.LocalPrefix + "foobar", expected: oci.LocalPrefix + "foobar"},
|
||||
{name: oci.RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: oci.RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: oci.RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: oci.RegistryPrefix + "127.0.0.1:36555/repo:sha256-digest.att"},
|
||||
}
|
||||
|
||||
digest := v1.Hash{
|
||||
@@ -131,7 +138,7 @@ func TestReplaceTag(t *testing.T) {
|
||||
}
|
||||
for _, c := range tc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
replaced, err := replaceTag(c.name, digest)
|
||||
replaced, err := oci.ReplaceTag(c.name, digest)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.expected, replaced)
|
||||
})
|
||||
|
||||
144
pkg/oci/output.go
Normal file
144
pkg/oci/output.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
// PushImageToRegistry pushes an image to the registry with the specified name.
|
||||
func PushImageToRegistry(image v1.Image, imageName string) error {
|
||||
ref, err := name.ParseReference(imageName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse image name '%s': %w", imageName, err)
|
||||
}
|
||||
|
||||
// Push the image to the registry
|
||||
return remote.Write(ref, image, MultiKeychainOption())
|
||||
}
|
||||
|
||||
// PushIndexToRegistry pushes an index to the registry with the specified name.
|
||||
func PushIndexToRegistry(index v1.ImageIndex, imageName string) error {
|
||||
// Parse the index name
|
||||
ref, err := name.ParseReference(imageName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse image name: %w", err)
|
||||
}
|
||||
|
||||
// Push the index to the registry
|
||||
return remote.WriteIndex(ref, index, MultiKeychainOption())
|
||||
}
|
||||
|
||||
// SaveIndexAsOCILayout saves an image as an OCI layout to the specified path.
|
||||
func SaveImageAsOCILayout(image v1.Image, path string) error {
|
||||
// Save the image to the local filesystem
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create directory: %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)
|
||||
}
|
||||
|
||||
// SaveIndexAsOCILayout saves an index as an OCI layout to the specified path.
|
||||
func SaveIndexAsOCILayout(image v1.ImageIndex, path string) error {
|
||||
// Save the index to the local filesystem
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
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
|
||||
}
|
||||
|
||||
// SaveIndex saves an index to the specified outputs.
|
||||
func SaveIndex(outputs []*ImageSpec, index v1.ImageIndex, indexName string) error {
|
||||
// split output by comma and write or push each one
|
||||
for _, output := range outputs {
|
||||
if output.Type == OCI {
|
||||
idx := v1.ImageIndex(empty.Index)
|
||||
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
||||
Add: index,
|
||||
Descriptor: v1.Descriptor{
|
||||
Annotations: map[string]string{
|
||||
OCIReferenceTarget: indexName,
|
||||
},
|
||||
},
|
||||
})
|
||||
err := SaveIndexAsOCILayout(idx, output.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write signed image: %w", err)
|
||||
}
|
||||
} else {
|
||||
err := PushIndexToRegistry(index, output.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to push signed image: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveImage saves an image to the specified output.
|
||||
func SaveImage(output *ImageSpec, image v1.Image, imageName string) error {
|
||||
if output.Type == OCI {
|
||||
idx := v1.ImageIndex(empty.Index)
|
||||
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
||||
Add: image,
|
||||
Descriptor: v1.Descriptor{
|
||||
Annotations: map[string]string{
|
||||
OCIReferenceTarget: imageName,
|
||||
},
|
||||
},
|
||||
})
|
||||
err := SaveIndexAsOCILayout(idx, output.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write signed image: %w", err)
|
||||
}
|
||||
} else {
|
||||
err := PushImageToRegistry(image, output.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to push signed image: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveImagesNoTag saves a list of images by digest to the specified outputs.
|
||||
func SaveImagesNoTag(images []v1.Image, outputs []*ImageSpec) error {
|
||||
for _, output := range outputs {
|
||||
// OCI layout output not supported
|
||||
if output.Type == OCI {
|
||||
continue
|
||||
}
|
||||
for _, image := range images {
|
||||
digest, err := image.Digest()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get image digest: %w", err)
|
||||
}
|
||||
spec, err := ReplaceDigestInSpec(output, digest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create image spec: %w", err)
|
||||
}
|
||||
err = PushImageToRegistry(image, spec.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to push image: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
package mirror
|
||||
package oci_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/oci"
|
||||
"github.com/google/go-containerregistry/pkg/registry"
|
||||
@@ -19,9 +17,8 @@ import (
|
||||
)
|
||||
|
||||
func TestSavingIndex(t *testing.T) {
|
||||
UnsignedTestImage := filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||
outputLayout := test.CreateTempDir(t, "", "mirror-test")
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
|
||||
server := httptest.NewServer(registry.New())
|
||||
@@ -33,17 +30,16 @@ func TestSavingIndex(t *testing.T) {
|
||||
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||
output, err := oci.ParseImageSpecs(indexName)
|
||||
require.NoError(t, err)
|
||||
err = SaveIndex(output, attIdx.Index, indexName)
|
||||
err = oci.SaveIndex(output, attIdx.Index, indexName)
|
||||
require.NoError(t, err)
|
||||
|
||||
ociOutput, err := oci.ParseImageSpecs("oci://" + outputLayout)
|
||||
ociOutput, err := oci.ParseImageSpecs(oci.LocalPrefix + outputLayout)
|
||||
require.NoError(t, err)
|
||||
err = SaveIndex(ociOutput, attIdx.Index, indexName)
|
||||
err = oci.SaveIndex(ociOutput, attIdx.Index, indexName)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSavingImage(t *testing.T) {
|
||||
|
||||
outputLayout := test.CreateTempDir(t, "", "mirror-test")
|
||||
|
||||
img := empty.Image
|
||||
@@ -57,12 +53,12 @@ func TestSavingImage(t *testing.T) {
|
||||
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||
output, err := oci.ParseImageSpec(indexName)
|
||||
require.NoError(t, err)
|
||||
err = SaveImage(output, img, indexName)
|
||||
err = oci.SaveImage(output, img, indexName)
|
||||
require.NoError(t, err)
|
||||
|
||||
ociOutput, err := oci.ParseImageSpec("oci://" + outputLayout)
|
||||
ociOutput, err := oci.ParseImageSpec(oci.LocalPrefix + outputLayout)
|
||||
require.NoError(t, err)
|
||||
err = SaveImage(ociOutput, img, indexName)
|
||||
err = oci.SaveImage(ociOutput, img, indexName)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -81,9 +77,9 @@ func TestSavingReferrers(t *testing.T) {
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Digest: digest,
|
||||
}
|
||||
manifest, err := attest.NewAttestationManifest(subject)
|
||||
manifest, err := attestation.NewManifest(subject)
|
||||
require.NoError(t, err)
|
||||
err = manifest.AddAttestation(ctx, signer, statement, opts)
|
||||
err = manifest.Add(ctx, signer, statement, opts)
|
||||
require.NoError(t, err)
|
||||
server := httptest.NewServer(registry.New(registry.WithReferrersSupport(true)))
|
||||
defer server.Close()
|
||||
@@ -94,16 +90,18 @@ func TestSavingReferrers(t *testing.T) {
|
||||
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||
output, err := oci.ParseImageSpecs(indexName)
|
||||
require.NoError(t, err)
|
||||
err = SaveReferrers(manifest, output)
|
||||
artifacts, err := manifest.BuildReferringArtifacts()
|
||||
require.NoError(t, err)
|
||||
err = oci.SaveImagesNoTag(artifacts, output)
|
||||
require.NoError(t, err)
|
||||
|
||||
reg := &test.MockRegistryResolver{
|
||||
reg := &attestation.MockRegistryResolver{
|
||||
Subject: subject,
|
||||
MockResolver: &test.MockResolver{},
|
||||
MockResolver: &attestation.MockResolver{},
|
||||
ImageNameStr: indexName,
|
||||
}
|
||||
require.NoError(t, err)
|
||||
refResolver, err := oci.NewReferrersAttestationResolver(reg)
|
||||
refResolver, err := attestation.NewReferrersResolver(reg)
|
||||
require.NoError(t, err)
|
||||
attestations, err := refResolver.Attestations(ctx, attestation.VSAPredicateType)
|
||||
require.NoError(t, err)
|
||||
@@ -4,18 +4,11 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
att "github.com/docker/attest/pkg/attestation"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
type RegistryResolver struct {
|
||||
*RegistryImageDetailsResolver
|
||||
*attestation.AttestationManifest
|
||||
}
|
||||
|
||||
type RegistryImageDetailsResolver struct {
|
||||
*ImageSpec
|
||||
descriptor *v1.Descriptor
|
||||
@@ -27,17 +20,11 @@ func NewRegistryImageDetailsResolver(src *ImageSpec) (*RegistryImageDetailsResol
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewRegistryAttestationResolver(src *RegistryImageDetailsResolver) (*RegistryResolver, error) {
|
||||
return &RegistryResolver{
|
||||
RegistryImageDetailsResolver: src,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *RegistryImageDetailsResolver) ImageName(ctx context.Context) (string, error) {
|
||||
func (r *RegistryImageDetailsResolver) ImageName(_ context.Context) (string, error) {
|
||||
return r.Identifier, nil
|
||||
}
|
||||
|
||||
func (r *RegistryImageDetailsResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
|
||||
func (r *RegistryImageDetailsResolver) ImagePlatform(_ context.Context) (*v1.Platform, error) {
|
||||
return r.Platform, nil
|
||||
}
|
||||
|
||||
@@ -72,70 +59,3 @@ func (r *RegistryImageDetailsResolver) ImageDescriptor(ctx context.Context) (*v1
|
||||
}
|
||||
return r.descriptor, nil
|
||||
}
|
||||
|
||||
func (r *RegistryResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
||||
if r.AttestationManifest == nil {
|
||||
attest, err := FetchAttestationManifest(ctx, r.Identifier, r.ImageSpec.Platform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.AttestationManifest = attest
|
||||
}
|
||||
return ExtractEnvelopes(r.AttestationManifest, predicateType)
|
||||
}
|
||||
|
||||
func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Platform) (*attestation.AttestationManifest, error) {
|
||||
// we want to get to the image index, so ignoring platform for now
|
||||
options := WithOptions(ctx, nil)
|
||||
ref, err := name.ParseReference(image)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse reference: %w", err)
|
||||
}
|
||||
index, err := remote.Index(ref, options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get index: %w", err)
|
||||
}
|
||||
indexManifest, err := index.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get index manifest: %w", err)
|
||||
}
|
||||
subjectDescriptor, err := imageDescriptor(indexManifest, platform)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to obtain image for platform: %w", err)
|
||||
}
|
||||
|
||||
digest := subjectDescriptor.Digest.String()
|
||||
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), digest))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
||||
}
|
||||
|
||||
attestationDigest, err := attestationDigestForDigest(indexManifest, digest, "attestation-manifest")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to obtain attestation for image: %w", err)
|
||||
}
|
||||
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), attestationDigest))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
||||
}
|
||||
remoteDescriptor, err := remote.Get(ref, options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestation: %w", err)
|
||||
}
|
||||
attestationImage, err := remoteDescriptor.Image()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestation image: %w", err)
|
||||
}
|
||||
|
||||
layers, err := attestation.GetAttestationsFromImage(attestationImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
|
||||
}
|
||||
attest := &attestation.AttestationManifest{
|
||||
OriginalLayers: layers,
|
||||
OriginalDescriptor: &remoteDescriptor.Descriptor,
|
||||
SubjectName: image,
|
||||
SubjectDescriptor: subjectDescriptor,
|
||||
}
|
||||
return attest, nil
|
||||
}
|
||||
|
||||
@@ -3,15 +3,9 @@ package oci
|
||||
import (
|
||||
"context"
|
||||
|
||||
att "github.com/docker/attest/pkg/attestation"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
)
|
||||
|
||||
type AttestationResolver interface {
|
||||
ImageDetailsResolver
|
||||
Attestations(ctx context.Context, mediaType string) ([]*att.Envelope, error)
|
||||
}
|
||||
|
||||
type ImageDetailsResolver interface {
|
||||
ImageName(ctx context.Context) (string, error)
|
||||
ImagePlatform(ctx context.Context) (*v1.Platform, error)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -11,24 +13,20 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
OciReferenceTarget = "org.opencontainers.image.ref.name"
|
||||
OCIReferenceTarget = "org.opencontainers.image.ref.name"
|
||||
LocalPrefix = "oci://"
|
||||
RegistryPrefix = "docker://"
|
||||
OCI SourceType = "OCI"
|
||||
Docker SourceType = "Docker"
|
||||
)
|
||||
|
||||
type SourceType string
|
||||
type NamedIndex struct {
|
||||
Index v1.ImageIndex
|
||||
Name string
|
||||
}
|
||||
|
||||
type AttestationOptions struct {
|
||||
NoReferrers bool
|
||||
Attach bool
|
||||
ReferrersRepo string
|
||||
}
|
||||
type (
|
||||
SourceType string
|
||||
NamedIndex struct {
|
||||
Index v1.ImageIndex
|
||||
Name string
|
||||
}
|
||||
)
|
||||
|
||||
type ImageSpecOption func(*ImageSpec) error
|
||||
|
||||
@@ -50,7 +48,7 @@ func IndexFromPath(path string) (*NamedIndex, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get digest: %w", err)
|
||||
}
|
||||
imageName := idxm.Manifests[0].Annotations[OciReferenceTarget]
|
||||
imageName := idxm.Manifests[0].Annotations[OCIReferenceTarget]
|
||||
idxDigest := idxm.Manifests[0].Digest
|
||||
|
||||
idx, err := wrapperIdx.ImageIndex(idxDigest)
|
||||
@@ -83,9 +81,8 @@ func IndexFromRemote(image string) (*NamedIndex, error) {
|
||||
func LoadIndex(input *ImageSpec) (*NamedIndex, error) {
|
||||
if input.Type == OCI {
|
||||
return IndexFromPath(input.Identifier)
|
||||
} else {
|
||||
return IndexFromRemote(input.Identifier)
|
||||
}
|
||||
return IndexFromRemote(input.Identifier)
|
||||
}
|
||||
|
||||
func (i *ImageSpec) ForPlatforms(platform string) ([]*ImageSpec, error) {
|
||||
@@ -179,3 +176,42 @@ func WithoutTag(image string) (string, error) {
|
||||
repo := ref.Context().Name()
|
||||
return prefix + repo, nil
|
||||
}
|
||||
|
||||
type EmptyConfigImage struct {
|
||||
v1.Image
|
||||
}
|
||||
|
||||
func (i *EmptyConfigImage) RawConfigFile() ([]byte, error) {
|
||||
return []byte("{}"), nil
|
||||
}
|
||||
|
||||
func (i *EmptyConfigImage) Manifest() (*v1.Manifest, error) {
|
||||
mf, err := i.Image.Manifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
||||
}
|
||||
mf.Config = v1.Descriptor{
|
||||
MediaType: "application/vnd.oci.empty.v1+json",
|
||||
Size: 2,
|
||||
Digest: v1.Hash{Algorithm: "sha256", Hex: "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"},
|
||||
Data: []byte("{}"),
|
||||
}
|
||||
return mf, nil
|
||||
}
|
||||
|
||||
func (i *EmptyConfigImage) RawManifest() ([]byte, error) {
|
||||
mf, err := i.Manifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
||||
}
|
||||
return json.Marshal(mf)
|
||||
}
|
||||
|
||||
func (i *EmptyConfigImage) Digest() (v1.Hash, error) {
|
||||
mb, err := i.RawManifest()
|
||||
if err != nil {
|
||||
return v1.Hash{}, err
|
||||
}
|
||||
digest, _, err := v1.SHA256(bytes.NewReader(mb))
|
||||
return digest, err
|
||||
}
|
||||
|
||||
21
pkg/oci/types_test.go
Normal file
21
pkg/oci/types_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/util"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEmptyConfigImageDigest(t *testing.T) {
|
||||
empty := empty.Image
|
||||
img := EmptyConfigImage{Image: empty}
|
||||
mf, err := img.RawManifest()
|
||||
require.NoError(t, err)
|
||||
hash := util.SHA256Hex(mf)
|
||||
digest, err := img.Digest()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, digest.Hex, hash)
|
||||
}
|
||||
2
pkg/policy/README.md
Normal file
2
pkg/policy/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## policy
|
||||
This package is for attestation policy mapping and evaluation.
|
||||
@@ -2,29 +2,10 @@ package policy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
)
|
||||
|
||||
type policyEvaluatorCtxKeyType struct{}
|
||||
|
||||
var PolicyEvaluatorCtxKey policyEvaluatorCtxKeyType
|
||||
|
||||
// sets PolicyEvaluator in context
|
||||
func WithPolicyEvaluator(ctx context.Context, pe PolicyEvaluator) context.Context {
|
||||
return context.WithValue(ctx, PolicyEvaluatorCtxKey, pe)
|
||||
}
|
||||
|
||||
// gets PolicyEvaluator from context, defaults to Rego PolicyEvaluator if not set
|
||||
func GetPolicyEvaluator(ctx context.Context) (PolicyEvaluator, error) {
|
||||
t, ok := ctx.Value(PolicyEvaluatorCtxKey).(PolicyEvaluator)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no policy evaluator client set on context (set one with policy.WithPolicyEvaluator)")
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
type PolicyEvaluator interface {
|
||||
Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *PolicyInput) (*Result, error)
|
||||
type Evaluator interface {
|
||||
Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error)
|
||||
}
|
||||
|
||||
65
pkg/policy/match.go
Normal file
65
pkg/policy/match.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/attest/pkg/config"
|
||||
)
|
||||
|
||||
type matchType string
|
||||
|
||||
const (
|
||||
matchTypePolicy matchType = "policy"
|
||||
matchTypeMatchNoPolicy matchType = "match_no_policy"
|
||||
matchTypeNoMatch matchType = "no_match"
|
||||
)
|
||||
|
||||
type policyMatch struct {
|
||||
matchType matchType
|
||||
policy *config.PolicyMapping
|
||||
rule *config.PolicyRule
|
||||
matchedName string
|
||||
}
|
||||
|
||||
func findPolicyMatch(imageName string, mappings *config.PolicyMappings) (*policyMatch, error) {
|
||||
if mappings == nil {
|
||||
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
|
||||
}
|
||||
return findPolicyMatchImpl(imageName, mappings, make(map[*config.PolicyRule]bool))
|
||||
}
|
||||
|
||||
func findPolicyMatchImpl(imageName string, mappings *config.PolicyMappings, matched map[*config.PolicyRule]bool) (*policyMatch, error) {
|
||||
for _, rule := range mappings.Rules {
|
||||
if rule.Pattern.MatchString(imageName) {
|
||||
switch {
|
||||
case rule.PolicyID == "" && rule.Replacement == "":
|
||||
return nil, fmt.Errorf("rule %s has neither policy-id nor rewrite", rule.Pattern)
|
||||
case rule.PolicyID != "" && rule.Replacement != "":
|
||||
return nil, fmt.Errorf("rule %s has both policy-id and rewrite", rule.Pattern)
|
||||
case rule.PolicyID != "":
|
||||
policy := mappings.Policies[rule.PolicyID]
|
||||
if policy != nil {
|
||||
return &policyMatch{
|
||||
matchType: matchTypePolicy,
|
||||
policy: policy,
|
||||
rule: rule,
|
||||
matchedName: imageName,
|
||||
}, nil
|
||||
}
|
||||
return &policyMatch{
|
||||
matchType: matchTypeMatchNoPolicy,
|
||||
rule: rule,
|
||||
matchedName: imageName,
|
||||
}, nil
|
||||
case rule.Replacement != "":
|
||||
if matched[rule] {
|
||||
return nil, fmt.Errorf("rewrite loop detected")
|
||||
}
|
||||
matched[rule] = true
|
||||
imageName = rule.Pattern.ReplaceAllString(imageName, rule.Replacement)
|
||||
return findPolicyMatchImpl(imageName, mappings, matched)
|
||||
}
|
||||
}
|
||||
}
|
||||
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
|
||||
}
|
||||
@@ -112,7 +112,7 @@ func TestFindPolicyMatch(t *testing.T) {
|
||||
assert.Equal(t, tc.expectedMatchType, match.matchType)
|
||||
if match.matchType == matchTypePolicy {
|
||||
if assert.NotNil(t, match.policy) {
|
||||
assert.Equal(t, tc.expectedPolicyID, match.policy.Id)
|
||||
assert.Equal(t, tc.expectedPolicyID, match.policy.ID)
|
||||
}
|
||||
}
|
||||
assert.Equal(t, tc.expectedImageName, match.matchedName)
|
||||
@@ -3,23 +3,23 @@ package policy
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
)
|
||||
|
||||
type MockPolicyEvaluator struct {
|
||||
EvaluateFunc func(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *PolicyInput) (*Result, error)
|
||||
EvaluateFunc func(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error)
|
||||
}
|
||||
|
||||
func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *PolicyInput) (*Result, error) {
|
||||
func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error) {
|
||||
if pe.EvaluateFunc != nil {
|
||||
return pe.EvaluateFunc(ctx, resolver, pctx, input)
|
||||
}
|
||||
return AllowedResult(), nil
|
||||
}
|
||||
|
||||
func GetMockPolicy() PolicyEvaluator {
|
||||
func GetMockPolicy() Evaluator {
|
||||
return &MockPolicyEvaluator{
|
||||
EvaluateFunc: func(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *PolicyInput) (*Result, error) {
|
||||
EvaluateFunc: func(_ context.Context, _ attestation.Resolver, _ *Policy, _ *Input) (*Result, error) {
|
||||
return AllowedResult(), nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,238 +1,38 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/config"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
)
|
||||
|
||||
func resolveLocalPolicy(opts *PolicyOptions, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
|
||||
if opts.LocalPolicyDir == "" {
|
||||
return nil, fmt.Errorf("local policy dir not set")
|
||||
}
|
||||
files := make([]*PolicyFile, 0, len(mapping.Files))
|
||||
for _, f := range mapping.Files {
|
||||
filename := f.Path
|
||||
filePath := path.Join(opts.LocalPolicyDir, filename)
|
||||
fileContents, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read policy file %s: %w", filename, err)
|
||||
}
|
||||
files = append(files, &PolicyFile{
|
||||
Path: filename,
|
||||
Content: fileContents,
|
||||
})
|
||||
}
|
||||
policy := &Policy{
|
||||
InputFiles: files,
|
||||
Mapping: mapping,
|
||||
}
|
||||
if imageName != matchedName {
|
||||
policy.ResolvedName = matchedName
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func resolveTufPolicy(opts *PolicyOptions, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
|
||||
files := make([]*PolicyFile, 0, len(mapping.Files))
|
||||
for _, f := range mapping.Files {
|
||||
filename := f.Path
|
||||
_, fileContents, err := opts.TufClient.DownloadTarget(filename, filepath.Join(opts.LocalTargetsDir, filename))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download policy file %s: %w", filename, err)
|
||||
}
|
||||
files = append(files, &PolicyFile{
|
||||
Path: filename,
|
||||
Content: fileContents,
|
||||
})
|
||||
}
|
||||
policy := &Policy{
|
||||
InputFiles: files,
|
||||
Mapping: mapping,
|
||||
}
|
||||
if imageName != matchedName {
|
||||
policy.ResolvedName = matchedName
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
type matchType string
|
||||
|
||||
const (
|
||||
matchTypePolicy matchType = "policy"
|
||||
matchTypeMatchNoPolicy matchType = "match_no_policy"
|
||||
matchTypeNoMatch matchType = "no_match"
|
||||
)
|
||||
|
||||
type policyMatch struct {
|
||||
matchType matchType
|
||||
policy *config.PolicyMapping
|
||||
rule *config.PolicyRule
|
||||
matchedName string
|
||||
}
|
||||
|
||||
func findPolicyMatch(imageName string, mappings *config.PolicyMappings) (*policyMatch, error) {
|
||||
if mappings == nil {
|
||||
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
|
||||
}
|
||||
return findPolicyMatchImpl(imageName, mappings, make(map[*config.PolicyRule]bool))
|
||||
}
|
||||
|
||||
func findPolicyMatchImpl(imageName string, mappings *config.PolicyMappings, matched map[*config.PolicyRule]bool) (*policyMatch, error) {
|
||||
for _, rule := range mappings.Rules {
|
||||
if rule.Pattern.MatchString(imageName) {
|
||||
switch {
|
||||
case rule.PolicyId == "" && rule.Replacement == "":
|
||||
return nil, fmt.Errorf("rule %s has neither policy-id nor rewrite", rule.Pattern)
|
||||
case rule.PolicyId != "" && rule.Replacement != "":
|
||||
return nil, fmt.Errorf("rule %s has both policy-id and rewrite", rule.Pattern)
|
||||
case rule.PolicyId != "":
|
||||
policy := mappings.Policies[rule.PolicyId]
|
||||
if policy != nil {
|
||||
return &policyMatch{
|
||||
matchType: matchTypePolicy,
|
||||
policy: policy,
|
||||
rule: rule,
|
||||
matchedName: imageName,
|
||||
}, nil
|
||||
}
|
||||
return &policyMatch{
|
||||
matchType: matchTypeMatchNoPolicy,
|
||||
rule: rule,
|
||||
matchedName: imageName,
|
||||
}, nil
|
||||
case rule.Replacement != "":
|
||||
if matched[rule] {
|
||||
return nil, fmt.Errorf("rewrite loop detected")
|
||||
}
|
||||
matched[rule] = true
|
||||
imageName = rule.Pattern.ReplaceAllString(imageName, rule.Replacement)
|
||||
return findPolicyMatchImpl(imageName, mappings, matched)
|
||||
}
|
||||
}
|
||||
}
|
||||
return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil
|
||||
}
|
||||
|
||||
func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
|
||||
if opts.PolicyId != "" {
|
||||
localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
|
||||
}
|
||||
if localMappings != nil {
|
||||
policy := localMappings.Policies[opts.PolicyId]
|
||||
if policy != nil {
|
||||
return resolveLocalPolicy(opts, policy, "", "")
|
||||
}
|
||||
}
|
||||
|
||||
// must check tuf
|
||||
tufMappings, err := config.LoadTufMappings(opts.TufClient, opts.LocalTargetsDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err)
|
||||
}
|
||||
policy := tufMappings.Policies[opts.PolicyId]
|
||||
if policy != nil {
|
||||
return resolveTufPolicy(opts, policy, "", "")
|
||||
}
|
||||
return nil, fmt.Errorf("policy with id %s not found", opts.PolicyId)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver, opts *PolicyOptions) (*Policy, error) {
|
||||
p, err := resolvePolicyById(opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve policy by id: %w", err)
|
||||
}
|
||||
if p != nil {
|
||||
return p, nil
|
||||
}
|
||||
imageName, err := detailsResolver.ImageName(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get image name: %w", err)
|
||||
}
|
||||
imageName, err = normalizeImageName(imageName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse image name: %w", err)
|
||||
}
|
||||
localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
|
||||
}
|
||||
match, err := findPolicyMatch(imageName, localMappings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if match.matchType == matchTypePolicy {
|
||||
return resolveLocalPolicy(opts, match.policy, imageName, match.matchedName)
|
||||
}
|
||||
// must check tuf
|
||||
tufMappings, err := config.LoadTufMappings(opts.TufClient, opts.LocalTargetsDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err)
|
||||
}
|
||||
|
||||
// it's a mirror of a tuf policy
|
||||
if match.matchType == matchTypeMatchNoPolicy {
|
||||
for _, mapping := range tufMappings.Policies {
|
||||
if mapping.Id == match.rule.PolicyId {
|
||||
return resolveTufPolicy(opts, mapping, imageName, match.matchedName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try to resolve a tuf policy directly
|
||||
match, err = findPolicyMatch(imageName, tufMappings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if match.matchType == matchTypePolicy {
|
||||
return resolveTufPolicy(opts, match.policy, imageName, match.matchedName)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func normalizeImageName(imageName string) (string, error) {
|
||||
named, err := reference.ParseNormalizedNamed(imageName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse image name: %w", err)
|
||||
}
|
||||
return named.Name(), nil
|
||||
}
|
||||
|
||||
func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsResolver, error) {
|
||||
switch imageSource.Type {
|
||||
case oci.OCI:
|
||||
return oci.NewOCILayoutAttestationResolver(imageSource)
|
||||
return attestation.NewOCILayoutResolver(imageSource)
|
||||
case oci.Docker:
|
||||
return oci.NewRegistryImageDetailsResolver(imageSource)
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported image source type: %s", imageSource.Type)
|
||||
}
|
||||
|
||||
func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *config.PolicyMapping) (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)
|
||||
func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *config.PolicyMapping) (attestation.Resolver, error) {
|
||||
if mapping.Attestations != nil {
|
||||
if mapping.Attestations.Style == config.AttestationStyleAttached {
|
||||
switch resolver := resolver.(type) {
|
||||
case *oci.RegistryImageDetailsResolver:
|
||||
return attestation.NewRegistryResolver(resolver)
|
||||
case *attestation.LayoutResolver:
|
||||
return resolver, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver)
|
||||
}
|
||||
}
|
||||
case *oci.OCILayoutResolver:
|
||||
return resolver, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver)
|
||||
if mapping.Attestations.Repo != "" {
|
||||
return attestation.NewReferrersResolver(resolver, attestation.WithReferrersRepo(mapping.Attestations.Repo))
|
||||
}
|
||||
}
|
||||
return attestation.NewReferrersResolver(resolver)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/docker/attest/pkg/config"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/policy"
|
||||
"github.com/docker/attest/pkg/tuf"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -22,7 +21,7 @@ func loadAttestation(t *testing.T, path string) *attestation.Envelope {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var env = new(attestation.Envelope)
|
||||
env := new(attestation.Envelope)
|
||||
err = json.Unmarshal(ex, env)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -32,63 +31,61 @@ func loadAttestation(t *testing.T, path string) *attestation.Envelope {
|
||||
|
||||
func TestRegoEvaluator_Evaluate(t *testing.T) {
|
||||
ctx, _ := test.Setup(t)
|
||||
errorStr := "failed to resolve policy by id: policy with id non-existent-policy-id not found"
|
||||
resolveErrorStr := "failed to resolve policy by id: policy with id non-existent-policy-id not found"
|
||||
TestDataPath := filepath.Join("..", "..", "test", "testdata")
|
||||
ExampleAttestation := filepath.Join(TestDataPath, "example_attestation.json")
|
||||
|
||||
re := policy.NewRegoEvaluator(true)
|
||||
|
||||
defaultResolver := test.MockResolver{
|
||||
defaultResolver := attestation.MockResolver{
|
||||
Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
repo string
|
||||
expectSuccess bool
|
||||
isCanonical bool
|
||||
resolver oci.AttestationResolver
|
||||
policy *policy.PolicyOptions
|
||||
policyId string
|
||||
errorStr string
|
||||
policyPath string
|
||||
expectSuccess bool
|
||||
isCanonical bool
|
||||
resolver attestation.Resolver
|
||||
opts *policy.Options
|
||||
policyID string
|
||||
resolveErrorStr string
|
||||
}{
|
||||
{repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver},
|
||||
{repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver, policyId: "docker-official-images"},
|
||||
{repo: "testdata/mock-tuf-allow", expectSuccess: false, isCanonical: false, resolver: defaultResolver, policyId: "non-existent-policy-id", errorStr: errorStr},
|
||||
{repo: "testdata/mock-tuf-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},
|
||||
{policyPath: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver},
|
||||
{policyPath: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver, policyID: "docker-official-images"},
|
||||
{policyPath: "testdata/policies/allow", resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr},
|
||||
{policyPath: "testdata/policies/deny", resolver: defaultResolver},
|
||||
{policyPath: "testdata/policies/verify-sig", expectSuccess: true, resolver: defaultResolver},
|
||||
{policyPath: "testdata/policies/wrong-key", resolver: defaultResolver},
|
||||
{policyPath: "testdata/policies/allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver},
|
||||
{policyPath: "testdata/policies/allow-canonical", resolver: defaultResolver},
|
||||
{policyPath: "testdata/policies/no-rego", resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.repo, func(t *testing.T) {
|
||||
input := &policy.PolicyInput{
|
||||
Digest: "sha256:test-digest",
|
||||
Purl: "test-purl",
|
||||
IsCanonical: tc.isCanonical,
|
||||
t.Run(tc.policyPath, func(t *testing.T) {
|
||||
input := &policy.Input{
|
||||
Digest: "sha256:test-digest",
|
||||
PURL: "test-purl",
|
||||
}
|
||||
if !tc.isCanonical {
|
||||
input.Tag = "test"
|
||||
}
|
||||
|
||||
tufClient := tuf.NewMockTufClient(tc.repo, test.CreateTempDir(t, "", "tuf-dest"))
|
||||
if tc.policy == nil {
|
||||
tc.policy = &policy.PolicyOptions{
|
||||
TufClient: tufClient,
|
||||
if tc.opts == nil {
|
||||
tc.opts = &policy.Options{
|
||||
LocalTargetsDir: test.CreateTempDir(t, "", "tuf-targets"),
|
||||
PolicyId: tc.policyId,
|
||||
PolicyID: tc.policyID,
|
||||
LocalPolicyDir: tc.policyPath,
|
||||
DisableTUF: true,
|
||||
}
|
||||
}
|
||||
imageName, err := tc.resolver.ImageName(ctx)
|
||||
require.NoError(t, err)
|
||||
platform, err := tc.resolver.ImagePlatform(ctx)
|
||||
require.NoError(t, err)
|
||||
src, err := oci.ParseImageSpec(imageName, oci.WithPlatform(platform.String()))
|
||||
require.NoError(t, err)
|
||||
resolver, err := policy.CreateImageDetailsResolver(src)
|
||||
require.NoError(t, err)
|
||||
policy, err := policy.ResolvePolicy(ctx, resolver, tc.policy)
|
||||
if tc.errorStr != "" {
|
||||
resolver := policy.NewResolver(nil, tc.opts)
|
||||
policy, err := resolver.ResolvePolicy(ctx, imageName)
|
||||
if tc.resolveErrorStr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.errorStr)
|
||||
assert.Contains(t, err.Error(), tc.resolveErrorStr)
|
||||
return
|
||||
}
|
||||
require.NoErrorf(t, err, "failed to resolve policy")
|
||||
@@ -103,16 +100,76 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestLoadingMappings(t *testing.T) {
|
||||
policyMappings, err := config.LoadLocalMappings(filepath.Join("testdata", "mock-tuf-allow"))
|
||||
policyMappings, err := config.LoadLocalMappings(filepath.Join("testdata", "policies", "allow"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, len(policyMappings.Rules), 3)
|
||||
for _, mirror := range policyMappings.Rules {
|
||||
if mirror.PolicyId != "" {
|
||||
assert.Equal(t, "docker-official-images", mirror.PolicyId)
|
||||
if mirror.PolicyID != "" {
|
||||
assert.Equal(t, "docker-official-images", mirror.PolicyID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAttestationResolver(t *testing.T) {
|
||||
mockResolver := attestation.MockResolver{
|
||||
Envs: []*attestation.Envelope{},
|
||||
}
|
||||
layoutResolver := &attestation.LayoutResolver{}
|
||||
registryResolver := &oci.RegistryImageDetailsResolver{}
|
||||
|
||||
nilRepoReferrers := &config.PolicyMapping{
|
||||
Attestations: &config.AttestationConfig{
|
||||
Style: config.AttestationStyleReferrers,
|
||||
},
|
||||
}
|
||||
referrers := &config.PolicyMapping{
|
||||
Attestations: &config.AttestationConfig{
|
||||
Repo: "localhost:5000/repo",
|
||||
Style: config.AttestationStyleReferrers,
|
||||
},
|
||||
}
|
||||
attached := &config.PolicyMapping{
|
||||
Attestations: &config.AttestationConfig{
|
||||
Style: config.AttestationStyleAttached,
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
resolver oci.ImageDetailsResolver
|
||||
mapping *config.PolicyMapping
|
||||
errorStr string
|
||||
}{
|
||||
{name: "referrers", resolver: layoutResolver, mapping: referrers},
|
||||
{name: "referrers (no mapped repo)", resolver: layoutResolver, mapping: nilRepoReferrers},
|
||||
{name: "referrers (no mapping)", resolver: layoutResolver, mapping: &config.PolicyMapping{Attestations: nil}},
|
||||
{name: "attached (registry)", resolver: registryResolver, mapping: attached},
|
||||
{name: "attached (layout)", resolver: layoutResolver, mapping: attached},
|
||||
{name: "attached (unsupported)", resolver: mockResolver, mapping: attached, errorStr: "unsupported image details resolver type"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
resolver, err := policy.CreateAttestationResolver(tc.resolver, tc.mapping)
|
||||
if tc.errorStr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
assert.Contains(t, err.Error(), tc.errorStr)
|
||||
}
|
||||
if tc.mapping.Attestations == nil {
|
||||
return
|
||||
}
|
||||
switch resolver.(type) {
|
||||
case *attestation.ReferrersResolver:
|
||||
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleReferrers)
|
||||
case *attestation.RegistryResolver:
|
||||
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached)
|
||||
case *attestation.LayoutResolver:
|
||||
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
att "github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
@@ -30,13 +29,13 @@ const (
|
||||
resultBinding = "result"
|
||||
)
|
||||
|
||||
func NewRegoEvaluator(debug bool) PolicyEvaluator {
|
||||
func NewRegoEvaluator(debug bool) Evaluator {
|
||||
return ®oEvaluator{
|
||||
debug: debug,
|
||||
}
|
||||
}
|
||||
|
||||
func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *PolicyInput) (*Result, error) {
|
||||
func (re *regoEvaluator) Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error) {
|
||||
var regoOpts []func(*rego.Rego)
|
||||
|
||||
// Create a new in-memory store
|
||||
@@ -113,7 +112,7 @@ func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationR
|
||||
}
|
||||
|
||||
func jsonGenerator[T any]() func(t *ast.Term, ec *rego.EvalContext) (any, error) {
|
||||
return func(t *ast.Term, ec *rego.EvalContext) (any, error) {
|
||||
return func(t *ast.Term, _ *rego.EvalContext) (any, error) {
|
||||
// TODO: this is horrible - we're converting the AST to JSON and then back to AST, then using ast.As to convert it to a struct
|
||||
// We can't use ast.As directly because it fails if the AST contains a set
|
||||
json, err := ast.JSON(t.Value)
|
||||
@@ -140,6 +139,7 @@ var verifyDecl = &ast.Builtin{
|
||||
Decl: types.NewFunction(types.Args(dynamicObj, dynamicObj), dynamicObj),
|
||||
Nondeterministic: true,
|
||||
}
|
||||
|
||||
var attestDecl = &ast.Builtin{
|
||||
Name: "attest.fetch",
|
||||
Decl: types.NewFunction(types.Args(types.S), dynamicObj),
|
||||
@@ -163,13 +163,13 @@ func handleErrors1(f func(rCtx rego.BuiltinContext, a *ast.Term) (*ast.Term, err
|
||||
}
|
||||
}
|
||||
|
||||
func handleErrors2(f func(rCtx rego.BuiltinContext, a, b *ast.Term) (*ast.Term, error)) rego.Builtin2 {
|
||||
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))
|
||||
return wrapFunctionResult(f(&rCtx, a, b))
|
||||
}
|
||||
}
|
||||
|
||||
func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin {
|
||||
func RegoFunctions(resolver attestation.Resolver) []*tester.Builtin {
|
||||
return []*tester.Builtin{
|
||||
{
|
||||
Decl: verifyDecl,
|
||||
@@ -180,7 +180,7 @@ func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin {
|
||||
Memoize: true,
|
||||
Nondeterministic: verifyDecl.Nondeterministic,
|
||||
},
|
||||
handleErrors2(verifyIntotoEnvelope)),
|
||||
handleErrors2(verifyInTotoEnvelope)),
|
||||
},
|
||||
{
|
||||
Decl: attestDecl,
|
||||
@@ -191,12 +191,12 @@ func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin {
|
||||
Memoize: true,
|
||||
Nondeterministic: attestDecl.Nondeterministic,
|
||||
},
|
||||
handleErrors1(fetchIntotoAttestations(resolver))),
|
||||
handleErrors1(fetchInTotoAttestations(resolver))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func fetchIntotoAttestations(resolver oci.AttestationResolver) rego.Builtin1 {
|
||||
func fetchInTotoAttestations(resolver attestation.Resolver) rego.Builtin1 {
|
||||
return func(rCtx rego.BuiltinContext, predicateTypeTerm *ast.Term) (*ast.Term, error) {
|
||||
predicateTypeStr, ok := predicateTypeTerm.Value.(ast.String)
|
||||
if !ok {
|
||||
@@ -226,9 +226,9 @@ func fetchIntotoAttestations(resolver oci.AttestationResolver) rego.Builtin1 {
|
||||
}
|
||||
}
|
||||
|
||||
func verifyIntotoEnvelope(rCtx rego.BuiltinContext, envTerm, optsTerm *ast.Term) (*ast.Term, error) {
|
||||
env := new(att.Envelope)
|
||||
opts := new(att.VerifyOptions)
|
||||
func verifyInTotoEnvelope(rCtx *rego.BuiltinContext, envTerm, optsTerm *ast.Term) (*ast.Term, error) {
|
||||
env := new(attestation.Envelope)
|
||||
opts := new(attestation.VerifyOptions)
|
||||
err := ast.As(envTerm.Value, env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to cast envelope: %w", err)
|
||||
@@ -238,7 +238,7 @@ func verifyIntotoEnvelope(rCtx rego.BuiltinContext, envTerm, optsTerm *ast.Term)
|
||||
return nil, fmt.Errorf("failed to cast verifier options: %w", err)
|
||||
}
|
||||
|
||||
payload, err := att.VerifyDSSE(rCtx.Context, env, opts)
|
||||
payload, err := attestation.VerifyDSSE(rCtx.Context, env, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
194
pkg/policy/resolver.go
Normal file
194
pkg/policy/resolver.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/attest/internal/util"
|
||||
"github.com/docker/attest/pkg/config"
|
||||
"github.com/docker/attest/pkg/tuf"
|
||||
)
|
||||
|
||||
type Resolver struct {
|
||||
tufClient tuf.Downloader
|
||||
opts *Options
|
||||
}
|
||||
|
||||
func NewResolver(tufClient tuf.Downloader, opts *Options) *Resolver {
|
||||
return &Resolver{
|
||||
tufClient: tufClient,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resolver) ResolvePolicy(_ context.Context, imageName string) (*Policy, error) {
|
||||
p, err := r.resolvePolicyByID()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve policy by id: %w", err)
|
||||
}
|
||||
if p != nil {
|
||||
return p, nil
|
||||
}
|
||||
imageName, err = normalizeImageName(imageName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse image name: %w", err)
|
||||
}
|
||||
localMappings, err := config.LoadLocalMappings(r.opts.LocalPolicyDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
|
||||
}
|
||||
match, err := findPolicyMatch(imageName, localMappings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if match.matchType == matchTypePolicy {
|
||||
return r.resolveLocalPolicy(match.policy, imageName, match.matchedName)
|
||||
}
|
||||
if !r.opts.DisableTUF {
|
||||
tufMappings, err := config.LoadTUFMappings(r.tufClient, r.opts.LocalTargetsDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err)
|
||||
}
|
||||
|
||||
// it's a mirror of a tuf policy
|
||||
if match.matchType == matchTypeMatchNoPolicy {
|
||||
for _, mapping := range tufMappings.Policies {
|
||||
if mapping.ID == match.rule.PolicyID {
|
||||
return r.resolveTUFPolicy(mapping, imageName, match.matchedName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try to resolve a tuf policy directly
|
||||
match, err = findPolicyMatch(imageName, tufMappings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if match.matchType == matchTypePolicy {
|
||||
return r.resolveTUFPolicy(match.policy, imageName, match.matchedName)
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) resolveLocalPolicy(mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
|
||||
if r.opts.LocalPolicyDir == "" {
|
||||
return nil, fmt.Errorf("local policy dir not set")
|
||||
}
|
||||
var URI string
|
||||
var digest map[string]string
|
||||
files := make([]*File, 0, len(mapping.Files))
|
||||
for _, f := range mapping.Files {
|
||||
filename := f.Path
|
||||
filePath := path.Join(r.opts.LocalPolicyDir, filename)
|
||||
fileContents, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read policy file %s: %w", filename, err)
|
||||
}
|
||||
files = append(files, &File{
|
||||
Path: filename,
|
||||
Content: fileContents,
|
||||
})
|
||||
// if the file is a policy file, store the URI and digest
|
||||
if filepath.Ext(filename) == ".rego" {
|
||||
// TODO: support multiple rego files, need some way to identify the main policy file
|
||||
if URI != "" {
|
||||
return nil, fmt.Errorf("multiple policy files found in policy mapping")
|
||||
}
|
||||
URI = filePath
|
||||
digest = map[string]string{"sha256": util.SHA256Hex(fileContents)}
|
||||
}
|
||||
}
|
||||
if URI == "" {
|
||||
return nil, fmt.Errorf("no policy file found in policy mapping")
|
||||
}
|
||||
policy := &Policy{
|
||||
InputFiles: files,
|
||||
Mapping: mapping,
|
||||
URI: URI,
|
||||
Digest: digest,
|
||||
}
|
||||
if imageName != matchedName {
|
||||
policy.ResolvedName = matchedName
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) resolveTUFPolicy(mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
|
||||
var URI string
|
||||
var digest map[string]string
|
||||
files := make([]*File, 0, len(mapping.Files))
|
||||
for _, f := range mapping.Files {
|
||||
filename := f.Path
|
||||
file, err := r.tufClient.DownloadTarget(filename, filepath.Join(r.opts.LocalTargetsDir, filename))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download policy file %s: %w", filename, err)
|
||||
}
|
||||
files = append(files, &File{
|
||||
Path: filename,
|
||||
Content: file.Data,
|
||||
})
|
||||
// if the file is a policy file, store the URI and digest
|
||||
if filepath.Ext(filename) == ".rego" {
|
||||
// TODO: support multiple rego files, need some way to identify the main policy file
|
||||
if URI != "" {
|
||||
return nil, fmt.Errorf("multiple policy files found in policy mapping")
|
||||
}
|
||||
URI = file.TargetURI
|
||||
digest = map[string]string{"sha256": file.Digest}
|
||||
}
|
||||
}
|
||||
if URI == "" {
|
||||
return nil, fmt.Errorf("no policy file found in policy mapping")
|
||||
}
|
||||
policy := &Policy{
|
||||
InputFiles: files,
|
||||
Mapping: mapping,
|
||||
URI: URI,
|
||||
Digest: digest,
|
||||
}
|
||||
if imageName != matchedName {
|
||||
policy.ResolvedName = matchedName
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) resolvePolicyByID() (*Policy, error) {
|
||||
if r.opts.PolicyID != "" {
|
||||
localMappings, err := config.LoadLocalMappings(r.opts.LocalPolicyDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
|
||||
}
|
||||
if localMappings != nil {
|
||||
policy := localMappings.Policies[r.opts.PolicyID]
|
||||
if policy != nil {
|
||||
return r.resolveLocalPolicy(policy, "", "")
|
||||
}
|
||||
}
|
||||
|
||||
if !r.opts.DisableTUF {
|
||||
tufMappings, err := config.LoadTUFMappings(r.tufClient, r.opts.LocalTargetsDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err)
|
||||
}
|
||||
policy := tufMappings.Policies[r.opts.PolicyID]
|
||||
if policy != nil {
|
||||
return r.resolveTUFPolicy(policy, "", "")
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("policy with id %s not found", r.opts.PolicyID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func normalizeImageName(imageName string) (string, error) {
|
||||
named, err := reference.ParseNormalizedNamed(imageName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse image name: %w", err)
|
||||
}
|
||||
return named.Name(), nil
|
||||
}
|
||||
65
pkg/policy/resolver_test.go
Normal file
65
pkg/policy/resolver_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package policy_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/pkg/policy"
|
||||
"github.com/docker/attest/pkg/tuf"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResolvePolicy(t *testing.T) {
|
||||
localPolicyPath := "testdata/policies/allow"
|
||||
tufPolicyPath := "testdata/policies/allow-canonical"
|
||||
noLocalPolicyPath := "testdata/policies/no-policy"
|
||||
testPolicyID := "docker-official-images"
|
||||
testImageName := "localhost:5001/test/repo:tag"
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
policyPath string
|
||||
policyID string
|
||||
localOverridesTUF bool // if a policy is provided locally, it should override TUF
|
||||
DisableTUF bool
|
||||
}{
|
||||
{name: "resolve by id (TUF only)", policyID: testPolicyID, DisableTUF: false},
|
||||
{name: "resolve by id (local mapping, TUF policy)", policyPath: noLocalPolicyPath, policyID: testPolicyID, DisableTUF: false},
|
||||
{name: "resolve by id (local mapping, local policy, no TUF)", policyPath: localPolicyPath, policyID: testPolicyID, DisableTUF: true},
|
||||
{name: "resolve by id (local mapping, local policy)", policyPath: localPolicyPath, policyID: testPolicyID, DisableTUF: false, localOverridesTUF: true},
|
||||
{name: "resolve by match (TUF only)", DisableTUF: false},
|
||||
{name: "resolve by match (local mapping, TUF policy)", policyPath: noLocalPolicyPath, DisableTUF: false},
|
||||
{name: "resolve by match (local mapping, local policy, no TUF)", policyPath: localPolicyPath, DisableTUF: true},
|
||||
{name: "resolve by match (local mapping, local policy)", policyPath: localPolicyPath, DisableTUF: false, localOverridesTUF: true},
|
||||
}
|
||||
|
||||
var tufClient tuf.Downloader
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
opts := &policy.Options{}
|
||||
tempDir := test.CreateTempDir(t, "", "tuf-dest")
|
||||
if !tc.DisableTUF {
|
||||
tufClient = tuf.NewMockTufClient(tufPolicyPath)
|
||||
}
|
||||
if tc.policyID != "" {
|
||||
opts.PolicyID = tc.policyID
|
||||
}
|
||||
if tc.policyPath != "" {
|
||||
opts.LocalPolicyDir = tc.policyPath
|
||||
}
|
||||
opts.DisableTUF = tc.DisableTUF
|
||||
opts.LocalTargetsDir = tempDir
|
||||
resolver := policy.NewResolver(tufClient, opts)
|
||||
policy, err := resolver.ResolvePolicy(context.Background(), testImageName)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, policy)
|
||||
if tc.DisableTUF || tc.localOverridesTUF {
|
||||
assert.Contains(t, policy.URI, localPolicyPath)
|
||||
} else {
|
||||
assert.Contains(t, policy.URI, tufPolicyPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package attest
|
||||
|
||||
import rego.v1
|
||||
|
||||
result := {
|
||||
"success": input.isCanonical,
|
||||
}
|
||||
11
pkg/policy/testdata/policies/allow-canonical/doi/policy.rego
vendored
Normal file
11
pkg/policy/testdata/policies/allow-canonical/doi/policy.rego
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
package attest
|
||||
|
||||
import rego.v1
|
||||
|
||||
default canonical = false
|
||||
|
||||
canonical if {
|
||||
not input.tag
|
||||
}
|
||||
|
||||
result := {"success": canonical}
|
||||
10
pkg/policy/testdata/policies/no-policy/mapping.yaml
vendored
Normal file
10
pkg/policy/testdata/policies/no-policy/mapping.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# map repos to policies
|
||||
version: v1
|
||||
kind: policy-mapping
|
||||
rules:
|
||||
- pattern: "^docker[.]io/library/(.*)$"
|
||||
policy-id: docker-official-images
|
||||
- pattern: ^localhost:5001/(.*)$
|
||||
rewrite: docker.io/library/$1
|
||||
- pattern: ^registry[.]local:5000/(.*)$
|
||||
rewrite: docker.io/library/$1
|
||||
1
pkg/policy/testdata/policies/no-rego/doi/policy.yaml
vendored
Normal file
1
pkg/policy/testdata/policies/no-rego/doi/policy.yaml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
policy: "this is not rego"
|
||||
11
pkg/policy/testdata/policies/no-rego/mapping.yaml
vendored
Normal file
11
pkg/policy/testdata/policies/no-rego/mapping.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# map repos to policies
|
||||
version: v1
|
||||
kind: policy-mapping
|
||||
policies:
|
||||
- id: docker-official-images
|
||||
description: Docker Official Images
|
||||
files:
|
||||
- path: doi/policy.yaml
|
||||
rules:
|
||||
- pattern: "^docker[.]io/library/(.*)$"
|
||||
policy-id: docker-official-images
|
||||
@@ -26,29 +26,37 @@ type Result struct {
|
||||
Summary Summary `json:"summary"`
|
||||
}
|
||||
|
||||
type PolicyOptions struct {
|
||||
TufClient tuf.TUFClient
|
||||
type Options struct {
|
||||
TUFClientOptions *tuf.ClientOptions
|
||||
DisableTUF bool
|
||||
LocalTargetsDir string
|
||||
LocalPolicyDir string
|
||||
PolicyId string
|
||||
PolicyID string
|
||||
ReferrersRepo string
|
||||
AttestationStyle config.AttestationStyle
|
||||
Debug bool
|
||||
}
|
||||
|
||||
type Policy struct {
|
||||
InputFiles []*PolicyFile
|
||||
InputFiles []*File
|
||||
Query string
|
||||
Mapping *config.PolicyMapping
|
||||
ResolvedName string
|
||||
URI string
|
||||
Digest map[string]string
|
||||
}
|
||||
|
||||
type PolicyInput struct {
|
||||
Digest string `json:"digest"`
|
||||
Purl string `json:"purl"`
|
||||
IsCanonical bool `json:"isCanonical"`
|
||||
type Input struct {
|
||||
Digest string `json:"digest"`
|
||||
PURL string `json:"purl"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Domain string `json:"domain"`
|
||||
NormalizedName string `json:"normalized_name"`
|
||||
FamiliarName string `json:"familiar_name"`
|
||||
Platform string `json:"platform"`
|
||||
}
|
||||
|
||||
type PolicyFile struct {
|
||||
type File struct {
|
||||
Path string
|
||||
Content []byte
|
||||
}
|
||||
|
||||
2
pkg/signerverifier/README.md
Normal file
2
pkg/signerverifier/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## signerverifier
|
||||
This package implements methods to sign and verify attestation envelopes.
|
||||
@@ -9,18 +9,18 @@ import (
|
||||
awssigner "github.com/sigstore/sigstore/pkg/signature/kms/aws"
|
||||
)
|
||||
|
||||
// using AWS KMS
|
||||
func GetAWSSigner(ctx context.Context, keyArn string, region string) (dsse.SignerVerifier, error) {
|
||||
keypath := fmt.Sprintf("awskms:///%s", keyArn)
|
||||
sv, err := awssigner.LoadSignerVerifier(ctx, keypath, config.WithRegion(region))
|
||||
// using AWS KMS.
|
||||
func GetAWSSigner(ctx context.Context, keyARN string, region string) (dsse.SignerVerifier, error) {
|
||||
keyPath := fmt.Sprintf("awskms:///%s", keyARN)
|
||||
sv, err := awssigner.LoadSignerVerifier(ctx, keyPath, config.WithRegion(region))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error loading aws signer verifier: %w", err)
|
||||
}
|
||||
cs, _, err := sv.CryptoSigner(context.Background(), func(err error) {})
|
||||
cs, _, err := sv.CryptoSigner(context.Background(), func(_ error) {})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting aws crypto signer: %w", err)
|
||||
}
|
||||
signer := &ECDSA256_SignerVerifier{
|
||||
signer := &ECDSA256SignerVerifier{
|
||||
Signer: cs,
|
||||
}
|
||||
return signer, nil
|
||||
|
||||
@@ -14,12 +14,12 @@ import (
|
||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||
)
|
||||
|
||||
type ECDSA256_SignerVerifier struct {
|
||||
type ECDSA256SignerVerifier struct {
|
||||
crypto.Signer
|
||||
}
|
||||
|
||||
// implement keyid function
|
||||
func (s *ECDSA256_SignerVerifier) KeyID() (string, error) {
|
||||
// implement keyid function.
|
||||
func (s *ECDSA256SignerVerifier) KeyID() (string, error) {
|
||||
keyid, err := KeyID(s.Signer.Public())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error getting keyid: %w", err)
|
||||
@@ -27,15 +27,15 @@ func (s *ECDSA256_SignerVerifier) KeyID() (string, error) {
|
||||
return keyid, nil
|
||||
}
|
||||
|
||||
func (s *ECDSA256_SignerVerifier) Public() crypto.PublicKey {
|
||||
func (s *ECDSA256SignerVerifier) Public() crypto.PublicKey {
|
||||
return s.Signer.Public()
|
||||
}
|
||||
|
||||
func (s *ECDSA256_SignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, error) {
|
||||
func (s *ECDSA256SignerVerifier) Sign(_ context.Context, data []byte) ([]byte, error) {
|
||||
return s.Signer.Sign(rand.Reader, data, crypto.SHA256)
|
||||
}
|
||||
|
||||
func (s *ECDSA256_SignerVerifier) Verify(ctx context.Context, data []byte, sig []byte) error {
|
||||
func (s *ECDSA256SignerVerifier) Verify(_ context.Context, data []byte, sig []byte) error {
|
||||
pub, ok := s.Signer.Public().(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("public key is not ecdsa")
|
||||
@@ -52,7 +52,7 @@ func LoadKeyPair(priv []byte) (dsse.SignerVerifier, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ECDSA256_SignerVerifier{
|
||||
return &ECDSA256SignerVerifier{
|
||||
Signer: privateKey,
|
||||
}, nil
|
||||
}
|
||||
@@ -78,7 +78,7 @@ func GenKeyPair() (dsse.SignerVerifier, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ECDSA256_SignerVerifier{
|
||||
return &ECDSA256SignerVerifier{
|
||||
Signer: signer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -10,18 +10,18 @@ import (
|
||||
)
|
||||
|
||||
// using GCP KMS
|
||||
// reference should be in the format projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING]/cryptoKeys/[KEY]/cryptoKeyVersions/[VERSION]
|
||||
// reference should be in the format projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING]/cryptoKeys/[KEY]/cryptoKeyVersions/[VERSION].
|
||||
func GetGCPSigner(ctx context.Context, reference string, opts ...option.ClientOption) (dsse.SignerVerifier, error) {
|
||||
reference = fmt.Sprintf("gcpkms://%s", reference)
|
||||
sv, err := gcpsigner.LoadSignerVerifier(ctx, reference, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error loading gcp signer verifier: %w", err)
|
||||
}
|
||||
cs, _, err := sv.CryptoSigner(ctx, func(err error) {})
|
||||
cs, _, err := sv.CryptoSigner(ctx, func(_ error) {})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting gcp crypto signer: %w", err)
|
||||
}
|
||||
signer := &ECDSA256_SignerVerifier{
|
||||
signer := &ECDSA256SignerVerifier{
|
||||
Signer: cs,
|
||||
}
|
||||
return signer, nil
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestGCPKMS_Signer(t *testing.T) {
|
||||
keyId, err := signer.KeyID()
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, keyId)
|
||||
publicKey, err := Parse([]byte(publicKeyPEM))
|
||||
publicKey, err := ParsePublicKey([]byte(publicKeyPEM))
|
||||
require.NoError(t, err)
|
||||
// verify payload ecdsa signature
|
||||
ok := ecdsa.VerifyASN1(publicKey, hash, sig)
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
func KeyID(pubKey crypto.PublicKey) (string, error) {
|
||||
pub, err := x509.MarshalPKIXPublicKey(pubKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error marshalling public key: %w", err)
|
||||
return "", fmt.Errorf("error marshaling public key: %w", err)
|
||||
}
|
||||
return util.SHA256Hex(pub), nil
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
const pemType = "PUBLIC KEY"
|
||||
|
||||
func Parse(pubkeyBytes []byte) (*ecdsa.PublicKey, error) {
|
||||
func ParsePublicKey(pubkeyBytes []byte) (*ecdsa.PublicKey, error) {
|
||||
p, _ := pem.Decode(pubkeyBytes)
|
||||
if p == nil {
|
||||
return nil, fmt.Errorf("pubkey file does not contain any PEM data")
|
||||
@@ -29,7 +29,7 @@ func Parse(pubkeyBytes []byte) (*ecdsa.PublicKey, error) {
|
||||
return ecdsaPubKey, nil
|
||||
}
|
||||
|
||||
func ToPEM(ecdsaPubKey *ecdsa.PublicKey) ([]byte, error) {
|
||||
func ConvertToPEM(ecdsaPubKey *ecdsa.PublicKey) ([]byte, error) {
|
||||
pubKeyBytes, err := x509.MarshalPKIXPublicKey(ecdsaPubKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error failed to marshal public key: %w", err)
|
||||
|
||||
2
pkg/tlog/README.md
Normal file
2
pkg/tlog/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## tlog
|
||||
This package implements transparency logging.
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
USE_MOCK_TL = true
|
||||
UseMockTL = true
|
||||
|
||||
TestEntry = `{"body":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI5Zjg2ZDA4MTg4NGM3ZDY1OWEyZmVhYTBjNTVhZDAxNWEzYmY0ZjFiMmIwYjgyMmNkMTVkNmMxNWIwZjAwYTA4In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJQUlyVUZGUzBIYmNzZjc5L08yajVXdHl2R2Vvd1NVSXpZcDlBM2IwWnREVUFpQVQxZU42ZjFyVmVWa011REFlN3dxWkJ2bE5LY2VsajNVVDNmaWhyQjZSY2c9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVSlZla05DSzJGQlJFRm5SVU5CWjBWQ1RVRnZSME5EY1VkVFRUUTVRa0ZOUTAxQk9IaEVWRUZNUW1kT1ZrSkJUVlJDU0ZKc1l6TlJkMGhvWTA0S1RXcE5lRTFxU1ROTlZHdDVUWHBWTlZkb1kwNU5hbEY0VFdwSk1rMVVhM2xOZWxVMVYycEJVRTFSTUhkRGQxbEVWbEZSUkVWM1VqQmFXRTR3VFVacmR3cEZkMWxJUzI5YVNYcHFNRU5CVVZsSlMyOWFTWHBxTUVSQlVXTkVVV2RCUlVRMFZpdFNSV2g0SzJGeFYwZzNlV3hOVFVSSVlXaE9UVzVOVEZOUFNsQXZDamxyUVcwNWJIQXJNMjF4V1ZSQmFGVlNjbUUyVDBRMVVYZzRXbUprSzJWMVVIbFFhemw1SzNjdloxZEhSRUk1ZW00dlNXd3hTMDVIVFVWUmQwUm5XVVFLVmxJd1VFRlJTQzlDUVZGRVFXZGxRVTFDVFVkQk1WVmtTbEZSVFUxQmIwZERRM05IUVZGVlJrSjNUVVJOUVhkSFFURlZaRVYzUlVJdmQxRkRUVUZCZHdwRWQxbEVWbEl3VWtKQlozZENiMGxGWkVkV2VtUkVRVXRDWjJkeGFHdHFUMUJSVVVSQlowNUtRVVJDUjBGcFJVRTNOMjFFTDFSbVJtRlJVemxrWlhRMENqbFhaRk41YURKT1VTOUZiMVJtYVVGdFFtaHVWblpEVTNSUVowTkpVVU1yZDNSdllpOU9iMUp4T0c5cU4wZDNibTVKYUZKVGRDOVJNbmtyVXpoUkwzSUthRkpVYW5GaE9HZExRVDA5Q2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIn19fX0=","integratedTime":1703705039,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d","logIndex":59674396,"verification":{"inclusionProof":{"checkpoint":"rekor.sigstore.dev - 2605736670972794746\n55510966\nJCi1O53Xmdi9lXnui4Q5SQ+MJSMnWr1Bxn+Q2Qf22tU=\nTimestamp: 1703705040158839214\n\n— rekor.sigstore.dev wNI9ajBFAiAXgtjFDVqCSgiSP04TQzELrz4+EyBwyYVL2EEULTCy0AIhAI9peLU76ZUD1tvU8qvzBJBo77IYD1rc+A1MPc35AeVK\n","hashes":["fb77ee213b48f4b18dc81c6e634c570abf99b257713561f174f2e0f4c039af67","6cb113bbefadecbbb8b89b1c08232438a6125071790b6a062cff8c1ccfdcb91e","6fbe1424e264e4590ca502d671b7a036c87f7a90d1f57534b98eb781144160bf","077b606720a6478200f6c3ed08a68e9b01b1cae192cb120888ddcc95521601bd","b6f8e8bc21ae0cde82b92422a4b4f37b28a43185821e468a4e65b6c79ed8f5b7","89332533fac54e9bc68c7353c42f6ebb9fe38039f67910332ff95082072068d4","0814d6f707a75fb3334bab14ab5466bd8b9a64ae7be7cd4d53a428c64932bc66","e883e826f10329c63a4a2ed21156037a050df43b9d74079296beac6968ed4150","d79230703257b7e4a8a61b032b6980d1a0bdbc7ae96ca838b525b3751785fe48","2f4a77e5288462cd3b75084d37f1502dcbe0943d18dd95cb247fc1ebbabc0aad","38562c253d3536d0d00e3547c880b6b0251a25ac69605b50c9eaa1a27186cc7a","9dea192350ff8b3c0f5ccda38261cb38ebd61869281c3928912332d1144e0a04","2c4d25ba59aa573ab2c79c2d3cd9e1d74789b10632432724d63112ce50b44874","98c486feb5d87092a78a46c4b5be04868654900affc2e86ffb20074dc73a883a","6969c49bd73f19bf28a5eaeabd331ddd60502defb2cd3d96e17b741c80adec6c"],"logIndex":55510965,"rootHash":"2428b53b9dd799d8bd9579ee8b8439490f8c2523275abd41c67f90d907f6dad5","treeSize":55510966},"signedEntryTimestamp":"MEUCIQCG9PRI8PcvtJyE9pbcculZipze6NEWR1Nk8EYocto3BwIgYu5gqgjW80HMjSjUxUNJLp0wlVTesnJCeByUBySc59w="}}`
|
||||
)
|
||||
@@ -26,10 +26,10 @@ func GetMockTL() TL {
|
||||
}
|
||||
|
||||
return &MockTL{
|
||||
UploadLogEntryFunc: func(ctx context.Context, subject string, payload []byte, signature []byte, signer dsse.SignerVerifier) ([]byte, error) {
|
||||
UploadLogEntryFunc: func(_ context.Context, _ string, _ []byte, _ []byte, _ dsse.SignerVerifier) ([]byte, error) {
|
||||
return []byte(TestEntry), nil
|
||||
},
|
||||
VerifyLogEntryFunc: func(ctx context.Context, entryBytes []byte) (time.Time, error) {
|
||||
VerifyLogEntryFunc: func(_ context.Context, entryBytes []byte) (time.Time, error) {
|
||||
// return the integrated time in the log entry without any checking
|
||||
le, err := unmarshalEntry(entryBytes)
|
||||
if err != nil {
|
||||
@@ -40,7 +40,7 @@ func GetMockTL() TL {
|
||||
}
|
||||
return time.Unix(*le.IntegratedTime, 0), nil
|
||||
},
|
||||
VerifyEntryPayloadFunc: func(entryBytes, payload, pkToken []byte) error {
|
||||
VerifyEntryPayloadFunc: func(_, _, _ []byte) error {
|
||||
return nil
|
||||
},
|
||||
UnmarshalEntryFunc: func(entry []byte) (any, error) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tlog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
@@ -31,23 +32,23 @@ const (
|
||||
|
||||
type tlCtxKeyType struct{}
|
||||
|
||||
var TlCtxKey tlCtxKeyType
|
||||
var TLCtxKey tlCtxKeyType
|
||||
|
||||
// sets TL in context
|
||||
// sets TL in context.
|
||||
func WithTL(ctx context.Context, tl TL) context.Context {
|
||||
return context.WithValue(ctx, TlCtxKey, tl)
|
||||
return context.WithValue(ctx, TLCtxKey, tl)
|
||||
}
|
||||
|
||||
// gets TL from context, defaults to Rekor TL if not set
|
||||
// gets TL from context, defaults to Rekor TL if not set.
|
||||
func GetTL(ctx context.Context) TL {
|
||||
t, ok := ctx.Value(TlCtxKey).(TL)
|
||||
t, ok := ctx.Value(TLCtxKey).(TL)
|
||||
if !ok {
|
||||
t = &RekorTL{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
type TlPayload struct {
|
||||
type TLPayload struct {
|
||||
Algorithm string
|
||||
Hash string
|
||||
Signature string
|
||||
@@ -98,7 +99,7 @@ func (tl *MockTL) UnmarshalEntry(entryBytes []byte) (any, error) {
|
||||
|
||||
type RekorTL struct{}
|
||||
|
||||
// UploadLogEntry submits a PK token signature to the transparency log
|
||||
// UploadLogEntry submits a PK token signature to the transparency log.
|
||||
func (tl *RekorTL) UploadLogEntry(ctx context.Context, subject string, payload, signature []byte, signer dsse.SignerVerifier) ([]byte, error) {
|
||||
// generate self-signed x509 cert
|
||||
pubCert, err := CreateX509Cert(subject, signer)
|
||||
@@ -121,12 +122,12 @@ func (tl *RekorTL) UploadLogEntry(ctx context.Context, subject string, payload,
|
||||
}
|
||||
entryBytes, err := entry.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshalling TL entry: %w", err)
|
||||
return nil, fmt.Errorf("error marshaling TL entry: %w", err)
|
||||
}
|
||||
return entryBytes, nil
|
||||
}
|
||||
|
||||
// VerifyLogEntry verifies a transparency log entry
|
||||
// VerifyLogEntry verifies a transparency log entry.
|
||||
func (tl *RekorTL) VerifyLogEntry(ctx context.Context, entryBytes []byte) (time.Time, error) {
|
||||
zeroTime := time.Time{}
|
||||
entry, err := tl.UnmarshalEntry(entryBytes)
|
||||
@@ -157,12 +158,12 @@ func (tl *RekorTL) VerifyLogEntry(ctx context.Context, entryBytes []byte) (time.
|
||||
return integratedTime, nil
|
||||
}
|
||||
|
||||
// CreateX509Cert generates a self-signed x509 cert for TL submission
|
||||
// CreateX509Cert generates a self-signed x509 cert for TL submission.
|
||||
func CreateX509Cert(subject string, signer dsse.SignerVerifier) ([]byte, error) {
|
||||
// encode ephemeral public key
|
||||
ecPub, err := x509.MarshalPKIXPublicKey(signer.Public())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshalling public key: %w", err)
|
||||
return nil, fmt.Errorf("error marshaling public key: %w", err)
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
@@ -180,7 +181,7 @@ func CreateX509Cert(subject string, signer dsse.SignerVerifier) ([]byte, error)
|
||||
|
||||
// dsse.SignerVerifier doesn't implement cypto.Signer exactly
|
||||
|
||||
csigner, ok := signer.(*signerverifier.ECDSA256_SignerVerifier)
|
||||
csigner, ok := signer.(*signerverifier.ECDSA256SignerVerifier)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected signer to be of type *signerverifier.ECDSA_SignerVerifier, got %T", signer)
|
||||
}
|
||||
@@ -193,7 +194,7 @@ func CreateX509Cert(subject string, signer dsse.SignerVerifier) ([]byte, error)
|
||||
return pem.EncodeToMemory(certBlock), nil
|
||||
}
|
||||
|
||||
// VerifyEntryPayload checks that the TL entry payload matches envelope payload
|
||||
// VerifyEntryPayload checks that the TL entry payload matches envelope payload.
|
||||
func (tl *RekorTL) VerifyEntryPayload(entryBytes, payload, publicKey []byte) error {
|
||||
entry, err := tl.UnmarshalEntry(entryBytes)
|
||||
if err != nil {
|
||||
@@ -228,7 +229,7 @@ func (tl *RekorTL) VerifyEntryPayload(entryBytes, payload, publicKey []byte) err
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse certificate: %w", err)
|
||||
}
|
||||
if string(result.RawSubjectPublicKeyInfo) != string(publicKey) {
|
||||
if !bytes.Equal(result.RawSubjectPublicKeyInfo, publicKey) {
|
||||
return fmt.Errorf("error payload and tl entry public key mismatch")
|
||||
}
|
||||
return nil
|
||||
@@ -243,9 +244,9 @@ func (tl *RekorTL) UnmarshalEntry(entry []byte) (any, error) {
|
||||
return le, nil
|
||||
}
|
||||
|
||||
func extractHashedRekord(Body string) (*TlPayload, error) {
|
||||
sig := new(TlPayload)
|
||||
pe, err := models.UnmarshalProposedEntry(base64.NewDecoder(base64.StdEncoding, strings.NewReader(Body)), runtime.JSONConsumer())
|
||||
func extractHashedRekord(body string) (*TLPayload, error) {
|
||||
sig := new(TLPayload)
|
||||
pe, err := models.UnmarshalProposedEntry(base64.NewDecoder(base64.StdEncoding, strings.NewReader(body)), runtime.JSONConsumer())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// test artifacts
|
||||
// test artifacts.
|
||||
TestPayload = "test"
|
||||
TestPublicKey = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAED4V+REhx+aqWH7ylMMDHahNMnMLS\nOJP/9kAm9lp+3mqYTAhURra6OD5Qx8Zbd+euPyPk9y+w/gWGDB9zn/Il1A==\n-----END PUBLIC KEY-----"
|
||||
)
|
||||
@@ -53,15 +53,15 @@ func TestUploadAndVerifyLogEntry(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
var tl TL
|
||||
if USE_MOCK_TL {
|
||||
if UseMockTL {
|
||||
tl = &MockTL{
|
||||
UploadLogEntryFunc: func(ctx context.Context, subject string, payload []byte, signature []byte, signer dsse.SignerVerifier) ([]byte, error) {
|
||||
UploadLogEntryFunc: func(_ context.Context, _ string, _ []byte, _ []byte, _ dsse.SignerVerifier) ([]byte, error) {
|
||||
return []byte(TestEntry), nil
|
||||
},
|
||||
VerifyLogEntryFunc: func(ctx context.Context, entryBytes []byte) (time.Time, error) {
|
||||
VerifyLogEntryFunc: func(_ context.Context, _ []byte) (time.Time, error) {
|
||||
return time.Time{}, nil
|
||||
},
|
||||
VerifyEntryPayloadFunc: func(entryBytes, payload, publicKey []byte) error {
|
||||
VerifyEntryPayloadFunc: func(_, _, _ []byte) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
2
pkg/tuf/README.md
Normal file
2
pkg/tuf/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## tuf
|
||||
This package implements TUF clients for http and oci data sources.
|
||||
@@ -4,12 +4,11 @@ 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() {
|
||||
func ExampleNewClient_registry() {
|
||||
// create a tuf client
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
@@ -21,23 +20,20 @@ func ExampleNewTufClient_registry() {
|
||||
metadataURI := "registry-1.docker.io/docker/tuf-metadata:latest"
|
||||
targetsURI := "registry-1.docker.io/docker/tuf-targets"
|
||||
|
||||
registryClient, err := tuf.NewTufClient(embed.RootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker())
|
||||
registryClient, err := tuf.NewClient(&tuf.ClientOptions{tuf.DockerTUFRootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker()})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// get trusted tuf metadata
|
||||
trustedMetadata := registryClient.GetMetadata()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// top-level target files
|
||||
targets := trustedMetadata.Targets[metadata.TARGETS].Signed.Targets
|
||||
|
||||
for _, t := range targets {
|
||||
// download target files
|
||||
_, _, err := registryClient.DownloadTarget(t.Path, filepath.Join(tufOutputPath, "download"))
|
||||
_, err := registryClient.DownloadTarget(t.Path, filepath.Join(tufOutputPath, "download"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -4,69 +4,47 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/attest/internal/util"
|
||||
)
|
||||
|
||||
type mockTufClient struct {
|
||||
type MockTufClient struct {
|
||||
srcPath string
|
||||
dstPath string
|
||||
}
|
||||
|
||||
func NewMockTufClient(srcPath string, dstPath string) *mockTufClient {
|
||||
func NewMockTufClient(srcPath string) *MockTufClient {
|
||||
if srcPath == "" {
|
||||
panic("srcPath must be set")
|
||||
}
|
||||
if dstPath == "" {
|
||||
panic("dstPath must be set")
|
||||
}
|
||||
return &mockTufClient{
|
||||
return &MockTufClient{
|
||||
srcPath: srcPath,
|
||||
dstPath: dstPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (dc *mockTufClient) DownloadTarget(target string, filePath string) (actualFilePath string, data []byte, err error) {
|
||||
src, err := os.Open(filepath.Join(dc.srcPath, target))
|
||||
func (dc *MockTufClient) DownloadTarget(target string, _ string) (file *TargetFile, err error) {
|
||||
targetPath := filepath.Join(dc.srcPath, target)
|
||||
src, err := os.Open(targetPath)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
var dstFilePath string
|
||||
if filePath == "" {
|
||||
dstFilePath = filepath.Join(dc.dstPath, filepath.FromSlash(target))
|
||||
} else {
|
||||
dstFilePath = filePath
|
||||
}
|
||||
|
||||
err = os.MkdirAll(filepath.Dir(dstFilePath), 0755)
|
||||
b, err := io.ReadAll(src)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
dst, err := os.Create(dstFilePath)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// reading from tee will read from src and write to dst at the same time
|
||||
tee := io.TeeReader(src, dst)
|
||||
|
||||
b, err := io.ReadAll(tee)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dstFilePath, b, nil
|
||||
return &TargetFile{TargetURI: targetPath, Data: b, Digest: util.SHA256Hex(b)}, nil
|
||||
}
|
||||
|
||||
type mockVersionChecker struct {
|
||||
type MockVersionChecker struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func NewMockVersionChecker() *mockVersionChecker {
|
||||
return &mockVersionChecker{}
|
||||
func NewMockVersionChecker() *MockVersionChecker {
|
||||
return &MockVersionChecker{}
|
||||
}
|
||||
|
||||
func (vc *mockVersionChecker) CheckVersion(client TUFClient) error {
|
||||
func (vc *MockVersionChecker) CheckVersion(_ Downloader) error {
|
||||
return vc.err
|
||||
}
|
||||
|
||||
@@ -10,23 +10,25 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/crane"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
"github.com/theupdateframework/go-tuf/v2/metadata"
|
||||
"github.com/theupdateframework/go-tuf/v2/metadata/config"
|
||||
)
|
||||
|
||||
const (
|
||||
TufFileNameAnnotation = "tuf.io/filename"
|
||||
TUFFileNameAnnotation = "tuf.io/filename"
|
||||
)
|
||||
|
||||
type TufRole string
|
||||
type Role string
|
||||
|
||||
var TufRoles = []TufRole{metadata.ROOT, metadata.SNAPSHOT, metadata.TARGETS, metadata.TIMESTAMP}
|
||||
var Roles = []Role{metadata.ROOT, metadata.SNAPSHOT, metadata.TARGETS, metadata.TIMESTAMP}
|
||||
|
||||
// RegistryFetcher implements Fetcher
|
||||
// RegistryFetcher implements Fetcher.
|
||||
type RegistryFetcher struct {
|
||||
httpUserAgent string
|
||||
metadataRepo string
|
||||
@@ -34,6 +36,7 @@ type RegistryFetcher struct {
|
||||
targetsRepo string
|
||||
cache *ImageCache
|
||||
timeout time.Duration
|
||||
cfg *config.UpdaterConfig
|
||||
}
|
||||
|
||||
type ImageCache struct {
|
||||
@@ -46,13 +49,13 @@ func NewImageCache() *ImageCache {
|
||||
}
|
||||
}
|
||||
|
||||
// Get image from cache
|
||||
// Get image from cache.
|
||||
func (c *ImageCache) Get(imgRef string) ([]byte, bool) {
|
||||
img, found := c.cache[imgRef]
|
||||
return img, found
|
||||
}
|
||||
|
||||
// Add image to cache
|
||||
// Add image to cache.
|
||||
func (c *ImageCache) Put(imgRef string, img []byte) {
|
||||
c.cache[imgRef] = img
|
||||
}
|
||||
@@ -67,13 +70,31 @@ type Layers struct {
|
||||
MediaType string `json:"mediaType"`
|
||||
}
|
||||
|
||||
func NewRegistryFetcher(metadataRepo, metadataTag, targetsRepo string) *RegistryFetcher {
|
||||
func NewRegistryFetcher(cfg *config.UpdaterConfig) (*RegistryFetcher, error) {
|
||||
ref, err := reference.ParseNormalizedNamed(cfg.RemoteMetadataURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse metadata repo: %w", err)
|
||||
}
|
||||
// add latest tag
|
||||
metadataTag := LatestTag
|
||||
if tag, ok := ref.(reference.Tagged); ok {
|
||||
metadataTag = tag.Tag()
|
||||
}
|
||||
metadataRepo := ref.Name()
|
||||
|
||||
targetsRef, err := reference.ParseNormalizedNamed(cfg.RemoteTargetsURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse targets repo: %w", err)
|
||||
}
|
||||
targetsRepo := targetsRef.Name()
|
||||
return &RegistryFetcher{
|
||||
// we need to keep these reference so that we can unmangle the URL paths when downloading files
|
||||
cfg: cfg,
|
||||
metadataRepo: metadataRepo,
|
||||
metadataTag: metadataTag,
|
||||
targetsRepo: targetsRepo,
|
||||
cache: NewImageCache(),
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DownloadFile downloads a file from an OCI registry, errors out if it failed,
|
||||
@@ -113,7 +134,7 @@ func (d *RegistryFetcher) DownloadFile(urlPath string, maxLength int64, timeout
|
||||
}
|
||||
}
|
||||
|
||||
// getManifest returns the manifest for an image or index
|
||||
// getManifest returns the manifest for an image or index.
|
||||
func (d *RegistryFetcher) getManifest(ref string) ([]byte, error) {
|
||||
// Pull image manifest
|
||||
var err error
|
||||
@@ -135,7 +156,7 @@ func (d *RegistryFetcher) getManifest(ref string) ([]byte, error) {
|
||||
return mf, nil
|
||||
}
|
||||
|
||||
// pullFileLayer pulls a layer for an image or index and returns its data
|
||||
// pullFileLayer pulls a layer for an image or index and returns its data.
|
||||
func (d *RegistryFetcher) pullFileLayer(ref string, maxLength int64) ([]byte, error) {
|
||||
var data []byte
|
||||
var found bool
|
||||
@@ -159,7 +180,7 @@ func (d *RegistryFetcher) pullFileLayer(ref string, maxLength int64) ([]byte, er
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// getDataFromLayer returns the data from a layer in an image
|
||||
// getDataFromLayer returns the data from a layer in an image.
|
||||
func getDataFromLayer(fileLayer v1.Layer, maxLength int64) ([]byte, error) {
|
||||
length, err := fileLayer.Size()
|
||||
if err != nil {
|
||||
@@ -185,20 +206,20 @@ func getDataFromLayer(fileLayer v1.Layer, maxLength int64) ([]byte, error) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// parseImgRef maintains the Fetcher interface by parsing a URL path to an image reference and file name
|
||||
// parseImgRef maintains the Fetcher interface by parsing a URL path to an image reference and file name.
|
||||
func (d *RegistryFetcher) parseImgRef(urlPath string) (imgRef, fileName string, err error) {
|
||||
// Check if repo is target or metadata
|
||||
if strings.Contains(urlPath, d.targetsRepo) {
|
||||
if strings.HasPrefix(urlPath, d.cfg.RemoteTargetsURL) {
|
||||
// determine if the target path contains subdirectories and set image name accordingly
|
||||
// <repo>/<filename> -> image = <repo>:<filename>, layer = <filename>
|
||||
// <repo>/<subdir>/<filename> -> index = <repo>:<subdir> , image = <filename> -> layer = <filename>
|
||||
target := strings.TrimPrefix(urlPath, d.targetsRepo+"/")
|
||||
target := strings.TrimPrefix(urlPath, d.cfg.RemoteTargetsURL+"/")
|
||||
subdir, name, found := strings.Cut(target, "/")
|
||||
if found {
|
||||
return fmt.Sprintf("%s:%s", d.targetsRepo, subdir), fmt.Sprintf("%s/%s", subdir, name), nil
|
||||
}
|
||||
return fmt.Sprintf("%s:%s", d.targetsRepo, target), target, nil
|
||||
} else if strings.Contains(urlPath, d.metadataRepo) {
|
||||
} else if strings.HasPrefix(urlPath, d.cfg.RemoteMetadataURL) {
|
||||
// build the metadata image name
|
||||
// determine if role is a delegated role and set the tag accordingly
|
||||
fileName = path.Base(urlPath)
|
||||
@@ -208,12 +229,11 @@ func (d *RegistryFetcher) parseImgRef(urlPath string) (imgRef, fileName string,
|
||||
return fmt.Sprintf("%s:%s", d.metadataRepo, role), fileName, nil
|
||||
}
|
||||
return fmt.Sprintf("%s:%s", d.metadataRepo, d.metadataTag), fileName, nil
|
||||
} else {
|
||||
return "", "", fmt.Errorf("urlPath: %s must be in metadata or targets repo", urlPath)
|
||||
}
|
||||
return "", "", fmt.Errorf("urlPath: %s must be in metadata or targets repo", urlPath)
|
||||
}
|
||||
|
||||
// findFileInManifest searches the image or index manifest for a file with the given name and returns its digest
|
||||
// findFileInManifest searches the image or index manifest for a file with the given name and returns its digest.
|
||||
func (d *RegistryFetcher) findFileInManifest(mf []byte, name string) (*v1.Hash, error) {
|
||||
var index bool
|
||||
|
||||
@@ -226,20 +246,21 @@ func (d *RegistryFetcher) findFileInManifest(mf []byte, name string) (*v1.Hash,
|
||||
|
||||
// determine image or index manifest
|
||||
var layers []Layer
|
||||
if l.MediaType == string(types.OCIImageIndex) {
|
||||
switch l.MediaType {
|
||||
case string(types.OCIImageIndex):
|
||||
layers = l.Manifests
|
||||
index = true
|
||||
} else if l.MediaType == string(types.OCIManifestSchema1) {
|
||||
case string(types.OCIManifestSchema1):
|
||||
layers = l.Layers
|
||||
index = false
|
||||
} else {
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid manifest media type: %s", l.MediaType)
|
||||
}
|
||||
|
||||
// find annotation with file name
|
||||
var digest string
|
||||
for _, layer := range layers {
|
||||
if layer.Annotations[TufFileNameAnnotation] == name {
|
||||
if layer.Annotations[TUFFileNameAnnotation] == name {
|
||||
digest = layer.Digest
|
||||
break
|
||||
}
|
||||
@@ -267,7 +288,7 @@ func (d *RegistryFetcher) findFileInManifest(mf []byte, name string) (*v1.Hash,
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
// transportWithTimeout returns a http.RoundTripper with a specified timeout
|
||||
// transportWithTimeout returns a http.RoundTripper with a specified timeout.
|
||||
func transportWithTimeout(timeout time.Duration) http.RoundTripper {
|
||||
// transport is based on go-containerregistry remote.DefaultTransport
|
||||
// with modifications to include a specified timeout
|
||||
@@ -286,9 +307,9 @@ func transportWithTimeout(timeout time.Duration) http.RoundTripper {
|
||||
}
|
||||
}
|
||||
|
||||
// isDelegatedRole returns true if the role is a delegated role
|
||||
// isDelegatedRole returns true if the role is a delegated role.
|
||||
func isDelegatedRole(role string) bool {
|
||||
for _, r := range TufRoles {
|
||||
for _, r := range Roles {
|
||||
if role == string(r) {
|
||||
return false // role is not a delegated role
|
||||
}
|
||||
@@ -296,7 +317,7 @@ func isDelegatedRole(role string) bool {
|
||||
return true // role is a delegated role
|
||||
}
|
||||
|
||||
// roleFromConsistentName returns the role name from a consistent snapshot file name
|
||||
// roleFromConsistentName returns the role name from a consistent snapshot file name.
|
||||
func roleFromConsistentName(filename string) string {
|
||||
name := strings.TrimSuffix(filename, ".json")
|
||||
role := strings.Split(name, ".")
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/embed"
|
||||
"github.com/docker/attest/internal/util"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/google/go-containerregistry/pkg/crane"
|
||||
@@ -22,6 +21,7 @@ import (
|
||||
"github.com/google/go-containerregistry/pkg/v1/static"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/testcontainers/testcontainers-go/modules/registry"
|
||||
"github.com/theupdateframework/go-tuf/v2/metadata"
|
||||
"github.com/theupdateframework/go-tuf/v2/metadata/config"
|
||||
@@ -30,6 +30,11 @@ import (
|
||||
|
||||
const (
|
||||
tufTargetMediaType = "application/vnd.tuf.target"
|
||||
testRole = "test-role"
|
||||
tufMetadataRepo = "tuf-metadata"
|
||||
targetsPath = "/tuf-targets"
|
||||
metadataPath = "/tuf-metadata"
|
||||
targetsRepo = "test" + targetsPath
|
||||
)
|
||||
|
||||
func TestRegistryFetcher(t *testing.T) {
|
||||
@@ -40,44 +45,45 @@ func TestRegistryFetcher(t *testing.T) {
|
||||
t.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
|
||||
}
|
||||
}()
|
||||
LoadRegistryTestData(t, regAddr, OciTufTestDataPath)
|
||||
LoadRegistryTestData(t, regAddr, OCITUFTestDataPath)
|
||||
|
||||
metadataRepo := regAddr.Host + "/tuf-metadata"
|
||||
metadataImgTag := "latest"
|
||||
targetsRepo := regAddr.Host + "/tuf-targets"
|
||||
metadataRepo := regAddr.Host + metadataPath
|
||||
targetsRepo := regAddr.Host + targetsPath
|
||||
targetFile := "test.txt"
|
||||
delegatedRole := "test-role"
|
||||
delegatedRole := testRole
|
||||
dir := CreateTempDir(t, "", "tuf_temp")
|
||||
delegatedDir := CreateTempDir(t, dir, delegatedRole)
|
||||
delegatedTargetFile := fmt.Sprintf("%s/%s", delegatedRole, targetFile)
|
||||
|
||||
cfg, err := config.New(metadataRepo, embed.RootDev.Data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataImgTag, targetsRepo)
|
||||
// note - url is ignored here - needed to make http url parsing happy even when using oci
|
||||
cfg, err := config.New("", DockerTUFRootDev.Data)
|
||||
require.NoError(t, err)
|
||||
cfg.LocalMetadataDir = dir
|
||||
cfg.LocalTargetsDir = dir
|
||||
cfg.RemoteTargetsURL = targetsRepo
|
||||
cfg.RemoteMetadataURL = metadataRepo
|
||||
cfg.Fetcher, err = NewRegistryFetcher(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create a new Updater instance
|
||||
up, err := updater.New(cfg)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// refresh the metadata
|
||||
err = up.Refresh()
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// download top-level target
|
||||
targetInfo, err := up.GetTargetInfo(targetFile)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
_, _, err = up.DownloadTarget(targetInfo, filepath.Join(dir, targetInfo.Path), "")
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// download delegated target
|
||||
targetInfo, err = up.GetTargetInfo(delegatedTargetFile)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
_, _, err = up.DownloadTarget(targetInfo, filepath.Join(delegatedDir, targetFile), "")
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRoleFromConsistentName(t *testing.T) {
|
||||
@@ -120,17 +126,17 @@ func TestFindFileInManifest(t *testing.T) {
|
||||
// make test image manifest
|
||||
file := "test.json"
|
||||
data := []byte("test")
|
||||
hash := v1.Hash{Algorithm: "sha256", Hex: util.SHA256Hex(data)}
|
||||
hash := v1.Hash{Hex: util.SHA256Hex(data)}
|
||||
img := empty.Image
|
||||
img = mutate.MediaType(img, types.OCIManifestSchema1)
|
||||
img = mutate.ConfigMediaType(img, types.OCIConfigJSON)
|
||||
// add test layer
|
||||
name := strings.Join([]string{hash.Hex, file}, ".")
|
||||
ann := map[string]string{TufFileNameAnnotation: name}
|
||||
ann := map[string]string{TUFFileNameAnnotation: name}
|
||||
layer := mutate.Addendum{Layer: static.NewLayer(data, tufTargetMediaType), Annotations: ann}
|
||||
img, err := mutate.Append(img, layer)
|
||||
assert.NoError(t, err)
|
||||
image_manifest, err := img.RawManifest()
|
||||
imageManifest, err := img.RawManifest()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// make test index manifest
|
||||
@@ -141,21 +147,20 @@ func TestFindFileInManifest(t *testing.T) {
|
||||
Add: img,
|
||||
Descriptor: v1.Descriptor{
|
||||
Annotations: map[string]string{
|
||||
TufFileNameAnnotation: name,
|
||||
TUFFileNameAnnotation: name,
|
||||
},
|
||||
},
|
||||
})
|
||||
index_manifest, err := idx.RawManifest()
|
||||
indexManifest, err := idx.RawManifest()
|
||||
assert.NoError(t, err)
|
||||
// cache image layer
|
||||
targetsRepo := "test/tuf-targets"
|
||||
d := &RegistryFetcher{
|
||||
cache: NewImageCache(),
|
||||
targetsRepo: targetsRepo,
|
||||
}
|
||||
imgHash, err := img.Digest()
|
||||
assert.NoError(t, err)
|
||||
d.cache.Put(fmt.Sprintf("%s@%s", targetsRepo, imgHash.String()), image_manifest)
|
||||
d.cache.Put(fmt.Sprintf("%s@%s", targetsRepo, imgHash.String()), imageManifest)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -163,9 +168,9 @@ func TestFindFileInManifest(t *testing.T) {
|
||||
file string
|
||||
expected string
|
||||
}{
|
||||
{"consistent filename image", image_manifest, fmt.Sprintf("%s.%s", hash.Hex, file), hash.Hex},
|
||||
{"filename image", image_manifest, file, ""},
|
||||
{"consistent filename index", index_manifest, fmt.Sprintf("%s.%s", hash.Hex, file), hash.Hex},
|
||||
{"consistent filename image", imageManifest, fmt.Sprintf("%s.%s", hash.Hex, file), hash.Hex},
|
||||
{"filename image", imageManifest, file, ""},
|
||||
{"consistent filename index", indexManifest, fmt.Sprintf("%s.%s", hash.Hex, file), hash.Hex},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -181,32 +186,62 @@ func TestFindFileInManifest(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseImgRef(t *testing.T) {
|
||||
metadataRepo := "test/tuf-metadata"
|
||||
metadataTag := "latest"
|
||||
targetsRepo := "test/tuf-targets"
|
||||
delegatedRole := "test-role"
|
||||
metadataRepo := "test" + metadataPath
|
||||
metadataTag := LatestTag
|
||||
delegatedRole := testRole
|
||||
validRef := fmt.Sprintf("%s/2.root.json", metadataRepo)
|
||||
expectedRef := fmt.Sprintf("docker.io/%s:%s", metadataRepo, metadataTag)
|
||||
testCases := []struct {
|
||||
name string
|
||||
ref string
|
||||
expectedRef string
|
||||
expectedFile string
|
||||
name string
|
||||
ref string
|
||||
expectedRef string
|
||||
expectedFile string
|
||||
metadataRepo string
|
||||
metadataTag string
|
||||
expectedRefError string
|
||||
expectedConstructorError string
|
||||
targetsRepo string
|
||||
}{
|
||||
{"top-level metadata", fmt.Sprintf("%s/2.root.json", metadataRepo), fmt.Sprintf("%s:%s", metadataRepo, metadataTag), "2.root.json"},
|
||||
{"delegated metadata", fmt.Sprintf("%s/%s/5.test-role.json", metadataRepo, delegatedRole), fmt.Sprintf("%s:%s", metadataRepo, delegatedRole), "5.test-role.json"},
|
||||
{"top-level target", fmt.Sprintf("%s/policy.yaml", targetsRepo), fmt.Sprintf("%s:policy.yaml", targetsRepo), "policy.yaml"},
|
||||
{"delegated target", fmt.Sprintf("%s/%s/policy.yaml", targetsRepo, delegatedRole), fmt.Sprintf("%s:%s", targetsRepo, delegatedRole), fmt.Sprintf("%s/policy.yaml", delegatedRole)},
|
||||
{name: "top-level metadata", ref: validRef, expectedRef: expectedRef, expectedFile: "2.root.json"},
|
||||
{name: "short metdata repo", ref: validRef, metadataRepo: "test" + metadataPath, expectedRef: expectedRef, expectedFile: "2.root.json"},
|
||||
{name: "library path", ref: fmt.Sprintf("test%s/2.root.json", metadataPath), metadataRepo: "test" + metadataPath, expectedRef: "docker.io/test/tuf-metadata:latest", expectedFile: "2.root.json"},
|
||||
{name: "short targets repo", ref: validRef, targetsRepo: "test" + targetsPath, expectedRef: expectedRef, expectedFile: "2.root.json"},
|
||||
{name: "delegated metadata", ref: fmt.Sprintf("%s/%s/5.test-role.json", metadataRepo, delegatedRole), expectedRef: fmt.Sprintf("docker.io/%s:%s", metadataRepo, delegatedRole), expectedFile: "5.test-role.json"},
|
||||
{name: "top-level target", ref: fmt.Sprintf("%s/policy.yaml", targetsRepo), expectedRef: fmt.Sprintf("docker.io/%s:policy.yaml", targetsRepo), expectedFile: "policy.yaml"},
|
||||
{name: "delegated target", ref: fmt.Sprintf("%s/%s/policy.yaml", targetsRepo, delegatedRole), expectedRef: fmt.Sprintf("docker.io/%s:%s", targetsRepo, delegatedRole), expectedFile: fmt.Sprintf("%s/policy.yaml", delegatedRole)},
|
||||
{name: "docker/targets", ref: fmt.Sprintf("%s/2.root.json", "docker.io/docker/targets"), expectedRef: "docker.io/docker/targets:latest", expectedFile: "2.root.json", metadataRepo: "docker.io/docker/targets"},
|
||||
{name: "malformed ref", ref: fmt.Sprintf("%s/2.root.json", "@broken"), expectedRefError: "urlPath: @broken/2.root.json must be in metadata or targets repo"},
|
||||
{name: "malformed metadataRepo", ref: validRef, metadataRepo: "@broken", expectedConstructorError: "failed to parse metadata repo: invalid reference format"},
|
||||
{name: "malformed targetsRepo", ref: validRef, targetsRepo: "@broken", expectedConstructorError: "failed to parse targets repo: invalid reference format"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
d := &RegistryFetcher{
|
||||
metadataRepo: metadataRepo,
|
||||
metadataTag: "latest",
|
||||
targetsRepo: targetsRepo,
|
||||
repo := metadataRepo
|
||||
if tc.metadataRepo != "" {
|
||||
repo = tc.metadataRepo
|
||||
}
|
||||
targets := targetsRepo
|
||||
if tc.targetsRepo != "" {
|
||||
targets = tc.targetsRepo
|
||||
}
|
||||
cfg := &config.UpdaterConfig{
|
||||
RemoteMetadataURL: repo,
|
||||
RemoteTargetsURL: targets,
|
||||
}
|
||||
d, err := NewRegistryFetcher(cfg)
|
||||
if tc.expectedConstructorError != "" {
|
||||
assert.ErrorContains(t, err, tc.expectedConstructorError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
imgRef, file, err := d.parseImgRef(tc.ref)
|
||||
if tc.expectedRefError != "" {
|
||||
assert.ErrorContains(t, err, tc.expectedRefError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedRef, imgRef, "ref mismatch")
|
||||
assert.Equal(t, tc.expectedFile, file, "file mismatch")
|
||||
}
|
||||
}
|
||||
imgRef, file, err := d.parseImgRef(tc.ref)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedRef, imgRef)
|
||||
assert.Equal(t, tc.expectedFile, file)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -246,7 +281,7 @@ func TestPullFileLayer(t *testing.T) {
|
||||
}()
|
||||
|
||||
// make test layer
|
||||
repo := "tuf-metadata"
|
||||
repo := tufMetadataRepo
|
||||
data := []byte("test")
|
||||
testLayer := static.NewLayer(data, tufTargetMediaType)
|
||||
hash, err := testLayer.Digest()
|
||||
@@ -303,7 +338,7 @@ func TestGetManifest(t *testing.T) {
|
||||
}()
|
||||
|
||||
// make test manifest
|
||||
repo := "tuf-metadata"
|
||||
repo := tufMetadataRepo
|
||||
img := empty.Image
|
||||
img = mutate.MediaType(img, types.OCIManifestSchema1)
|
||||
img = mutate.ConfigMediaType(img, types.OCIConfigJSON)
|
||||
@@ -339,7 +374,7 @@ func TestGetManifest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// RunTestRegistry starts a registry testcontainer for TUF on OCI testdata
|
||||
// RunTestRegistry starts a registry testcontainer for TUF on OCI testdata.
|
||||
func RunTestRegistry(t *testing.T) (*registry.RegistryContainer, *url.URL) {
|
||||
registryContainer, err := registry.Run(context.Background(), "registry:2.8.3")
|
||||
if err != nil {
|
||||
@@ -353,28 +388,24 @@ func RunTestRegistry(t *testing.T) (*registry.RegistryContainer, *url.URL) {
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse container address: %s", err)
|
||||
}
|
||||
if addr.Hostname() == "127.0.0.1" {
|
||||
addr.Host = "localhost:" + addr.Port()
|
||||
}
|
||||
return registryContainer, addr
|
||||
}
|
||||
|
||||
// LoadRegistryTestData pushes TUF metadata and targets to an OCI registry
|
||||
// LoadRegistryTestData pushes TUF metadata and targets to an OCI registry.
|
||||
func LoadRegistryTestData(t *testing.T, registry *url.URL, path string) {
|
||||
// push tuf metadata and targets to local registry
|
||||
METADATA_REPO := "tuf-metadata"
|
||||
METADATA_TAG := "latest"
|
||||
TARGETS_REPO := "tuf-targets"
|
||||
DELEGATED_ROLE := "test-role"
|
||||
MetadataRepo := tufMetadataRepo
|
||||
TargetsRepo := "tuf-targets"
|
||||
DelegatedRole := testRole
|
||||
|
||||
// push top-level metadata -> metadata:latest
|
||||
err := LoadMetadata(filepath.Join(path, "metadata"), registry.Host, METADATA_REPO, METADATA_TAG)
|
||||
err := LoadMetadata(filepath.Join(path, "metadata"), registry.Host, MetadataRepo, LatestTag)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// push delegated metadata -> metadata:<DELEGATED_ROLE>
|
||||
err = LoadMetadata(filepath.Join(path, "metadata", DELEGATED_ROLE), registry.Host, METADATA_REPO, DELEGATED_ROLE)
|
||||
err = LoadMetadata(filepath.Join(path, "metadata", DelegatedRole), registry.Host, MetadataRepo, DelegatedRole)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -392,7 +423,7 @@ func LoadRegistryTestData(t *testing.T, registry *url.URL, path string) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ref, err := name.ParseReference(fmt.Sprintf("%s/%s:%s", registry.Host, TARGETS_REPO, dir.Name()))
|
||||
ref, err := name.ParseReference(fmt.Sprintf("%s/%s:%s", registry.Host, TargetsRepo, dir.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -400,7 +431,8 @@ func LoadRegistryTestData(t *testing.T, registry *url.URL, path string) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(mf.Manifests) == 1 {
|
||||
switch len(mf.Manifests) {
|
||||
case 1:
|
||||
// top-level target
|
||||
img, err := tIdx.Image(mf.Manifests[0].Digest)
|
||||
if err != nil {
|
||||
@@ -410,19 +442,19 @@ func LoadRegistryTestData(t *testing.T, registry *url.URL, path string) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else if len(mf.Manifests) > 1 {
|
||||
case 2:
|
||||
// delegated target
|
||||
err = remote.WriteIndex(ref, tIdx, oci.MultiKeychainOption())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
t.Fatal("no manifests found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LoadMetadata loads TUF metadata from a local path and pushes to a registry
|
||||
// LoadMetadata loads TUF metadata from a local path and pushes to a registry.
|
||||
func LoadMetadata(path, host, repo, tag string) error {
|
||||
mIdx, err := layout.ImageIndexFromPath(path)
|
||||
if err != nil {
|
||||
|
||||
166
pkg/tuf/tuf.go
166
pkg/tuf/tuf.go
@@ -20,78 +20,109 @@ import (
|
||||
"github.com/theupdateframework/go-tuf/v2/metadata/updater"
|
||||
)
|
||||
|
||||
type TufSource string
|
||||
type Source string
|
||||
|
||||
const (
|
||||
HttpSource TufSource = "http"
|
||||
OciSource TufSource = "oci"
|
||||
HTTPSource Source = "http"
|
||||
OCISource Source = "oci"
|
||||
LatestTag string = "latest"
|
||||
)
|
||||
|
||||
var (
|
||||
DockerTufRootProd = embed.RootProd
|
||||
DockerTufRootStaging = embed.RootStaging
|
||||
DockerTufRootDev = embed.RootDev
|
||||
DockerTufRootDefault = embed.RootDefault
|
||||
DockerTUFRootProd = embed.RootProd
|
||||
DockerTUFRootStaging = embed.RootStaging
|
||||
DockerTUFRootDev = embed.RootDev
|
||||
DockerTUFRootDefault = embed.RootDefault
|
||||
)
|
||||
|
||||
type TUFClient interface {
|
||||
DownloadTarget(target, filePath string) (actualFilePath string, data []byte, err error)
|
||||
const (
|
||||
defaultMetadataSource = "docker/tuf-metadata:latest"
|
||||
defaultTargetsSource = "docker/tuf-targets"
|
||||
)
|
||||
|
||||
type Downloader interface {
|
||||
DownloadTarget(target, filePath string) (file *TargetFile, err error)
|
||||
}
|
||||
|
||||
type TufClient struct {
|
||||
type Client struct {
|
||||
updater *updater.Updater
|
||||
cfg *config.UpdaterConfig
|
||||
}
|
||||
|
||||
// NewTufClient creates a new TUF client
|
||||
func NewTufClient(initialRoot []byte, tufPath, metadataSource, targetsSource string, versionChecker VersionChecker) (*TufClient, error) {
|
||||
var tufSource TufSource
|
||||
if strings.HasPrefix(metadataSource, "https://") || strings.HasPrefix(metadataSource, "http://") {
|
||||
tufSource = HttpSource
|
||||
type TargetFile struct {
|
||||
ActualFilePath string
|
||||
TargetURI string
|
||||
Digest string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type ClientOptions struct {
|
||||
InitialRoot []byte
|
||||
Path string
|
||||
MetadataSource string
|
||||
TargetsSource string
|
||||
VersionChecker VersionChecker
|
||||
}
|
||||
|
||||
func NewDockerDefaultClientOptions(tufPath string) *ClientOptions {
|
||||
return &ClientOptions{
|
||||
InitialRoot: DockerTUFRootDefault.Data,
|
||||
Path: tufPath,
|
||||
MetadataSource: defaultMetadataSource,
|
||||
TargetsSource: defaultTargetsSource,
|
||||
VersionChecker: NewDefaultVersionChecker(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewClient creates a new TUF client.
|
||||
func NewClient(opts *ClientOptions) (*Client, error) {
|
||||
var tufSource Source
|
||||
if strings.HasPrefix(opts.MetadataSource, "https://") || strings.HasPrefix(opts.MetadataSource, "http://") {
|
||||
tufSource = HTTPSource
|
||||
} else {
|
||||
tufSource = OciSource
|
||||
tufSource = OCISource
|
||||
}
|
||||
|
||||
tufRootDigest := util.SHA256Hex(initialRoot)
|
||||
tufRootDigest := util.SHA256Hex(opts.InitialRoot)
|
||||
|
||||
// create a directory for each initial root.json
|
||||
metadataPath := filepath.Join(tufPath, tufRootDigest)
|
||||
err := os.MkdirAll(metadataPath, 0755)
|
||||
metadataPath := filepath.Join(opts.Path, tufRootDigest)
|
||||
err := os.MkdirAll(metadataPath, os.ModePerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create directory '%s': %w", metadataPath, err)
|
||||
}
|
||||
rootFile := filepath.Join(metadataPath, "root.json")
|
||||
var rootBytes []byte
|
||||
rootBytes, err = os.ReadFile(rootFile)
|
||||
|
||||
if err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, fmt.Errorf("failed to read root.json: %w", err)
|
||||
}
|
||||
// write the root.json file to the metadata directory
|
||||
err = os.WriteFile(rootFile, initialRoot, 0644)
|
||||
err = os.WriteFile(rootFile, opts.InitialRoot, 0o666) // #nosec G306
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to write root.json %w", err)
|
||||
}
|
||||
rootBytes = initialRoot
|
||||
rootBytes = opts.InitialRoot
|
||||
}
|
||||
|
||||
// create updater configuration
|
||||
cfg, err := config.New(metadataSource, rootBytes) // default config
|
||||
// this is parsed as an HTTP url (which doesn't work for OCI). We're setting this to make TUF happy
|
||||
// and overwriding the configuration below
|
||||
cfg, err := config.New("", rootBytes) // default config
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create TUF updater configuration: %w", err)
|
||||
}
|
||||
cfg.LocalMetadataDir = metadataPath
|
||||
cfg.LocalTargetsDir = filepath.Join(metadataPath, "download")
|
||||
cfg.RemoteTargetsURL = targetsSource
|
||||
cfg.RemoteMetadataURL = opts.MetadataSource
|
||||
cfg.RemoteTargetsURL = opts.TargetsSource
|
||||
|
||||
if tufSource == OciSource {
|
||||
metadataRepo, metadataTag, found := strings.Cut(metadataSource, ":")
|
||||
if !found {
|
||||
fmt.Printf("metadata tag not found in URL, using latest\n")
|
||||
metadataTag = "latest"
|
||||
if tufSource == OCISource {
|
||||
cfg.Fetcher, err = NewRegistryFetcher(cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create registry fetcher: %w", err)
|
||||
}
|
||||
cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataTag, targetsSource)
|
||||
}
|
||||
|
||||
// create a new Updater instance
|
||||
@@ -106,12 +137,12 @@ func NewTufClient(initialRoot []byte, tufPath, metadataSource, targetsSource str
|
||||
return nil, fmt.Errorf("failed to refresh trusted metadata: %w", err)
|
||||
}
|
||||
|
||||
client := &TufClient{
|
||||
client := &Client{
|
||||
updater: up,
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
err = versionChecker.CheckVersion(client)
|
||||
err = opts.VersionChecker.CheckVersion(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -119,51 +150,80 @@ func NewTufClient(initialRoot []byte, tufPath, metadataSource, targetsSource str
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (t *Client) generateTargetURI(target *metadata.TargetFiles, digest string) (string, error) {
|
||||
switch fetcher := t.cfg.Fetcher.(type) {
|
||||
case *RegistryFetcher:
|
||||
return fmt.Sprintf("%s@sha256:%s", t.cfg.RemoteTargetsURL, digest), nil
|
||||
case *fetcher.DefaultFetcher:
|
||||
targetBaseURL := ensureTrailingSlash(t.cfg.RemoteTargetsURL)
|
||||
targetRemotePath := target.Path
|
||||
// if PrefixTargetsWithHash is set, we need to prefix the target name with the hash and handle subdirectories
|
||||
// similar logic to https://github.com/theupdateframework/go-tuf/blob/f95222bdd22d2ac4e5b8ed6fe912b645e213c3b5/metadata/updater/updater.go#L227-L247
|
||||
if t.cfg.PrefixTargetsWithHash {
|
||||
baseName := filepath.Base(targetRemotePath)
|
||||
dirName, ok := strings.CutSuffix(targetRemotePath, "/"+baseName)
|
||||
if !ok {
|
||||
// <hash>.<target-name>
|
||||
targetRemotePath = fmt.Sprintf("%s.%s", digest, baseName)
|
||||
} else {
|
||||
// <dir-prefix>/<hash>.<target-name>
|
||||
targetRemotePath = fmt.Sprintf("%s/%s.%s", dirName, digest, baseName)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s%s", targetBaseURL, targetRemotePath), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported fetcher type: %T", fetcher)
|
||||
}
|
||||
}
|
||||
|
||||
// DownloadTarget downloads the target file using Updater. The Updater gets the target
|
||||
// information, verifies if the target is already cached, and if it is not cached,
|
||||
// downloads the target file.
|
||||
func (t *TufClient) DownloadTarget(target string, filePath string) (actualFilePath string, data []byte, err error) {
|
||||
func (t *Client) DownloadTarget(target string, filePath string) (file *TargetFile, err error) {
|
||||
// search if the desired target is available
|
||||
targetInfo, err := t.updater.GetTargetInfo(target)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check if filePath exists and create the directory if it doesn't
|
||||
if _, err := os.Stat(filepath.Dir(filePath)); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(filepath.Dir(filePath), 0755)
|
||||
err = os.MkdirAll(filepath.Dir(filePath), os.ModePerm)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to create target download directory '%s': %w", filepath.Dir(filePath), err)
|
||||
return nil, fmt.Errorf("failed to create target download directory '%s': %w", filepath.Dir(filePath), err)
|
||||
}
|
||||
}
|
||||
|
||||
// target is available, so let's see if the target is already present locally
|
||||
actualFilePath, data, err = t.updater.FindCachedTarget(targetInfo, filePath)
|
||||
actualFilePath, data, err := t.updater.FindCachedTarget(targetInfo, filePath)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed while finding a cached target: %w", err)
|
||||
return nil, fmt.Errorf("failed while finding a cached target: %w", err)
|
||||
}
|
||||
if data != nil {
|
||||
return actualFilePath, data, err
|
||||
digest := util.SHA256Hex(data)
|
||||
uri, err := t.generateTargetURI(targetInfo, digest)
|
||||
return &TargetFile{ActualFilePath: actualFilePath, TargetURI: uri, Data: data, Digest: digest}, err
|
||||
}
|
||||
|
||||
// target is not present locally, so let's try to download it
|
||||
actualFilePath, data, err = t.updater.DownloadTarget(targetInfo, filePath, "")
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to download target file %s - %w", target, err)
|
||||
return nil, fmt.Errorf("failed to download target file %s - %w", target, err)
|
||||
}
|
||||
|
||||
return actualFilePath, data, err
|
||||
digest := util.SHA256Hex(data)
|
||||
uri, err := t.generateTargetURI(targetInfo, digest)
|
||||
return &TargetFile{ActualFilePath: actualFilePath, TargetURI: uri, Data: data, Digest: digest}, err
|
||||
}
|
||||
|
||||
func (t *TufClient) GetMetadata() trustedmetadata.TrustedMetadata {
|
||||
func (t *Client) GetMetadata() trustedmetadata.TrustedMetadata {
|
||||
return t.updater.GetTrustedMetadataSet()
|
||||
}
|
||||
|
||||
func (t *TufClient) MaxRootLength() int64 {
|
||||
func (t *Client) MaxRootLength() int64 {
|
||||
return t.cfg.RootMaxLength
|
||||
}
|
||||
|
||||
func (t *TufClient) GetPriorRoots(metadataURL string) (map[string][]byte, error) {
|
||||
func (t *Client) GetPriorRoots(metadataURL string) (map[string][]byte, error) {
|
||||
rootMetadata := map[string][]byte{}
|
||||
trustedMetadata := t.GetMetadata()
|
||||
client := fetcher.DefaultFetcher{}
|
||||
@@ -177,12 +237,12 @@ func (t *TufClient) GetPriorRoots(metadataURL string) (map[string][]byte, error)
|
||||
return rootMetadata, nil
|
||||
}
|
||||
|
||||
func (t *TufClient) SetRemoteTargetsURL(url string) {
|
||||
func (t *Client) SetRemoteTargetsURL(url string) {
|
||||
t.cfg.RemoteTargetsURL = url
|
||||
}
|
||||
|
||||
// Derived from updater.loadTargets() in theupdateframework/go-tuf
|
||||
func (t *TufClient) LoadDelegatedTargets(roleName, parentName string) (*metadata.Metadata[metadata.TargetsType], error) {
|
||||
// Derived from updater.loadTargets() in theupdateframework/go-tuf.
|
||||
func (t *Client) LoadDelegatedTargets(roleName, parentName string) (*metadata.Metadata[metadata.TargetsType], error) {
|
||||
// extract the targets meta from the trusted snapshot metadata
|
||||
meta := t.updater.GetTrustedMetadataSet()
|
||||
metaInfo := meta.Snapshot.Signed.Meta[fmt.Sprintf("%s.json", roleName)]
|
||||
@@ -209,8 +269,8 @@ func (t *TufClient) LoadDelegatedTargets(roleName, parentName string) (*metadata
|
||||
return delegatedTargets, nil
|
||||
}
|
||||
|
||||
// downloadMetadata download a metadata file and return it as bytes
|
||||
func (t *TufClient) downloadMetadata(roleName string, length int64, version string) ([]byte, error) {
|
||||
// downloadMetadata download a metadata file and return it as bytes.
|
||||
func (t *Client) downloadMetadata(roleName string, length int64, version string) ([]byte, error) {
|
||||
urlPath := ensureTrailingSlash(t.cfg.RemoteMetadataURL)
|
||||
// build urlPath
|
||||
if version == "" {
|
||||
@@ -221,7 +281,7 @@ func (t *TufClient) downloadMetadata(roleName string, length int64, version stri
|
||||
return t.cfg.Fetcher.DownloadFile(urlPath, length, time.Second*15)
|
||||
}
|
||||
|
||||
// ensureTrailingSlash ensures url ends with a slash
|
||||
// ensureTrailingSlash ensures url ends with a slash.
|
||||
func ensureTrailingSlash(url string) string {
|
||||
if updater.IsWindowsPath(url) {
|
||||
slash := string(filepath.Separator)
|
||||
@@ -236,7 +296,7 @@ func ensureTrailingSlash(url string) string {
|
||||
return url + "/"
|
||||
}
|
||||
|
||||
// GetEmbeddedTufRoot returns the embedded TUF root based on the given root name
|
||||
func GetEmbeddedTufRoot(root string) (*embed.EmbeddedRoot, error) {
|
||||
// GetEmbeddedRoot returns the embedded TUF root based on the given root name.
|
||||
func GetEmbeddedRoot(root string) (*embed.EmbeddedRoot, error) {
|
||||
return embed.GetRootFromName(root)
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/embed"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/theupdateframework/go-tuf/v2/metadata"
|
||||
)
|
||||
|
||||
var (
|
||||
HttpTufTestDataPath = filepath.Join("..", "..", "test", "testdata", "tuf", "test-repo")
|
||||
OciTufTestDataPath = filepath.Join("..", "..", "test", "testdata", "tuf", "test-repo-oci")
|
||||
HTTPTUFTestDataPath = filepath.Join("..", "..", "test", "testdata", "tuf", "test-repo")
|
||||
OCITUFTestDataPath = filepath.Join("..", "..", "test", "testdata", "tuf", "test-repo-oci")
|
||||
)
|
||||
|
||||
func CreateTempDir(t *testing.T, dir, pattern string) string {
|
||||
@@ -35,12 +35,12 @@ func CreateTempDir(t *testing.T, dir, pattern string) string {
|
||||
return tempDir
|
||||
}
|
||||
|
||||
// NewTufClient creates a new TUF client
|
||||
// NewTufClient creates a new TUF client.
|
||||
func TestRootInit(t *testing.T) {
|
||||
tufPath := CreateTempDir(t, "", "tuf_temp")
|
||||
|
||||
// Start a test HTTP server to serve data from /test/testdata/tuf/test-repo/ paths
|
||||
server := httptest.NewServer(http.FileServer(http.Dir(HttpTufTestDataPath)))
|
||||
server := httptest.NewServer(http.FileServer(http.Dir(HTTPTUFTestDataPath)))
|
||||
defer server.Close()
|
||||
|
||||
// run local registry
|
||||
@@ -50,10 +50,10 @@ func TestRootInit(t *testing.T) {
|
||||
t.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
|
||||
}
|
||||
}()
|
||||
LoadRegistryTestData(t, regAddr, OciTufTestDataPath)
|
||||
LoadRegistryTestData(t, regAddr, OCITUFTestDataPath)
|
||||
|
||||
alwaysGoodVersionChecker := &mockVersionChecker{err: nil}
|
||||
alwaysBadVersionChecker := &mockVersionChecker{err: assert.AnError}
|
||||
alwaysGoodVersionChecker := &MockVersionChecker{err: nil}
|
||||
alwaysBadVersionChecker := &MockVersionChecker{err: assert.AnError}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -65,29 +65,29 @@ func TestRootInit(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
_, err := NewTufClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker)
|
||||
_, err := NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
|
||||
assert.NoErrorf(t, err, "Failed to create TUF client: %v", err)
|
||||
|
||||
// recreation should work with same root
|
||||
_, err = NewTufClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker)
|
||||
_, err = NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
|
||||
assert.NoErrorf(t, err, "Failed to recreate TUF client: %v", err)
|
||||
|
||||
_, err = NewTufClient([]byte("broken"), tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker)
|
||||
_, err = NewClient(&ClientOptions{[]byte("broken"), tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
|
||||
assert.Errorf(t, err, "Expected error recreating TUF client with broken root: %v", err)
|
||||
|
||||
_, err = NewTufClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysBadVersionChecker)
|
||||
assert.Errorf(t, err, "Expected error creating TUF client with bad attest version: %v", err)
|
||||
_, err = NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysBadVersionChecker})
|
||||
assert.Errorf(t, err, "Expected error recreating TUF client with bad version checker")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadTarget(t *testing.T) {
|
||||
tufPath := CreateTempDir(t, "", "tuf_temp")
|
||||
targetFile := "test.txt"
|
||||
delegatedRole := "test-role"
|
||||
delegatedRole := testRole
|
||||
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)))
|
||||
server := httptest.NewServer(http.FileServer(http.Dir(HTTPTUFTestDataPath)))
|
||||
defer server.Close()
|
||||
|
||||
// run local registry
|
||||
@@ -97,9 +97,9 @@ func TestDownloadTarget(t *testing.T) {
|
||||
t.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
|
||||
}
|
||||
}()
|
||||
LoadRegistryTestData(t, regAddr, OciTufTestDataPath)
|
||||
LoadRegistryTestData(t, regAddr, OCITUFTestDataPath)
|
||||
|
||||
alwaysGoodVersionChecker := &mockVersionChecker{err: nil}
|
||||
alwaysGoodVersionChecker := &MockVersionChecker{err: nil}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -108,49 +108,53 @@ func TestDownloadTarget(t *testing.T) {
|
||||
}{
|
||||
{"http", server.URL + "/metadata", server.URL + "/targets"},
|
||||
{"oci", regAddr.Host + "/tuf-metadata:latest", regAddr.Host + "/tuf-targets"},
|
||||
{"http, download before init", server.URL + "/metadata", server.URL + "/targets"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tufClient, err := NewTufClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker)
|
||||
assert.NoErrorf(t, err, "Failed to create TUF client: %v", err)
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tufClient, err := NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
|
||||
require.NoErrorf(t, err, "Failed to create TUF client: %v", err)
|
||||
require.NotNil(t, tufClient.updater, "Failed to create updater")
|
||||
|
||||
// get trusted tuf metadata
|
||||
trustedMetadata := tufClient.updater.GetTrustedMetadataSet()
|
||||
assert.NotNil(t, trustedMetadata, "Failed to get trusted metadata")
|
||||
// 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 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)
|
||||
// download delegated target
|
||||
targetInfo, err := tufClient.updater.GetTargetInfo(delegatedTargetFile)
|
||||
require.NoError(t, err)
|
||||
_, err = tufClient.DownloadTarget(targetInfo.Path, filepath.Join(tufPath, targetInfo.Path))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEmbeddedTufRootBytes(t *testing.T) {
|
||||
dev, err := GetEmbeddedTufRoot("dev")
|
||||
dev, err := GetEmbeddedRoot("dev")
|
||||
assert.NoError(t, err)
|
||||
|
||||
staging, err := GetEmbeddedTufRoot("staging")
|
||||
staging, err := GetEmbeddedRoot("staging")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, dev.Data, staging.Data)
|
||||
|
||||
prod, err := GetEmbeddedTufRoot("prod")
|
||||
prod, err := GetEmbeddedRoot("prod")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, dev.Data, prod.Data)
|
||||
assert.NotEqual(t, staging.Data, prod.Data)
|
||||
|
||||
def, err := GetEmbeddedTufRoot("")
|
||||
def, err := GetEmbeddedRoot("")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, def.Data, prod.Data)
|
||||
|
||||
_, err = GetEmbeddedTufRoot("invalid")
|
||||
_, err = GetEmbeddedRoot("invalid")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ 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
|
||||
CheckVersion(tufClient Downloader) error
|
||||
}
|
||||
|
||||
type InvalidVersionError struct {
|
||||
@@ -32,13 +32,13 @@ func (e *InvalidVersionError) Error() string {
|
||||
return fmt.Sprintf("%s version %s does not satisfy constraints %s: %s", ThisModulePath, e.AttestVersion, e.VersionConstraint, errsStr.String())
|
||||
}
|
||||
|
||||
func NewVersionChecker() *versionChecker {
|
||||
return &versionChecker{}
|
||||
func NewDefaultVersionChecker() *DefaultVersionChecker {
|
||||
return &DefaultVersionChecker{}
|
||||
}
|
||||
|
||||
type versionChecker struct{}
|
||||
type DefaultVersionChecker struct{}
|
||||
|
||||
func (vc *versionChecker) CheckVersion(client TUFClient) error {
|
||||
func (vc *DefaultVersionChecker) CheckVersion(client Downloader) error {
|
||||
var attestMod *debug.Module
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
@@ -67,11 +67,11 @@ func (vc *versionChecker) CheckVersion(client TUFClient) error {
|
||||
|
||||
// see https://github.com/Masterminds/semver/blob/v3.2.1/README.md#checking-version-constraints
|
||||
// for more information on the expected format of the version constraints in the TUF repo
|
||||
_, versionConstraintsBytes, err := client.DownloadTarget("version-constraints", "")
|
||||
target, err := client.DownloadTarget("version-constraints", "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download version-constraints: %w", err)
|
||||
}
|
||||
versionConstraints, err := semver.NewConstraint(string(versionConstraintsBytes))
|
||||
versionConstraints, err := semver.NewConstraint(string(target.Data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse minimum version: %w", err)
|
||||
}
|
||||
|
||||
4
scripts/README.md
Normal file
4
scripts/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## scripts
|
||||
This directory contains project scripts.
|
||||
|
||||
`gen-testdata.sh` - used to generate static test data saved in `/test/testdata/`
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user