Compare commits
57 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 |
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -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"]
|
||||
|
||||
90
go.mod
90
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
|
||||
@@ -17,51 +17,50 @@ require (
|
||||
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.190.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.3 // 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.12 // indirect
|
||||
cloud.google.com/go/kms v1.18.4 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.11 // 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.1.0+incompatible // indirect
|
||||
github.com/docker/docker v27.1.1+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.1 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
@@ -148,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
|
||||
@@ -160,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
|
||||
@@ -181,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-20240730163845-b1a4ccb954bf // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // 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
|
||||
|
||||
214
go.sum
214
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.3 h1:98Vr+5jMaCZ5NZk6e/uBgf60phTk/XN84r8QEWB9yjY=
|
||||
cloud.google.com/go/auth v0.7.3/go.mod h1:HJtWUx1P5eqjy/f6Iq5KeytNpbAcGolPhOgyop2LlzA=
|
||||
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.12 h1:JixGLimRrNGcxvJEQ8+clfLxPlbeZA6MuRJ+qJNQ5Xw=
|
||||
cloud.google.com/go/iam v1.1.12/go.mod h1:9LDX8J7dN5YRyzVHxwQzrQs9opFFqn0Mxs9nAeB+Hhg=
|
||||
cloud.google.com/go/kms v1.18.4 h1:dYN3OCsQ6wJLLtOnI8DGUwQ5shMusXsWCCC+s09ATsk=
|
||||
cloud.google.com/go/kms v1.18.4/go.mod h1:SG1bgQ3UWW6/KdPo9uuJnzELXY5YTTMJtDYvajiQ22g=
|
||||
cloud.google.com/go/longrunning v0.5.11 h1:Havn1kGjz3whCfoD8dxMLP73Ph5w+ODyZB9RUsDxtGk=
|
||||
cloud.google.com/go/longrunning v0.5.11/go.mod h1:rDn7//lmlfWV1Dx6IB4RatCPenTwwmqXuiP0/RgoEO4=
|
||||
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.1.0+incompatible h1:rEHVQc4GZ0MIQKifQPHSFGV/dVgaZafgRf8fCPtDYBs=
|
||||
github.com/docker/docker v27.1.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
|
||||
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
|
||||
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
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=
|
||||
@@ -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=
|
||||
@@ -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.190.0 h1:ASM+IhLY1zljNdLu19W1jTmU6A+gMk6M46Wlur61s+Q=
|
||||
google.golang.org/api v0.190.0/go.mod h1:QIr6I9iedBLnfqoD6L6Vze1UvS5Hzj5r2aUBOaZnLHo=
|
||||
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-20240730163845-b1a4ccb954bf h1:OqdXDEakZCVtDiZTjcxfwbHPCT11ycCEsTKesBVKvyY=
|
||||
google.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:mCr1K1c8kX+1iSBREvU3Juo11CB+QOEWxbRS01wWl5M=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f h1:b1Ln/PG8orm0SsBbHZWke8dDp2lrCD4jSmfglFpTZbk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/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=
|
||||
|
||||
@@ -2,34 +2,25 @@ package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"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 (
|
||||
UseMockTL = true
|
||||
UseMockKMS = true
|
||||
UseMockPolicy = true
|
||||
UseMockTL = true
|
||||
UseMockKMS = true
|
||||
|
||||
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)
|
||||
@@ -64,15 +55,6 @@ func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) {
|
||||
|
||||
ctx := tlog.WithTL(context.Background(), tl)
|
||||
|
||||
var policyEvaluator policy.Evaluator
|
||||
if UseMockPolicy {
|
||||
policyEvaluator = policy.GetMockPolicy()
|
||||
} else {
|
||||
policyEvaluator = policy.NewRegoEvaluator(true)
|
||||
}
|
||||
|
||||
ctx = policy.WithPolicyEvaluator(ctx, policyEvaluator)
|
||||
|
||||
var signer dsse.SignerVerifier
|
||||
var err error
|
||||
if UseMockKMS {
|
||||
@@ -89,108 +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 i := range mfs2.Manifests {
|
||||
mf := &mfs2.Manifests[i]
|
||||
if mf.Annotations[attestation.DockerReferenceType] != "attestation-manifest" {
|
||||
continue
|
||||
}
|
||||
|
||||
attestationImage, err := idx.Image(mf.Digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
|
||||
}
|
||||
layers, err := attestationImage.Layers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err)
|
||||
}
|
||||
|
||||
for _, layer := range layers {
|
||||
// parse layer blob as json
|
||||
mt, err := layer.MediaType()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||
}
|
||||
|
||||
if string(mt) != mediaType {
|
||||
continue
|
||||
}
|
||||
r, err := layer.Uncompressed()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||
}
|
||||
defer r.Close()
|
||||
inTotoStatement := new(intoto.Statement)
|
||||
var desc *v1.Descriptor
|
||||
if strings.HasSuffix(string(mt), "+dsse") {
|
||||
env := new(attestation.Envelope)
|
||||
err = json.NewDecoder(r).Decode(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode env: %w", err)
|
||||
}
|
||||
payload, err := base64.StdEncoding.Strict().DecodeString(env.Payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode payload: %w", err)
|
||||
}
|
||||
err = json.Unmarshal([]byte(payload), inTotoStatement)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode %s statement: %w", mediaType, err)
|
||||
}
|
||||
} else {
|
||||
desc := new(v1.Descriptor)
|
||||
err = json.NewDecoder(r).Decode(desc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode statement: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
layerDesc, err := partial.Descriptor(layer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get descriptor for layer: %w", err)
|
||||
}
|
||||
annotations := make(map[string]string)
|
||||
for k, v := range layerDesc.Annotations {
|
||||
annotations[k] = v
|
||||
}
|
||||
statements = append(statements, &AnnotatedStatement{
|
||||
OCIDescriptor: desc,
|
||||
InTotoStatement: inTotoStatement,
|
||||
Annotations: annotations,
|
||||
})
|
||||
}
|
||||
}
|
||||
return statements, nil
|
||||
}
|
||||
|
||||
func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStatement, error) {
|
||||
idx, err := layout.ImageIndexFromPath(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load image index: %w", err)
|
||||
}
|
||||
|
||||
idxm, err := idx.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get digest: %w", err)
|
||||
}
|
||||
idxDigest := idxm.Manifests[0].Digest
|
||||
|
||||
mfs, err := idx.ImageIndex(idxDigest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
|
||||
}
|
||||
return ExtractStatementsFromIndex(mfs, mediaType)
|
||||
}
|
||||
|
||||
2
pkg/attest/README.md
Normal file
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)
|
||||
}
|
||||
@@ -70,7 +69,7 @@ func ExampleSignStatements_remote() {
|
||||
},
|
||||
},
|
||||
})
|
||||
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.Client, error) {
|
||||
// using oci tuf metadata and targets
|
||||
metadataURI := "registry-1.docker.io/docker/tuf-metadata:latest"
|
||||
targetsURI := "registry-1.docker.io/docker/tuf-targets"
|
||||
// example using http tuf metadata and targets
|
||||
// metadataURI := "https://docker.github.io/tuf-staging/metadata"
|
||||
// targetsURI := "https://docker.github.io/tuf-staging/targets"
|
||||
|
||||
return tuf.NewClient(embed.RootStaging.Data, outputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker())
|
||||
}
|
||||
|
||||
func ExampleVerify_remote() {
|
||||
// create a tuf client
|
||||
home, err := os.UserHomeDir()
|
||||
@@ -31,10 +19,7 @@ func ExampleVerify_remote() {
|
||||
panic(err)
|
||||
}
|
||||
tufOutputPath := filepath.Join(home, ".docker", "tuf")
|
||||
tufClient, err := createTufClient(tufOutputPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tufClientOpts := tuf.NewDockerDefaultClientOptions(tufOutputPath)
|
||||
|
||||
// create a resolver for remote attestations
|
||||
image := "registry-1.docker.io/library/notary:server"
|
||||
@@ -42,10 +27,11 @@ func ExampleVerify_remote() {
|
||||
|
||||
// configure policy options
|
||||
opts := &policy.Options{
|
||||
TUFClient: tufClient,
|
||||
LocalTargetsDir: filepath.Join(home, ".docker", "policy"), // location to store policy files downloaded from TUF
|
||||
LocalPolicyDir: "", // overrides TUF policy for local policy files if set
|
||||
PolicyID: "", // set to ignore policy mapping and select a policy by id
|
||||
TUFClientOptions: tufClientOpts,
|
||||
LocalTargetsDir: filepath.Join(home, ".docker", "policy"), // location to store policy files downloaded from TUF
|
||||
LocalPolicyDir: "", // overrides TUF policy for local policy files if set
|
||||
PolicyID: "", // set to ignore policy mapping and select a policy by id
|
||||
DisableTUF: false, // set to disable TUF and rely on local policy files
|
||||
}
|
||||
|
||||
src, err := oci.ParseImageSpec(image, oci.WithPlatform(platform))
|
||||
|
||||
@@ -12,14 +12,14 @@ import (
|
||||
// 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,22 +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/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"
|
||||
@@ -24,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"
|
||||
)
|
||||
|
||||
@@ -43,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.Options{
|
||||
LocalPolicyDir: PassPolicyDir,
|
||||
DisableTUF: true,
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -65,16 +58,16 @@ func TestSignVerifyOCILayout(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
spec, err := oci.ParseImageSpec(oci.LocalPrefix + outputLayout)
|
||||
require.NoError(t, err)
|
||||
err = mirror.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name)
|
||||
err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name)
|
||||
require.NoError(t, err)
|
||||
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...)
|
||||
|
||||
@@ -84,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.Layer{
|
||||
Layer: testLayer,
|
||||
Statement: &intoto.Statement{
|
||||
StatementHeader: intoto.StatementHeader{
|
||||
PredicateType: attestation.VSAPredicateType,
|
||||
},
|
||||
},
|
||||
Annotations: map[string]string{"test": "test"},
|
||||
}
|
||||
|
||||
manifest := &attestation.Manifest{
|
||||
OriginalDescriptor: &v1.Descriptor{
|
||||
MediaType: mediaType,
|
||||
},
|
||||
OriginalLayers: []*attestation.Layer{
|
||||
originalLayer,
|
||||
},
|
||||
SubjectDescriptor: &v1.Descriptor{},
|
||||
}
|
||||
err := manifest.AddAttestation(ctx, signer, originalLayer.Statement, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
newImg, err := manifest.BuildAttestationImage(attestation.WithReplacedLayers(tc.replace))
|
||||
require.NoError(t, err)
|
||||
mf, _ := newImg.RawManifest()
|
||||
type Annotations struct {
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
}
|
||||
type Layers struct {
|
||||
Layers []Annotations `json:"layers"`
|
||||
}
|
||||
l := &Layers{}
|
||||
err = json.Unmarshal(mf, l)
|
||||
require.NoError(t, err)
|
||||
_, ok := l.Layers[0].Annotations["test"]
|
||||
assert.Truef(t, ok, "missing annotations")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleStatementSigning(t *testing.T) {
|
||||
ctx, signer := test.Setup(t)
|
||||
empty := types.MediaType("application/vnd.oci.empty.v1+json")
|
||||
testCases := []struct {
|
||||
name string
|
||||
replace bool
|
||||
}{
|
||||
{"replaced", true},
|
||||
{"not replaced", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
opts := &attestation.SigningOptions{}
|
||||
statement := &intoto.Statement{
|
||||
StatementHeader: intoto.StatementHeader{
|
||||
PredicateType: attestation.VSAPredicateType,
|
||||
},
|
||||
}
|
||||
statement2 := &intoto.Statement{
|
||||
StatementHeader: intoto.StatementHeader{
|
||||
PredicateType: attestation.VSAPredicateType,
|
||||
},
|
||||
}
|
||||
digest, err := v1.NewHash("sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620")
|
||||
require.NoError(t, err)
|
||||
subject := &v1.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Digest: digest,
|
||||
}
|
||||
manifest, err := NewAttestationManifest(subject)
|
||||
require.NoError(t, err)
|
||||
err = manifest.AddAttestation(ctx, signer, statement, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = manifest.AddAttestation(ctx, signer, statement2, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
// fake that the manfifest was loaded from a real image
|
||||
manifest.OriginalLayers = manifest.SignedLayers
|
||||
envelopes, err := oci.ExtractEnvelopes(manifest, attestation.VSAPredicateType)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, envelopes, 2)
|
||||
|
||||
newImg, err := manifest.BuildAttestationImage(attestation.WithReplacedLayers(tc.replace))
|
||||
require.NoError(t, err)
|
||||
layers, err := newImg.Layers()
|
||||
require.NoError(t, err)
|
||||
if tc.replace {
|
||||
assert.Len(t, layers, 2)
|
||||
} else {
|
||||
assert.Len(t, layers, 4)
|
||||
}
|
||||
|
||||
newImgs, err := manifest.BuildReferringArtifacts()
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, newImgs, 2)
|
||||
for _, img := range newImgs {
|
||||
mf, err := img.Manifest()
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, mf.ArtifactType, "application/vnd.in-toto")
|
||||
assert.Contains(t, mf.ArtifactType, "+dsse")
|
||||
assert.Equal(t, subject.MediaType, mf.MediaType)
|
||||
assert.Equal(t, empty, mf.Config.MediaType)
|
||||
assert.Equal(t, int64(2), mf.Config.Size)
|
||||
assert.Equal(t, "{}", string(mf.Config.Data))
|
||||
layers, err := img.Layers()
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, layers, 1)
|
||||
}
|
||||
server := httptest.NewServer(registry.New(registry.WithReferrersSupport(true)))
|
||||
defer server.Close()
|
||||
|
||||
u, err := url.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||
output, err := oci.ParseImageSpecs(indexName)
|
||||
require.NoError(t, err)
|
||||
err = mirror.SaveReferrers(manifest, output)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package attest
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -11,56 +13,124 @@ 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.Options) (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 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 {
|
||||
@@ -87,6 +157,8 @@ func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vsaPolicy := attestation.VSAPolicy{URI: result.Summary.PolicyURI, DownloadLocation: p.URI, Digest: p.Digest}
|
||||
|
||||
return &VerificationResult{
|
||||
Policy: p,
|
||||
Outcome: outcome,
|
||||
@@ -104,7 +176,7 @@ func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.
|
||||
},
|
||||
TimeVerified: time.Now().UTC().Format(time.RFC3339),
|
||||
ResourceURI: resourceURI,
|
||||
Policy: attestation.VSAPolicy{URI: result.Summary.PolicyURI},
|
||||
Policy: vsaPolicy,
|
||||
VerificationResult: outcomeStr,
|
||||
VerifiedLevels: result.Summary.SLSALevels,
|
||||
},
|
||||
@@ -112,7 +184,7 @@ func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.
|
||||
}, 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.Input{
|
||||
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.Manifest, error) {
|
||||
return &attestation.Manifest{
|
||||
OriginalDescriptor: &v1.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
},
|
||||
OriginalLayers: []*attestation.Layer{},
|
||||
SubjectDescriptor: subject,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -8,12 +8,13 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/config"
|
||||
"github.com/docker/attest/pkg/mirror"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/policy"
|
||||
"github.com/docker/attest/pkg/tuf"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -32,7 +33,7 @@ func TestVerifyAttestations(t *testing.T) {
|
||||
env := new(attestation.Envelope)
|
||||
err = json.Unmarshal(ex, env)
|
||||
assert.NoError(t, err)
|
||||
resolver := &oci.MockResolver{
|
||||
resolver := &attestation.MockResolver{
|
||||
Envs: []*attestation.Envelope{env},
|
||||
}
|
||||
|
||||
@@ -44,17 +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(_ context.Context, _ oci.AttestationResolver, _ *policy.Policy, _ *policy.Input) (*policy.Result, error) {
|
||||
EvaluateFunc: func(_ context.Context, _ attestation.Resolver, _ *policy.Policy, _ *policy.Input) (*policy.Result, error) {
|
||||
return policy.AllowedResult(), tc.policyEvaluationError
|
||||
},
|
||||
}
|
||||
|
||||
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())
|
||||
@@ -68,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)
|
||||
@@ -84,13 +83,14 @@ func TestVSA(t *testing.T) {
|
||||
// output signed attestations
|
||||
spec, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64))
|
||||
require.NoError(t, err)
|
||||
err = mirror.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name)
|
||||
err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// mocked vsa query should pass
|
||||
policyOpts := &policy.Options{
|
||||
LocalPolicyDir: PassPolicyDir,
|
||||
AttestationStyle: config.AttestationStyleAttached,
|
||||
DisableTUF: true,
|
||||
}
|
||||
results, err := Verify(ctx, spec, policyOpts)
|
||||
require.NoError(t, err)
|
||||
@@ -99,7 +99,7 @@ func TestVSA(t *testing.T) {
|
||||
|
||||
if assert.NotNil(t, results.Input) {
|
||||
assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", results.Input.Digest)
|
||||
assert.False(t, results.Input.IsCanonical)
|
||||
assert.NotNil(t, results.Input.Tag)
|
||||
}
|
||||
|
||||
assert.Equal(t, intoto.StatementInTotoV01, results.VSA.Type)
|
||||
@@ -113,17 +113,18 @@ func TestVSA(t *testing.T) {
|
||||
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)
|
||||
@@ -134,13 +135,14 @@ func TestVerificationFailure(t *testing.T) {
|
||||
// output signed attestations
|
||||
spec, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64))
|
||||
require.NoError(t, err)
|
||||
err = mirror.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name)
|
||||
err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// mocked vsa query should fail
|
||||
policyOpts := &policy.Options{
|
||||
LocalPolicyDir: FailPolicyDir,
|
||||
AttestationStyle: config.AttestationStyleAttached,
|
||||
DisableTUF: true,
|
||||
}
|
||||
results, err := Verify(ctx, spec, policyOpts)
|
||||
require.NoError(t, err)
|
||||
@@ -163,30 +165,32 @@ func TestVerificationFailure(t *testing.T) {
|
||||
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 {
|
||||
@@ -208,24 +212,98 @@ func TestSignVerify(t *testing.T) {
|
||||
// output signed attestations
|
||||
spec, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64))
|
||||
require.NoError(t, err)
|
||||
err = mirror.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, imageName)
|
||||
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)
|
||||
if tc.expectError {
|
||||
require.Error(t, err)
|
||||
require.NoError(t, err)
|
||||
if tc.expectedNonSuccess != "" {
|
||||
assert.Equal(t, tc.expectedNonSuccess, results.Outcome)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, OutcomeSuccess, results.Outcome)
|
||||
platform, err := oci.ParsePlatform(LinuxAMD64)
|
||||
require.NoError(t, err)
|
||||
expectedPURL, _, err := oci.RefToPURL(attIdx.Name, platform)
|
||||
|
||||
ref, err := reference.ParseNormalizedNamed(attIdx.Name)
|
||||
require.NoError(t, err)
|
||||
expectedPURL, _, err := oci.RefToPURL(ref, platform)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedPURL, results.Input.PURL)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultOptions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
tufOpts *tuf.ClientOptions
|
||||
localTargetsDir string
|
||||
attestationStyle config.AttestationStyle
|
||||
referrersRepo string
|
||||
expectedError string
|
||||
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,8 +21,19 @@ import (
|
||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||
)
|
||||
|
||||
// GetAttestationManifestsFromIndex extracts all attestation manifests from an index.
|
||||
func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, 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)
|
||||
@@ -41,7 +56,7 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, error)
|
||||
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)
|
||||
}
|
||||
@@ -56,8 +71,8 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, error)
|
||||
return attestationManifests, nil
|
||||
}
|
||||
|
||||
// GetAttestationsFromImage extracts all attestation layers from an image.
|
||||
func GetAttestationsFromImage(image v1.Image) ([]*Layer, 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)
|
||||
@@ -93,7 +108,7 @@ func GetAttestationsFromImage(image v1.Image) ([]*Layer, error) {
|
||||
return attestationLayers, nil
|
||||
}
|
||||
|
||||
func (manifest *Manifest) AddAttestation(ctx context.Context, signer dsse.SignerVerifier, statement *intoto.Statement, opts *SigningOptions) error {
|
||||
func (manifest *Manifest) Add(ctx context.Context, signer dsse.SignerVerifier, statement *intoto.Statement, opts *SigningOptions) error {
|
||||
layer, err := createSignedImageLayer(ctx, statement, signer, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create signed layer: %w", err)
|
||||
@@ -104,7 +119,7 @@ func (manifest *Manifest) AddAttestation(ctx context.Context, signer dsse.Signer
|
||||
|
||||
func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*Layer, error) {
|
||||
// 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)
|
||||
}
|
||||
@@ -127,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)
|
||||
@@ -139,12 +154,12 @@ func SignInTotoStatement(ctx context.Context, statement *intoto.Statement, signe
|
||||
return env, nil
|
||||
}
|
||||
|
||||
func UpdateIndexImage(
|
||||
func updateImageIndex(
|
||||
idx v1.ImageIndex,
|
||||
manifest *Manifest,
|
||||
options ...func(*ManifestImageOptions) error,
|
||||
) (v1.ImageIndex, error) {
|
||||
image, err := manifest.BuildAttestationImage(options...)
|
||||
image, err := manifest.BuildImage(options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build image: %w", err)
|
||||
}
|
||||
@@ -169,7 +184,7 @@ func UpdateIndexImage(
|
||||
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)
|
||||
}
|
||||
@@ -203,7 +218,7 @@ func WithReplacedLayers(replaceLayers bool) func(*ManifestImageOptions) error {
|
||||
}
|
||||
|
||||
// build an image with signed attestations, optionally replacing existing layers with signed layers.
|
||||
func (manifest *Manifest) BuildAttestationImage(options ...func(*ManifestImageOptions) error) (v1.Image, error) {
|
||||
func (manifest *Manifest) BuildImage(options ...func(*ManifestImageOptions) error) (v1.Image, error) {
|
||||
opts, err := newOptions(options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create options: %w", err)
|
||||
@@ -228,7 +243,7 @@ func (manifest *Manifest) BuildAttestationImage(options ...func(*ManifestImageOp
|
||||
}
|
||||
// so that we attach all attestations to a single attestations image - as per current buildkit
|
||||
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)
|
||||
}
|
||||
@@ -240,7 +255,7 @@ func (manifest *Manifest) BuildReferringArtifacts() ([]v1.Image, error) {
|
||||
var images []v1.Image
|
||||
for _, layer := range manifest.SignedLayers {
|
||||
opts := &ManifestImageOptions{}
|
||||
newImg, err := buildImage([]*Layer{layer}, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts)
|
||||
newImg, err := buildImageFromLayers([]*Layer{layer}, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build image: %w", err)
|
||||
}
|
||||
@@ -249,8 +264,8 @@ func (manifest *Manifest) BuildReferringArtifacts() ([]v1.Image, error) {
|
||||
return images, nil
|
||||
}
|
||||
|
||||
// build and image containing only layers.
|
||||
func buildImage(layers []*Layer, manifest *v1.Descriptor, subject *v1.Descriptor, opts *ManifestImageOptions) (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 {
|
||||
@@ -295,37 +310,135 @@ func buildImage(layers []*Layer, manifest *v1.Descriptor, subject *v1.Descriptor
|
||||
}
|
||||
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,21 +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"
|
||||
@@ -63,13 +61,13 @@ func ExampleManifest() {
|
||||
}
|
||||
|
||||
// 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 ExampleManifest() {
|
||||
}
|
||||
|
||||
// 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,36 +1,35 @@
|
||||
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.
|
||||
// implementation of Resolver that closes over attestations from an oci layout.
|
||||
type LayoutResolver struct {
|
||||
*attestation.Manifest
|
||||
*ImageSpec
|
||||
*Manifest
|
||||
*oci.ImageSpec
|
||||
}
|
||||
|
||||
func NewOCILayoutAttestationResolver(src *ImageSpec) (*LayoutResolver, error) {
|
||||
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 *LayoutResolver) fetchAttestationManifest() (*attestation.Manifest, error) {
|
||||
func (r *LayoutResolver) fetchManifest() (*Manifest, error) {
|
||||
if r.Manifest == nil {
|
||||
m, err := attestationManifestFromOCILayout(r.Identifier, r.ImageSpec.Platform)
|
||||
m, err := manifestFromOCILayout(r.Identifier, r.ImageSpec.Platform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -40,9 +39,9 @@ func (r *LayoutResolver) fetchAttestationManifest() (*attestation.Manifest, erro
|
||||
return r.Manifest, nil
|
||||
}
|
||||
|
||||
func (r *LayoutResolver) Attestations(_ 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)
|
||||
}
|
||||
@@ -55,7 +54,7 @@ func (r *LayoutResolver) Attestations(_ context.Context, predicateType string) (
|
||||
if mts != dsseMediaType {
|
||||
continue
|
||||
}
|
||||
env := new(att.Envelope)
|
||||
env := new(Envelope)
|
||||
// parse layer blob as json
|
||||
r, err := attestationLayer.Layer.Uncompressed()
|
||||
if err != nil {
|
||||
@@ -83,7 +82,7 @@ func (r *LayoutResolver) ImagePlatform(_ context.Context) (*v1.Platform, error)
|
||||
return r.ImageSpec.Platform, nil
|
||||
}
|
||||
|
||||
func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*attestation.Manifest, error) {
|
||||
func manifestFromOCILayout(path string, platform *v1.Platform) (*Manifest, error) {
|
||||
idx, err := layout.ImageIndexFromPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -120,11 +119,11 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*atte
|
||||
}
|
||||
for i := range mfs2.Manifests {
|
||||
mf := &mfs2.Manifests[i]
|
||||
if mf.Annotations[att.DockerReferenceType] != attestation.AttestationManifestType {
|
||||
if mf.Annotations[DockerReferenceType] != AttestationManifestType {
|
||||
continue
|
||||
}
|
||||
|
||||
if mf.Annotations[att.DockerReferenceDigest] != subjectDescriptor.Digest.String() {
|
||||
if mf.Annotations[DockerReferenceDigest] != subjectDescriptor.Digest.String() {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -132,11 +131,11 @@ 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.Manifest{
|
||||
attest := &Manifest{
|
||||
OriginalLayers: layers,
|
||||
OriginalDescriptor: mf,
|
||||
SubjectName: idxDescriptor.Annotations["org.opencontainers.image.ref.name"],
|
||||
@@ -1,4 +1,4 @@
|
||||
package oci_test
|
||||
package attestation_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -7,7 +7,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"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
@@ -25,7 +24,7 @@ func TestAttestationFromOCILayout(t *testing.T) {
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -34,7 +33,7 @@ func TestAttestationFromOCILayout(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
spec, err := oci.ParseImageSpec(oci.LocalPrefix + outputLayout)
|
||||
require.NoError(t, err)
|
||||
err = mirror.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, outputLayout)
|
||||
err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, outputLayout)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
@@ -1,17 +1,17 @@
|
||||
package oci
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
)
|
||||
|
||||
type MockResolver struct {
|
||||
Envs []*attestation.Envelope
|
||||
Envs []*Envelope
|
||||
}
|
||||
|
||||
func (r MockResolver) Attestations(_ context.Context, _ string) ([]*attestation.Envelope, error) {
|
||||
func (r MockResolver) Attestations(_ context.Context, _ string) ([]*Envelope, error) {
|
||||
return r.Envs, nil
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func (r MockResolver) ImageDescriptor(_ context.Context) (*v1.Descriptor, error)
|
||||
}
|
||||
|
||||
func (r MockResolver) ImagePlatform(_ context.Context) (*v1.Platform, error) {
|
||||
return ParsePlatform("linux/amd64")
|
||||
return oci.ParsePlatform("linux/amd64")
|
||||
}
|
||||
|
||||
type MockRegistryResolver struct {
|
||||
@@ -1,22 +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,
|
||||
}
|
||||
@@ -36,8 +35,8 @@ func WithReferrersRepo(repo string) func(*ReferrersResolver) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateType string) ([]*attestation.Manifest, 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", strings.TrimPrefix(r.referrersRepo, RegistryPrefix), 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,16 +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.Manifest, 0)
|
||||
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)
|
||||
}
|
||||
@@ -99,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.Manifest{
|
||||
attest := &Manifest{
|
||||
SubjectName: imageName,
|
||||
OriginalLayers: layers,
|
||||
OriginalDescriptor: &m,
|
||||
@@ -110,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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -139,6 +138,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
|
||||
|
||||
policyOpts := &policy.Options{
|
||||
LocalPolicyDir: LocalPolicy,
|
||||
DisableTUF: true,
|
||||
}
|
||||
|
||||
if tc.referrersRepo != "" {
|
||||
@@ -212,11 +212,11 @@ func TestReferencesInDifferentRepo(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)
|
||||
err = oci.PushIndexToRegistry(attIdx.Index, indexName)
|
||||
require.NoError(t, err)
|
||||
|
||||
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
||||
@@ -225,9 +225,9 @@ 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
|
||||
@@ -238,11 +238,11 @@ func TestReferencesInDifferentRepo(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)
|
||||
err = oci.PushIndexToRegistry(attIdx.Index, indexName)
|
||||
require.NoError(t, err)
|
||||
|
||||
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
||||
@@ -254,7 +254,7 @@ 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)
|
||||
}
|
||||
}
|
||||
@@ -269,6 +269,7 @@ func TestReferencesInDifferentRepo(t *testing.T) {
|
||||
referencedImage := fmt.Sprintf("%s@%s", indexName, mf.Digest.String())
|
||||
policyOpts := &policy.Options{
|
||||
LocalPolicyDir: PassPolicyDir,
|
||||
DisableTUF: true,
|
||||
}
|
||||
src, err := oci.ParseImageSpec(referencedImage)
|
||||
require.NoError(t, err)
|
||||
@@ -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)
|
||||
err = oci.PushIndexToRegistry(attIdx.Index, indexName)
|
||||
require.NoError(t, err)
|
||||
|
||||
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
||||
@@ -308,7 +309,7 @@ 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)
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,12 @@ type Extension struct {
|
||||
Ext *DockerDSSEExtension `json:"ext"`
|
||||
}
|
||||
|
||||
type AnnotatedStatement struct {
|
||||
OCIDescriptor *v1.Descriptor
|
||||
InTotoStatement *intoto.Statement
|
||||
Annotations map[string]string
|
||||
}
|
||||
|
||||
type DockerDSSEExtension struct {
|
||||
TL *DockerTLExtension `json:"tl"`
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -16,7 +16,7 @@ type VSAPredicate struct {
|
||||
TimeVerified string `json:"timeVerified"`
|
||||
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 {
|
||||
|
||||
@@ -36,13 +36,13 @@ func LoadTUFMappings(tufClient tuf.Downloader, localTargetsDir string) (*PolicyM
|
||||
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)
|
||||
}
|
||||
|
||||
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,8 +6,8 @@ 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"
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"
|
||||
@@ -17,7 +18,7 @@ import (
|
||||
// -----------------
|
||||
|
||||
// GetMetadataManifest returns an image with TUF root metadata as layers.
|
||||
func (m *TUFMirror) GetMetadataManifest(metadataURL string) (v1.Image, error) {
|
||||
func (m *TUFMirror) GetMetadataManifest(metadataURL string) (*oci.EmptyConfigImage, error) {
|
||||
metadata, err := m.getMetadataMirror(metadataURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get metadata: %w", err)
|
||||
@@ -26,7 +27,7 @@ func (m *TUFMirror) GetMetadataManifest(metadataURL string) (v1.Image, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build metadata manifest: %w", err)
|
||||
}
|
||||
return manifest, nil
|
||||
return &oci.EmptyConfigImage{Image: manifest}, nil
|
||||
}
|
||||
|
||||
// getMetadataMirror returns a TufMetadata struct with TUF metadata as map of file names to bytes.
|
||||
@@ -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, &Image{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.getMetadataMirror(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,157 +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) {
|
||||
if root == nil {
|
||||
root = embed.RootDefault.Data
|
||||
root = tuf.DockerTUFRootDefault.Data
|
||||
}
|
||||
tufClient, err := tuf.NewClient(root, tufPath, metadataURL, targetsURL, versionChecker)
|
||||
tufClient, err := tuf.NewClient(&tuf.ClientOptions{InitialRoot: root, Path: tufPath, MetadataSource: metadataURL, TargetsSource: targetsURL, VersionChecker: versionChecker})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create TUF client: %w", err)
|
||||
}
|
||||
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.Manifest, outputs []*oci.ImageSpec) error {
|
||||
for _, output := range outputs {
|
||||
// OCI layout output for referrers not supported
|
||||
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 := PushImageToRegistry(image, attOut.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to push image: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return 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"
|
||||
@@ -22,7 +23,7 @@ func (m *TUFMirror) GetTUFTargetMirrors() ([]*Image, error) {
|
||||
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,12 +38,12 @@ func (m *TUFMirror) GetTUFTargetMirrors() ([]*Image, 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, &Image{Image: img, Tag: name})
|
||||
targetMirrors = append(targetMirrors, &Image{Image: &oci.EmptyConfigImage{Image: img}, Tag: name})
|
||||
}
|
||||
return targetMirrors, nil
|
||||
}
|
||||
@@ -68,7 +69,7 @@ func (m *TUFMirror) GetDelegatedTargetMirrors() ([]*Index, error) {
|
||||
// for each target file, create an image with the target file as a layer
|
||||
for _, 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() ([]*Index, 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),
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/embed"
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/pkg/tuf"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -27,7 +26,7 @@ func TestGetTufTargetsMirror(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
path := test.CreateTempDir(t, "", "tuf_temp")
|
||||
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/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()
|
||||
@@ -61,7 +60,7 @@ func TestTargetDelegationMetadata(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
path := test.CreateTempDir(t, "", "tuf_temp")
|
||||
tm, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/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")
|
||||
@@ -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"
|
||||
@@ -32,7 +33,7 @@ type DelegatedTargetMetadata struct {
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
Image v1.Image
|
||||
Image *oci.EmptyConfigImage
|
||||
Tag 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)
|
||||
@@ -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"
|
||||
@@ -45,36 +42,7 @@ func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
|
||||
return options
|
||||
}
|
||||
|
||||
func ExtractEnvelopes(manifest *attestation.Manifest, 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()
|
||||
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) {
|
||||
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) {
|
||||
@@ -84,24 +52,8 @@ func imageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descripto
|
||||
return nil, fmt.Errorf("no image found for platform %v", platform)
|
||||
}
|
||||
|
||||
func attestationDigestForDigest(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) {
|
||||
for i := range ix.Manifests {
|
||||
m := &ix.Manifests[i]
|
||||
if v, ok := m.Annotations[att.DockerReferenceType]; ok && v == attestType {
|
||||
if d, ok := m.Annotations[att.DockerReferenceDigest]; ok && d == imageDigest {
|
||||
return m.Digest.String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no attestation found for image %s", imageDigest)
|
||||
}
|
||||
|
||||
func RefToPURL(ref string, platform *v1.Platform) (string, bool, error) {
|
||||
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 {
|
||||
@@ -150,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)
|
||||
}
|
||||
@@ -162,7 +114,7 @@ func ReplaceTagInSpec(src *ImageSpec, digest v1.Hash) (*ImageSpec, error) {
|
||||
}
|
||||
|
||||
// so that the index tag is replaced with a tag unique to the image digest and doesn't overwrite it.
|
||||
func replaceTag(image string, digest v1.Hash) (string, error) {
|
||||
func ReplaceTag(image string, digest v1.Hash) (string, error) {
|
||||
if strings.HasPrefix(image, LocalPrefix) {
|
||||
return image, nil
|
||||
}
|
||||
@@ -172,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,54 +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()
|
||||
@@ -72,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)
|
||||
@@ -95,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: RegistryPrefix + "image:tag", expected: RegistryPrefix + "index.docker.io/library/image"},
|
||||
{name: oci.RegistryPrefix + "image:tag", expected: oci.RegistryPrefix + "index.docker.io/library/image"},
|
||||
{name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image"},
|
||||
{name: RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: RegistryPrefix + "index.docker.io/library/image"},
|
||||
{name: RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: RegistryPrefix + "127.0.0.1:36555/repo"},
|
||||
{name: oci.RegistryPrefix + "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)
|
||||
})
|
||||
}
|
||||
@@ -116,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: RegistryPrefix + "image:tag", expected: RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: oci.RegistryPrefix + "image:tag", expected: oci.RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: LocalPrefix + "foobar", expected: LocalPrefix + "foobar"},
|
||||
{name: RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: RegistryPrefix + "127.0.0.1:36555/repo:sha256-digest.att"},
|
||||
{name: oci.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{
|
||||
@@ -129,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,12 +30,12 @@ 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.LocalPrefix + outputLayout)
|
||||
require.NoError(t, err)
|
||||
err = SaveIndex(ociOutput, attIdx.Index, indexName)
|
||||
err = oci.SaveIndex(ociOutput, attIdx.Index, indexName)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -56,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.LocalPrefix + outputLayout)
|
||||
require.NoError(t, err)
|
||||
err = SaveImage(ociOutput, img, indexName)
|
||||
err = oci.SaveImage(ociOutput, img, indexName)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -80,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()
|
||||
@@ -93,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 := &oci.MockRegistryResolver{
|
||||
reg := &attestation.MockRegistryResolver{
|
||||
Subject: subject,
|
||||
MockResolver: &oci.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.Manifest
|
||||
}
|
||||
|
||||
type RegistryImageDetailsResolver struct {
|
||||
*ImageSpec
|
||||
descriptor *v1.Descriptor
|
||||
@@ -27,12 +20,6 @@ func NewRegistryImageDetailsResolver(src *ImageSpec) (*RegistryImageDetailsResol
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewRegistryAttestationResolver(src *RegistryImageDetailsResolver) (*RegistryResolver, error) {
|
||||
return &RegistryResolver{
|
||||
RegistryImageDetailsResolver: src,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *RegistryImageDetailsResolver) ImageName(_ context.Context) (string, error) {
|
||||
return r.Identifier, 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.Manifest == nil {
|
||||
attest, err := FetchAttestationManifest(ctx, r.Identifier, r.ImageSpec.Platform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Manifest = attest
|
||||
}
|
||||
return ExtractEnvelopes(r.Manifest, predicateType)
|
||||
}
|
||||
|
||||
func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Platform) (*attestation.Manifest, error) {
|
||||
// we want to get to the image index, so ignoring platform for now
|
||||
options := WithOptions(ctx, nil)
|
||||
ref, err := name.ParseReference(image)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse reference: %w", err)
|
||||
}
|
||||
index, err := remote.Index(ref, options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get index: %w", err)
|
||||
}
|
||||
indexManifest, err := index.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get index manifest: %w", err)
|
||||
}
|
||||
subjectDescriptor, err := imageDescriptor(indexManifest, platform)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to obtain image for platform: %w", err)
|
||||
}
|
||||
|
||||
digest := subjectDescriptor.Digest.String()
|
||||
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), digest))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
||||
}
|
||||
|
||||
attestationDigest, err := attestationDigestForDigest(indexManifest, digest, "attestation-manifest")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to obtain attestation for image: %w", err)
|
||||
}
|
||||
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), attestationDigest))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
||||
}
|
||||
remoteDescriptor, err := remote.Get(ref, options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestation: %w", err)
|
||||
}
|
||||
attestationImage, err := remoteDescriptor.Image()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestation image: %w", err)
|
||||
}
|
||||
|
||||
layers, err := attestation.GetAttestationsFromImage(attestationImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
|
||||
}
|
||||
attest := &attestation.Manifest{
|
||||
OriginalLayers: layers,
|
||||
OriginalDescriptor: &remoteDescriptor.Descriptor,
|
||||
SubjectName: image,
|
||||
SubjectDescriptor: subjectDescriptor,
|
||||
}
|
||||
return attest, nil
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -26,12 +28,6 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
type AttestationOptions struct {
|
||||
NoReferrers bool
|
||||
Attach bool
|
||||
ReferrersRepo string
|
||||
}
|
||||
|
||||
type ImageSpecOption func(*ImageSpec) error
|
||||
|
||||
type ImageSpec struct {
|
||||
@@ -180,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 Evaluator) context.Context {
|
||||
return context.WithValue(ctx, PolicyEvaluatorCtxKey, pe)
|
||||
}
|
||||
|
||||
// gets PolicyEvaluator from context, defaults to Rego PolicyEvaluator if not set.
|
||||
func GetPolicyEvaluator(ctx context.Context) (Evaluator, error) {
|
||||
t, ok := ctx.Value(PolicyEvaluatorCtxKey).(Evaluator)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no policy evaluator client set on context (set one with policy.WithPolicyEvaluator)")
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
type Evaluator interface {
|
||||
Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *Input) (*Result, error)
|
||||
Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -3,14 +3,14 @@ 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 *Input) (*Result, error)
|
||||
EvaluateFunc func(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error)
|
||||
}
|
||||
|
||||
func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *Input) (*Result, error) {
|
||||
func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error) {
|
||||
if pe.EvaluateFunc != nil {
|
||||
return pe.EvaluateFunc(ctx, resolver, pctx, input)
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver oci.Attest
|
||||
|
||||
func GetMockPolicy() Evaluator {
|
||||
return &MockPolicyEvaluator{
|
||||
EvaluateFunc: func(_ context.Context, _ oci.AttestationResolver, _ *Policy, _ *Input) (*Result, error) {
|
||||
EvaluateFunc: func(_ context.Context, _ attestation.Resolver, _ *Policy, _ *Input) (*Result, error) {
|
||||
return AllowedResult(), nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 *Options, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
|
||||
if opts.LocalPolicyDir == "" {
|
||||
return nil, fmt.Errorf("local policy dir not set")
|
||||
}
|
||||
files := make([]*File, 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, &File{
|
||||
Path: filename,
|
||||
Content: fileContents,
|
||||
})
|
||||
}
|
||||
policy := &Policy{
|
||||
InputFiles: files,
|
||||
Mapping: mapping,
|
||||
}
|
||||
if imageName != matchedName {
|
||||
policy.ResolvedName = matchedName
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func resolveTUFPolicy(opts *Options, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) {
|
||||
files := make([]*File, 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, &File{
|
||||
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 *Options) (*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 *Options) (*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) {
|
||||
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 oci.NewRegistryAttestationResolver(resolver)
|
||||
case *oci.LayoutResolver:
|
||||
return attestation.NewRegistryResolver(resolver)
|
||||
case *attestation.LayoutResolver:
|
||||
return resolver, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver)
|
||||
}
|
||||
}
|
||||
if mapping.Attestations.Repo != "" {
|
||||
return oci.NewReferrersAttestationResolver(resolver, oci.WithReferrersRepo(mapping.Attestations.Repo))
|
||||
return attestation.NewReferrersResolver(resolver, attestation.WithReferrersRepo(mapping.Attestations.Repo))
|
||||
}
|
||||
}
|
||||
return oci.NewReferrersAttestationResolver(resolver)
|
||||
return attestation.NewReferrersResolver(resolver)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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 := oci.MockResolver{
|
||||
defaultResolver := attestation.MockResolver{
|
||||
Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
repo string
|
||||
expectSuccess bool
|
||||
isCanonical bool
|
||||
resolver oci.AttestationResolver
|
||||
policy *policy.Options
|
||||
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) {
|
||||
t.Run(tc.policyPath, func(t *testing.T) {
|
||||
input := &policy.Input{
|
||||
Digest: "sha256:test-digest",
|
||||
PURL: "test-purl",
|
||||
IsCanonical: tc.isCanonical,
|
||||
Digest: "sha256:test-digest",
|
||||
PURL: "test-purl",
|
||||
}
|
||||
if !tc.isCanonical {
|
||||
input.Tag = "test"
|
||||
}
|
||||
|
||||
tufClient := tuf.NewMockTufClient(tc.repo, test.CreateTempDir(t, "", "tuf-dest"))
|
||||
if tc.policy == nil {
|
||||
tc.policy = &policy.Options{
|
||||
TUFClient: tufClient,
|
||||
if tc.opts == nil {
|
||||
tc.opts = &policy.Options{
|
||||
LocalTargetsDir: test.CreateTempDir(t, "", "tuf-targets"),
|
||||
PolicyID: tc.policyID,
|
||||
LocalPolicyDir: tc.policyPath,
|
||||
DisableTUF: true,
|
||||
}
|
||||
}
|
||||
imageName, err := tc.resolver.ImageName(ctx)
|
||||
require.NoError(t, err)
|
||||
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")
|
||||
@@ -106,7 +103,7 @@ 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 {
|
||||
@@ -117,10 +114,10 @@ func TestLoadingMappings(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateAttestationResolver(t *testing.T) {
|
||||
mockResolver := oci.MockResolver{
|
||||
mockResolver := attestation.MockResolver{
|
||||
Envs: []*attestation.Envelope{},
|
||||
}
|
||||
layoutResolver := &oci.LayoutResolver{}
|
||||
layoutResolver := &attestation.LayoutResolver{}
|
||||
registryResolver := &oci.RegistryImageDetailsResolver{}
|
||||
|
||||
nilRepoReferrers := &config.PolicyMapping{
|
||||
@@ -166,11 +163,11 @@ func TestCreateAttestationResolver(t *testing.T) {
|
||||
return
|
||||
}
|
||||
switch resolver.(type) {
|
||||
case *oci.ReferrersResolver:
|
||||
case *attestation.ReferrersResolver:
|
||||
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleReferrers)
|
||||
case *oci.RegistryResolver:
|
||||
case *attestation.RegistryResolver:
|
||||
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached)
|
||||
case *oci.LayoutResolver:
|
||||
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"
|
||||
@@ -36,7 +35,7 @@ func NewRegoEvaluator(debug bool) Evaluator {
|
||||
}
|
||||
}
|
||||
|
||||
func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *Input) (*Result, error) {
|
||||
func (re *regoEvaluator) Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error) {
|
||||
var regoOpts []func(*rego.Rego)
|
||||
|
||||
// Create a new in-memory store
|
||||
@@ -170,7 +169,7 @@ func handleErrors2(f func(rCtx *rego.BuiltinContext, a, b *ast.Term) (*ast.Term,
|
||||
}
|
||||
}
|
||||
|
||||
func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin {
|
||||
func RegoFunctions(resolver attestation.Resolver) []*tester.Builtin {
|
||||
return []*tester.Builtin{
|
||||
{
|
||||
Decl: verifyDecl,
|
||||
@@ -197,7 +196,7 @@ func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin {
|
||||
}
|
||||
}
|
||||
|
||||
func fetchInTotoAttestations(resolver oci.AttestationResolver) rego.Builtin1 {
|
||||
func fetchInTotoAttestations(resolver attestation.Resolver) rego.Builtin1 {
|
||||
return func(rCtx rego.BuiltinContext, predicateTypeTerm *ast.Term) (*ast.Term, error) {
|
||||
predicateTypeStr, ok := predicateTypeTerm.Value.(ast.String)
|
||||
if !ok {
|
||||
@@ -228,8 +227,8 @@ func fetchInTotoAttestations(resolver oci.AttestationResolver) rego.Builtin1 {
|
||||
}
|
||||
|
||||
func verifyInTotoEnvelope(rCtx *rego.BuiltinContext, envTerm, optsTerm *ast.Term) (*ast.Term, error) {
|
||||
env := new(att.Envelope)
|
||||
opts := new(att.VerifyOptions)
|
||||
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)
|
||||
@@ -239,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
|
||||
@@ -27,12 +27,14 @@ type Result struct {
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
TUFClient tuf.Downloader
|
||||
TUFClientOptions *tuf.ClientOptions
|
||||
DisableTUF bool
|
||||
LocalTargetsDir string
|
||||
LocalPolicyDir string
|
||||
PolicyID string
|
||||
ReferrersRepo string
|
||||
AttestationStyle config.AttestationStyle
|
||||
Debug bool
|
||||
}
|
||||
|
||||
type Policy struct {
|
||||
@@ -40,12 +42,18 @@ type Policy struct {
|
||||
Query string
|
||||
Mapping *config.PolicyMapping
|
||||
ResolvedName string
|
||||
URI string
|
||||
Digest map[string]string
|
||||
}
|
||||
|
||||
type Input struct {
|
||||
Digest string `json:"digest"`
|
||||
PURL string `json:"purl"`
|
||||
IsCanonical bool `json:"isCanonical"`
|
||||
Digest string `json:"digest"`
|
||||
PURL string `json:"purl"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Domain string `json:"domain"`
|
||||
NormalizedName string `json:"normalized_name"`
|
||||
FamiliarName string `json:"familiar_name"`
|
||||
Platform string `json:"platform"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
|
||||
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.
|
||||
2
pkg/tlog/README.md
Normal file
2
pkg/tlog/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## tlog
|
||||
This package implements transparency logging.
|
||||
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,7 +4,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/attest/internal/embed"
|
||||
"github.com/docker/attest/pkg/tuf"
|
||||
"github.com/theupdateframework/go-tuf/v2/metadata"
|
||||
)
|
||||
@@ -21,23 +20,20 @@ func ExampleNewClient_registry() {
|
||||
metadataURI := "registry-1.docker.io/docker/tuf-metadata:latest"
|
||||
targetsURI := "registry-1.docker.io/docker/tuf-targets"
|
||||
|
||||
registryClient, err := tuf.NewClient(embed.RootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker())
|
||||
registryClient, err := tuf.NewClient(&tuf.ClientOptions{tuf.DockerTUFRootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker()})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 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,59 +4,37 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/attest/internal/util"
|
||||
)
|
||||
|
||||
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{
|
||||
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), os.ModePerm)
|
||||
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 {
|
||||
|
||||
@@ -10,12 +10,14 @@ 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 (
|
||||
@@ -34,6 +36,7 @@ type RegistryFetcher struct {
|
||||
targetsRepo string
|
||||
cache *ImageCache
|
||||
timeout time.Duration
|
||||
cfg *config.UpdaterConfig
|
||||
}
|
||||
|
||||
type ImageCache struct {
|
||||
@@ -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,
|
||||
@@ -188,17 +209,17 @@ func getDataFromLayer(fileLayer v1.Layer, maxLength int64) ([]byte, error) {
|
||||
// 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)
|
||||
|
||||
@@ -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"
|
||||
@@ -32,6 +32,9 @@ 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) {
|
||||
@@ -44,42 +47,43 @@ func TestRegistryFetcher(t *testing.T) {
|
||||
}()
|
||||
LoadRegistryTestData(t, regAddr, OCITUFTestDataPath)
|
||||
|
||||
metadataRepo := regAddr.Host + "/tuf-metadata"
|
||||
metadataImgTag := LatestTag
|
||||
targetsRepo := regAddr.Host + "/tuf-targets"
|
||||
metadataRepo := regAddr.Host + metadataPath
|
||||
targetsRepo := regAddr.Host + targetsPath
|
||||
targetFile := "test.txt"
|
||||
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) {
|
||||
@@ -122,7 +126,7 @@ 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)
|
||||
@@ -150,7 +154,6 @@ func TestFindFileInManifest(t *testing.T) {
|
||||
indexManifest, err := idx.RawManifest()
|
||||
assert.NoError(t, err)
|
||||
// cache image layer
|
||||
targetsRepo := "test/tuf-targets"
|
||||
d := &RegistryFetcher{
|
||||
cache: NewImageCache(),
|
||||
targetsRepo: targetsRepo,
|
||||
@@ -183,32 +186,62 @@ func TestFindFileInManifest(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseImgRef(t *testing.T) {
|
||||
metadataRepo := "test/tuf-metadata"
|
||||
metadataRepo := "test" + metadataPath
|
||||
metadataTag := LatestTag
|
||||
targetsRepo := "test/tuf-targets"
|
||||
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: LatestTag,
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -355,9 +388,6 @@ 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
|
||||
}
|
||||
|
||||
|
||||
108
pkg/tuf/tuf.go
108
pkg/tuf/tuf.go
@@ -35,8 +35,13 @@ var (
|
||||
DockerTUFRootDefault = embed.RootDefault
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMetadataSource = "docker/tuf-metadata:latest"
|
||||
defaultTargetsSource = "docker/tuf-targets"
|
||||
)
|
||||
|
||||
type Downloader interface {
|
||||
DownloadTarget(target, filePath string) (actualFilePath string, data []byte, err error)
|
||||
DownloadTarget(target, filePath string) (file *TargetFile, err error)
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
@@ -44,19 +49,44 @@ type Client struct {
|
||||
cfg *config.UpdaterConfig
|
||||
}
|
||||
|
||||
type TargetFile struct {
|
||||
ActualFilePath string
|
||||
TargetURI string
|
||||
Digest string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type ClientOptions struct {
|
||||
InitialRoot []byte
|
||||
Path string
|
||||
MetadataSource string
|
||||
TargetsSource string
|
||||
VersionChecker VersionChecker
|
||||
}
|
||||
|
||||
func NewDockerDefaultClientOptions(tufPath string) *ClientOptions {
|
||||
return &ClientOptions{
|
||||
InitialRoot: DockerTUFRootDefault.Data,
|
||||
Path: tufPath,
|
||||
MetadataSource: defaultMetadataSource,
|
||||
TargetsSource: defaultTargetsSource,
|
||||
VersionChecker: NewDefaultVersionChecker(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewClient creates a new TUF client.
|
||||
func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string, versionChecker VersionChecker) (*Client, error) {
|
||||
func NewClient(opts *ClientOptions) (*Client, error) {
|
||||
var tufSource Source
|
||||
if strings.HasPrefix(metadataSource, "https://") || strings.HasPrefix(metadataSource, "http://") {
|
||||
if strings.HasPrefix(opts.MetadataSource, "https://") || strings.HasPrefix(opts.MetadataSource, "http://") {
|
||||
tufSource = HTTPSource
|
||||
} else {
|
||||
tufSource = OCISource
|
||||
}
|
||||
|
||||
tufRootDigest := util.SHA256Hex(initialRoot)
|
||||
tufRootDigest := util.SHA256Hex(opts.InitialRoot)
|
||||
|
||||
// create a directory for each initial root.json
|
||||
metadataPath := filepath.Join(tufPath, tufRootDigest)
|
||||
metadataPath := filepath.Join(opts.Path, tufRootDigest)
|
||||
err := os.MkdirAll(metadataPath, os.ModePerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create directory '%s': %w", metadataPath, err)
|
||||
@@ -69,29 +99,30 @@ func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string
|
||||
return nil, fmt.Errorf("failed to read root.json: %w", err)
|
||||
}
|
||||
// write the root.json file to the metadata directory
|
||||
err = os.WriteFile(rootFile, initialRoot, 0o666) // #nosec G306
|
||||
err = os.WriteFile(rootFile, opts.InitialRoot, 0o666) // #nosec G306
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to write root.json %w", err)
|
||||
}
|
||||
rootBytes = initialRoot
|
||||
rootBytes = opts.InitialRoot
|
||||
}
|
||||
|
||||
// create updater configuration
|
||||
cfg, err := config.New(metadataSource, rootBytes) // default config
|
||||
// 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 = LatestTag
|
||||
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
|
||||
@@ -111,7 +142,7 @@ func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
err = versionChecker.CheckVersion(client)
|
||||
err = opts.VersionChecker.CheckVersion(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -119,40 +150,69 @@ func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string
|
||||
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 *Client) DownloadTarget(target string, filePath string) (actualFilePath string, data []byte, err error) {
|
||||
func (t *Client) DownloadTarget(target string, filePath string) (file *TargetFile, err error) {
|
||||
// search if the desired target is available
|
||||
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), 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 *Client) GetMetadata() trustedmetadata.TrustedMetadata {
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/embed"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/theupdateframework/go-tuf/v2/metadata"
|
||||
)
|
||||
|
||||
@@ -65,18 +65,18 @@ func TestRootInit(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
_, err := NewClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker)
|
||||
_, err := NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
|
||||
assert.NoErrorf(t, err, "Failed to create TUF client: %v", err)
|
||||
|
||||
// recreation should work with same root
|
||||
_, err = NewClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker)
|
||||
_, err = NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
|
||||
assert.NoErrorf(t, err, "Failed to recreate TUF client: %v", err)
|
||||
|
||||
_, err = NewClient([]byte("broken"), tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker)
|
||||
_, err = NewClient(&ClientOptions{[]byte("broken"), tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
|
||||
assert.Errorf(t, err, "Expected error recreating TUF client with broken root: %v", err)
|
||||
|
||||
_, err = NewClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysBadVersionChecker)
|
||||
assert.Errorf(t, err, "Expected error creating TUF client with bad attest version: %v", err)
|
||||
_, err = NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysBadVersionChecker})
|
||||
assert.Errorf(t, err, "Expected error recreating TUF client with bad version checker")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,29 +108,33 @@ func TestDownloadTarget(t *testing.T) {
|
||||
}{
|
||||
{"http", server.URL + "/metadata", server.URL + "/targets"},
|
||||
{"oci", regAddr.Host + "/tuf-metadata:latest", regAddr.Host + "/tuf-targets"},
|
||||
{"http, download before init", server.URL + "/metadata", server.URL + "/targets"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tufClient, err := NewClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker)
|
||||
assert.NoErrorf(t, err, "Failed to create TUF client: %v", err)
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,11 +67,11 @@ func (vc *DefaultVersionChecker) CheckVersion(client Downloader) error {
|
||||
|
||||
// see https://github.com/Masterminds/semver/blob/v3.2.1/README.md#checking-version-constraints
|
||||
// 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/`
|
||||
2
test/README.md
Normal file
2
test/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## test
|
||||
This directory contains static `testdata` used to run go tests.
|
||||
43
test/testdata/local-policy-inputs/doi/policy.rego
vendored
Normal file
43
test/testdata/local-policy-inputs/doi/policy.rego
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
package attest
|
||||
|
||||
import rego.v1
|
||||
|
||||
keys := [{
|
||||
"id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4",
|
||||
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgH23D1i2+ZIOtVjmfB7iFvX8AhVN\n9CPJ4ie9axw+WRHozGnRy99U2dRge3zueBBg2MweF0zrToXGig2v3YOrdw==\n-----END PUBLIC KEY-----",
|
||||
"from": "2023-12-15T14:00:00Z",
|
||||
"to": null,
|
||||
"status": "active",
|
||||
"signing-format": "dssev1",
|
||||
}]
|
||||
|
||||
provs(pred) := p if {
|
||||
res := attest.fetch(pred)
|
||||
not res.error
|
||||
p := res.value
|
||||
}
|
||||
|
||||
atts := union({
|
||||
provs("https://slsa.dev/provenance/v0.2"),
|
||||
provs("https://spdx.dev/Document"),
|
||||
})
|
||||
|
||||
success if {
|
||||
input.domain == "docker.io"
|
||||
input.familiar_name == "test-image"
|
||||
input.normalized_name == "library/test-image"
|
||||
input.platform == "linux/amd64"
|
||||
input.tag == "test"
|
||||
}
|
||||
|
||||
result := {
|
||||
"success": success,
|
||||
"violations": set(),
|
||||
"attestations": set(),
|
||||
"summary": {
|
||||
"subjects": set(),
|
||||
"slsa_level": "SLSA_BUILD_LEVEL_3",
|
||||
"verifier": "docker-official-images",
|
||||
"policy_uri": "https://docker.com/official/policy/v0.1",
|
||||
},
|
||||
}
|
||||
18
test/testdata/local-policy-inputs/mapping.yaml
vendored
Normal file
18
test/testdata/local-policy-inputs/mapping.yaml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
version: v1
|
||||
kind: policy-mapping
|
||||
policies:
|
||||
- id: docker-official-images
|
||||
description: Docker Official Images
|
||||
files:
|
||||
- path: doi/policy.rego
|
||||
rules:
|
||||
- pattern: "^docker[.]io/library/(.*)$"
|
||||
policy-id: docker-official-images
|
||||
- pattern: "repo$"
|
||||
policy-id: docker-official-images
|
||||
- pattern: "test-image$"
|
||||
policy-id: docker-official-images
|
||||
- pattern: "image-signer-verifier-test$"
|
||||
policy-id: docker-official-images
|
||||
- pattern: "library/(.*)$"
|
||||
rewrite: docker.io/library/$1
|
||||
@@ -38,7 +38,7 @@ subjects contains subject if {
|
||||
}
|
||||
|
||||
success if {
|
||||
print("input:",input)
|
||||
# print("input:",input)
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
4
test/testdata/local-policy-no-policies/mapping.yaml
vendored
Normal file
4
test/testdata/local-policy-no-policies/mapping.yaml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
version: v1
|
||||
kind: policy-mapping
|
||||
policies:
|
||||
rules:
|
||||
Reference in New Issue
Block a user