Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
357768d421 | ||
|
|
6bd57e02b6 | ||
|
|
86878482c3 | ||
|
|
130e1f640b | ||
|
|
0d0d86854c | ||
|
|
1d9e14b99f | ||
|
|
83c7d7634a | ||
|
|
5c07bd70d9 | ||
|
|
c02e628600 | ||
|
|
3d46780a1c | ||
|
|
83dfd746b9 | ||
|
|
845fe93c11 | ||
|
|
c154613c52 | ||
|
|
e44390d2bc | ||
|
|
8ba9656645 | ||
|
|
e120439035 | ||
|
|
b20f452004 | ||
|
|
4be882aeb0 | ||
|
|
3b5c506739 | ||
|
|
f36bb50af5 | ||
|
|
c8c148c70a | ||
|
|
a334599635 | ||
|
|
e81016fc31 | ||
|
|
2ae5606c92 | ||
|
|
8a6e75ce39 |
33
go.mod
33
go.mod
@@ -4,22 +4,22 @@ go 1.22.1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/semver/v3 v3.2.1
|
github.com/Masterminds/semver/v3 v3.2.1
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.27.15
|
github.com/aws/aws-sdk-go-v2/config v1.27.19
|
||||||
github.com/containerd/containerd v1.7.17
|
github.com/containerd/containerd v1.7.18
|
||||||
github.com/distribution/reference v0.6.0
|
github.com/distribution/reference v0.6.0
|
||||||
github.com/go-openapi/runtime v0.28.0
|
github.com/go-openapi/runtime v0.28.0
|
||||||
github.com/go-openapi/strfmt v0.23.0
|
github.com/go-openapi/strfmt v0.23.0
|
||||||
github.com/google/go-containerregistry v0.19.1
|
github.com/google/go-containerregistry v0.19.2
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2
|
github.com/hashicorp/go-cleanhttp v0.5.2
|
||||||
github.com/in-toto/in-toto-golang v0.9.0
|
github.com/in-toto/in-toto-golang v0.9.0
|
||||||
github.com/open-policy-agent/opa v0.64.1
|
github.com/open-policy-agent/opa v0.65.0
|
||||||
github.com/opencontainers/image-spec v1.1.0
|
github.com/opencontainers/image-spec v1.1.0
|
||||||
github.com/package-url/packageurl-go v0.1.3
|
github.com/package-url/packageurl-go v0.1.3
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/secure-systems-lab/go-securesystemslib v0.8.0
|
github.com/secure-systems-lab/go-securesystemslib v0.8.0
|
||||||
github.com/sigstore/cosign/v2 v2.2.4
|
github.com/sigstore/cosign/v2 v2.2.4
|
||||||
github.com/sigstore/rekor v1.3.6
|
github.com/sigstore/rekor v1.3.6
|
||||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3
|
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.4
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/testcontainers/testcontainers-go v0.31.0
|
github.com/testcontainers/testcontainers-go v0.31.0
|
||||||
github.com/testcontainers/testcontainers-go/modules/registry v0.31.0
|
github.com/testcontainers/testcontainers-go/modules/registry v0.31.0
|
||||||
@@ -37,24 +37,25 @@ require (
|
|||||||
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||||
github.com/agnivade/levenshtein v1.1.1 // indirect
|
github.com/agnivade/levenshtein v1.1.1 // indirect
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2 v1.27.0 // indirect
|
github.com/aws/aws-sdk-go-v2 v1.28.0 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.15 // indirect
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.19 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.3 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.7 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.10 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.7 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.10 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.9 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.12 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/kms v1.31.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/kms v1.32.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.8 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.20.12 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.9 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.28.13 // indirect
|
||||||
github.com/aws/smithy-go v1.20.2 // indirect
|
github.com/aws/smithy-go v1.20.2 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudflare/circl v1.3.8 // indirect
|
github.com/cloudflare/circl v1.3.8 // indirect
|
||||||
|
github.com/containerd/errdefs v0.1.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
|
github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
|
||||||
github.com/cpuguy83/dockercfg v0.3.1 // indirect
|
github.com/cpuguy83/dockercfg v0.3.1 // indirect
|
||||||
@@ -74,7 +75,7 @@ require (
|
|||||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||||
github.com/go-ini/ini v1.67.0 // indirect
|
github.com/go-ini/ini v1.67.0 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
||||||
github.com/go-logr/logr v1.4.1 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-openapi/analysis v0.23.0 // indirect
|
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||||
|
|||||||
70
go.sum
70
go.sum
@@ -95,20 +95,20 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig
|
|||||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
github.com/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 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||||
github.com/aws/aws-sdk-go v1.51.6 h1:Ld36dn9r7P9IjU8WZSaswQ8Y/XUCRpewim5980DwYiU=
|
github.com/aws/aws-sdk-go v1.53.10 h1:3enP5l5WtezT9Ql+XZqs56JBf5YUd/FEzTCg///OIGY=
|
||||||
github.com/aws/aws-sdk-go v1.51.6/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
github.com/aws/aws-sdk-go v1.53.10/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.27.0 h1:7bZWKoXhzI+mMR/HjdMx8ZCC5+6fY0lS5tr0bbgiLlo=
|
github.com/aws/aws-sdk-go-v2 v1.28.0 h1:ne6ftNhY0lUvlazMUQF15FF6NH80wKmPRFG7g2q6TCw=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.27.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
|
github.com/aws/aws-sdk-go-v2 v1.28.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.27.15 h1:uNnGLZ+DutuNEkuPh6fwqK7LpEiPmzb7MIMA1mNWEUc=
|
github.com/aws/aws-sdk-go-v2/config v1.27.19 h1:+DBS8gJP6VsxYkZ6UEV0/VsRM2rYpbQCYsosW9RRmeQ=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.27.15/go.mod h1:7j7Kxx9/7kTmL7z4LlhwQe63MYEE5vkVV6nWg4ZAI8M=
|
github.com/aws/aws-sdk-go-v2/config v1.27.19/go.mod h1:KzZcioJWzy9oV+oS5CobYXlDtU9+eW7bPG1g7gizTW4=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.15 h1:YDexlvDRCA8ems2T5IP1xkMtOZ1uLJOCJdTr0igs5zo=
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.19 h1:R18G7nBBGLby51CFEqUBFF2IVl7LUdCtYj6iosUwh/0=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.15/go.mod h1:vxHggqW6hFNaeNC0WyXS3VdyjcV0a4KMUY4dKJ96buU=
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.19/go.mod h1:xr9kUMnaLTB866HItT6pg58JgiBP77fSQLBwIa//zk8=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.3 h1:dQLK4TjtnlRGb0czOht2CevZ5l6RSyRWAnKeGd7VAFE=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.6 h1:vVOuhRyslJ6T/HteG71ZWCTas1q2w6f0NKsNbkXHs/A=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.3/go.mod h1:TL79f2P6+8Q7dTsILpiVST+AL9lkF6PPGI167Ny0Cjw=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.6/go.mod h1:jimWaqLiT0sJGLh51dKCLLtExRYPtMU7MpxuCgtbkxg=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.7 h1:lf/8VTF2cM+N4SLzaYJERKEWAXq8MOMpZfU6wEPWsPk=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.10 h1:LZIUb8sQG2cb89QaVFtMSnER10gyKkqU1k3hP3g9das=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.7/go.mod h1:4SjkU7QiqK2M9oozyMzfZ/23LmUY+h3oFqhdeP5OMiI=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.10/go.mod h1:BRIqay//vnIOCZjoXWSLffL2uzbtxEmnSlfbvVh7Z/4=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.7 h1:4OYVp0705xu8yjdyoWix0r9wPIRXnIzzOoUpQVHIJ/g=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.10 h1:HY7CXLA0GiQUo3WYxOP7WYkLcwvRX4cLPf5joUcrQGk=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.7/go.mod h1:vd7ESTEvI76T2Na050gODNmNU7+OyKrIKroYTu4ABiI=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.10/go.mod h1:kfRBSxRa+I+VyON7el3wLZdrO91oxUxEwdAaWgFqN90=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.20.2 h1:y6LX9GUoEA3mO0qpFl1ZQHj1rFyPWVphlzebiSt2tKE=
|
github.com/aws/aws-sdk-go-v2/service/ecr v1.20.2 h1:y6LX9GUoEA3mO0qpFl1ZQHj1rFyPWVphlzebiSt2tKE=
|
||||||
@@ -117,16 +117,16 @@ github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.18.2 h1:PpbXaecV3sLAS6rjQiaKw4
|
|||||||
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.18.2/go.mod h1:fUHpGXr4DrXkEDpGAjClPsviWf+Bszeb0daKE0blxv8=
|
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.18.2/go.mod h1:fUHpGXr4DrXkEDpGAjClPsviWf+Bszeb0daKE0blxv8=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.9 h1:Wx0rlZoEJR7JwlSZcHnEa7CNjrSIyVxMFWGAaXy4fJY=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.12 h1:kO2J7WMroF/OTHN9WTcUtMjPhJ7ZoNxx0dwv6UCXQgY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.9/go.mod h1:aVMHdE0aHO3v+f/iw01fmXV/5DbfQ3Bi9nN7nd9bE9Y=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.12/go.mod h1:mrNxrjYvXaSjZe5fkKaWgDnOQ6BExLn/7Ru9OpRsMPY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/kms v1.31.2 h1:z4NOTY1sm0Vb/+Kovnbf8TLPcH8P36bILR5hgXE1sOY=
|
github.com/aws/aws-sdk-go-v2/service/kms v1.32.1 h1:FARrQLRQXpCFYylIUVF1dRij6YbPCmtwudq9NBk4kFc=
|
||||||
github.com/aws/aws-sdk-go-v2/service/kms v1.31.2/go.mod h1:6HNwTCo40yDvnmgT/NgRgWsx0/0bN2TV6RO5FfG8G60=
|
github.com/aws/aws-sdk-go-v2/service/kms v1.32.1/go.mod h1:8lETO9lelSG2B6KMXFh2OwPPqGV6WQM3RqLAEjP1xaU=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.8 h1:Kv1hwNG6jHC/sxMTe5saMjH6t6ZLkgfvVxyEjfWL1ks=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.20.12 h1:FsYii6U+2k8ynYBo+pywlCBY9HNAFRh+iICRHbn+Qyw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.8/go.mod h1:c1qtZUWtygI6ZdvKppzCSXsDOq5I4luJPZ0Ud3juFCA=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.20.12/go.mod h1:j9Rps+Lcs2A0tYypWsNBeJOjgsIYUf1Styppo9Es0Wo=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.2 h1:nWBZ1xHCF+A7vv9sDzJOq4NWIdzFYm0kH7Pr4OjHYsQ=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.6 h1:lEE+xEcq3lh9bk362tgErP1+n689q5ERdmTwmF1XT3M=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.2/go.mod h1:9lmoVDVLz/yUZwLaQ676TK02fhCu4+PgRSmMaKR1ozk=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.6/go.mod h1:2tR0x1DCL5IgnVZ1NQNFDNg5/XL/kiQgWI5l7I/N5Js=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.9 h1:Qp6Boy0cGDloOE3zI6XhNLNZgjNS8YmiFQFHe71SaW0=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.28.13 h1:TSzmuUeruVJ4XWYp3bYzKCXue70ECpJWmbP3UfEvhYY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.9/go.mod h1:0Aqn1MnEuitqfsCNyKsdKLhDUOr4txD/g19EfiUqgws=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.28.13/go.mod h1:FppRtFjBA9mSWTj2cIAWCP66+bbBPMuPpBfWRXC5Yi0=
|
||||||
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
|
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
|
||||||
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||||
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8 h1:SoFYaT9UyGkR0+nogNyD/Lj+bsixB+SNuAS4ABlEs6M=
|
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8 h1:SoFYaT9UyGkR0+nogNyD/Lj+bsixB+SNuAS4ABlEs6M=
|
||||||
@@ -168,8 +168,10 @@ github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUo
|
|||||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
|
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
|
||||||
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ=
|
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ=
|
||||||
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w=
|
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w=
|
||||||
github.com/containerd/containerd v1.7.17 h1:KjNnn0+tAVQHAoaWRjmdak9WlvnFR/8rU1CHHy8Rm2A=
|
github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao=
|
||||||
github.com/containerd/containerd v1.7.17/go.mod h1:vK+hhT4TIv2uejlcDlbVIc8+h/BqtKLIyNrtCZol8lI=
|
github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4=
|
||||||
|
github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM=
|
||||||
|
github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0=
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
|
github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
|
||||||
@@ -246,8 +248,8 @@ github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQr
|
|||||||
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
|
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
@@ -319,8 +321,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY=
|
github.com/google/go-containerregistry v0.19.2 h1:TannFKE1QSajsP6hPWb5oJNgKe1IKjHukIKDUmvsV6w=
|
||||||
github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
|
github.com/google/go-containerregistry v0.19.2/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
|
||||||
github.com/google/go-github/v55 v55.0.0 h1:4pp/1tNMB9X/LuAhs5i0KQAE40NmiR/y6prLNb9x9cg=
|
github.com/google/go-github/v55 v55.0.0 h1:4pp/1tNMB9X/LuAhs5i0KQAE40NmiR/y6prLNb9x9cg=
|
||||||
github.com/google/go-github/v55 v55.0.0/go.mod h1:JLahOTA1DnXzhxEymmFF5PP2tSS9JVNj68mSZNDwskA=
|
github.com/google/go-github/v55 v55.0.0/go.mod h1:JLahOTA1DnXzhxEymmFF5PP2tSS9JVNj68mSZNDwskA=
|
||||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
@@ -463,8 +465,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
|
|||||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||||
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
|
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
|
||||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||||
github.com/open-policy-agent/opa v0.64.1 h1:n8IJTYlFWzqiOYx+JiawbErVxiqAyXohovcZxYbskxQ=
|
github.com/open-policy-agent/opa v0.65.0 h1:wnEU0pEk80YjFi3yoDbFTMluyNssgPI4VJNJetD9a4U=
|
||||||
github.com/open-policy-agent/opa v0.64.1/go.mod h1:j4VeLorVpKipnkQ2TDjWshEuV3cvP/rHzQhYaraUXZY=
|
github.com/open-policy-agent/opa v0.65.0/go.mod h1:CNoLL44LuCH1Yot/zoeZXRKFylQtCJV+oGFiP2TeeEc=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
@@ -532,8 +534,8 @@ github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8=
|
|||||||
github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc=
|
github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc=
|
||||||
github.com/sigstore/sigstore v1.8.3 h1:G7LVXqL+ekgYtYdksBks9B38dPoIsbscjQJX/MGWkA4=
|
github.com/sigstore/sigstore v1.8.3 h1:G7LVXqL+ekgYtYdksBks9B38dPoIsbscjQJX/MGWkA4=
|
||||||
github.com/sigstore/sigstore v1.8.3/go.mod h1:mqbTEariiGA94cn6G3xnDiV6BD8eSLdL/eA7bvJ0fVs=
|
github.com/sigstore/sigstore v1.8.3/go.mod h1:mqbTEariiGA94cn6G3xnDiV6BD8eSLdL/eA7bvJ0fVs=
|
||||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3 h1:LTfPadUAo+PDRUbbdqbeSl2OuoFQwUFTnJ4stu+nwWw=
|
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.4 h1:okxaVlaTrQowE1FA4UQ3rw54f7BUjdnzERIxbZTBZuc=
|
||||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3/go.mod h1:QV/Lxlxm0POyhfyBtIbTWxNeF18clMlkkyL9mu45y18=
|
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.4/go.mod h1:jkcPErmnCECuSJajUaUq5pwCMOeBF19VzQo6bv4l1D0=
|
||||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3 h1:xgbPRCr2npmmsuVVteJqi/ERw9+I13Wou7kq0Yk4D8g=
|
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3 h1:xgbPRCr2npmmsuVVteJqi/ERw9+I13Wou7kq0Yk4D8g=
|
||||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3/go.mod h1:G4+I83FILPX6MtnoaUdmv/bRGEVtR3JdLeJa/kXdk/0=
|
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3/go.mod h1:G4+I83FILPX6MtnoaUdmv/bRGEVtR3JdLeJa/kXdk/0=
|
||||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.3 h1:vDl2fqPT0h3D/k6NZPlqnKFd1tz3335wm39qjvpZNJc=
|
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.3 h1:vDl2fqPT0h3D/k6NZPlqnKFd1tz3335wm39qjvpZNJc=
|
||||||
|
|||||||
@@ -96,23 +96,8 @@ type AnnotatedStatement struct {
|
|||||||
Annotations map[string]string
|
Annotations map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStatement, error) {
|
func ExtractStatementsFromIndex(idx v1.ImageIndex, mediaType string) ([]*AnnotatedStatement, error) {
|
||||||
idx, err := layout.ImageIndexFromPath(path)
|
mfs2, err := idx.IndexManifest()
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load image index: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
idxm, err := idx.IndexManifest()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get digest: %w", err)
|
|
||||||
}
|
|
||||||
idxDigest := idxm.Manifests[0].Digest
|
|
||||||
|
|
||||||
mfs, err := idx.ImageIndex(idxDigest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
|
|
||||||
}
|
|
||||||
mfs2, err := mfs.IndexManifest()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
||||||
}
|
}
|
||||||
@@ -124,7 +109,7 @@ func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStat
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
attestationImage, err := mfs.Image(mf.Digest)
|
attestationImage, err := idx.Image(mf.Digest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
|
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
|
||||||
}
|
}
|
||||||
@@ -189,3 +174,22 @@ func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStat
|
|||||||
}
|
}
|
||||||
return statements, nil
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/docker/attest/pkg/attest"
|
"github.com/docker/attest/pkg/attest"
|
||||||
|
"github.com/docker/attest/pkg/attestation"
|
||||||
"github.com/docker/attest/pkg/mirror"
|
"github.com/docker/attest/pkg/mirror"
|
||||||
"github.com/docker/attest/pkg/oci"
|
"github.com/docker/attest/pkg/oci"
|
||||||
"github.com/docker/attest/pkg/signerverifier"
|
"github.com/docker/attest/pkg/signerverifier"
|
||||||
@@ -25,13 +26,13 @@ func ExampleSign_remote() {
|
|||||||
// signer, err := signerverifier.GetAWSSigner(cmd.Context(), aws_arn, aws_region)
|
// signer, err := signerverifier.GetAWSSigner(cmd.Context(), aws_arn, aws_region)
|
||||||
|
|
||||||
// configure signing options
|
// configure signing options
|
||||||
opts := &attest.SigningOptions{
|
opts := &attestation.SigningOptions{
|
||||||
Replace: true, // replace unsigned intoto statements with signed intoto attestations, otherwise leave in place
|
Replace: true, // replace unsigned intoto statements with signed intoto attestations, otherwise leave in place
|
||||||
}
|
}
|
||||||
|
|
||||||
// load image index with unsigned attestation-manifests
|
// load image index with unsigned attestation-manifests
|
||||||
ref := "docker/image-signer-verifier:latest"
|
ref := "docker/image-signer-verifier:latest"
|
||||||
att, err := oci.AttestationIndexFromRemote(ref)
|
att, err := oci.SubjectIndexFromRemote(ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -46,7 +47,7 @@ func ExampleSign_remote() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// push image index with signed attestation-manifests
|
// push image index with signed attestation-manifests
|
||||||
err = mirror.PushToRegistry(signedImageIndex, ref)
|
err = mirror.PushIndexToRegistry(signedImageIndex, ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -61,7 +62,7 @@ func ExampleSign_remote() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
err = mirror.SaveAsOCILayout(idx, path)
|
err = mirror.SaveIndexAsOCILayout(idx, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,27 +39,21 @@ func ExampleVerify_remote() {
|
|||||||
// create a resolver for remote attestations
|
// create a resolver for remote attestations
|
||||||
image := "registry-1.docker.io/library/notary:server"
|
image := "registry-1.docker.io/library/notary:server"
|
||||||
platform := "linux/amd64"
|
platform := "linux/amd64"
|
||||||
resolver := &oci.RegistryResolver{
|
|
||||||
Image: image, // path to image index in OCI registry containing image attestations
|
|
||||||
Platform: platform, // platform of subject image (image that attestations are being verified against)
|
|
||||||
}
|
|
||||||
// example using a local resolver
|
|
||||||
// path := "/myimage"
|
|
||||||
// platform := "linux/amd64"
|
|
||||||
// resolver := &oci.OCILayoutResolver{
|
|
||||||
// Path: path, // file path to OCI layout containing image attestations
|
|
||||||
// Platform: platform, // platform of subject image (image that attestations are being verified against)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// configure policy options
|
// configure policy options
|
||||||
opts := &policy.PolicyOptions{
|
opts := &policy.PolicyOptions{
|
||||||
TufClient: tufClient,
|
TufClient: tufClient,
|
||||||
LocalTargetsDir: filepath.Join(home, ".docker", "policy"), // location to store policy files downloaded from TUF
|
LocalTargetsDir: filepath.Join(home, ".docker", "policy"), // location to store policy files downloaded from TUF
|
||||||
LocalPolicyDir: "", // overrides TUF policy for local policy files if set
|
LocalPolicyDir: "", // overrides TUF policy for local policy files if set
|
||||||
|
PolicyId: "", // set to ignore policy mapping and select a policy by id
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify attestations
|
// verify attestations
|
||||||
result, err := attest.Verify(context.Background(), opts, resolver)
|
src, err := oci.ParseImageSpec(image, oci.WithPlatform(platform))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
result, err := attest.Verify(context.Background(), src, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,21 +18,43 @@ import (
|
|||||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Sign(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *SigningOptions) (v1.ImageIndex, error) {
|
func Sign(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) (v1.ImageIndex, error) {
|
||||||
|
images, err := SignedAttestationImages(ctx, idx, signer, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to sign attestation images: %w", err)
|
||||||
|
}
|
||||||
|
for _, image := range images {
|
||||||
|
idx, err = addImageToIndex(idx, image.Image, image.Descriptor, image.AttestationManifest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to add signed layers to index: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return idx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SignedAttestationImages(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) ([]*attestation.SignedAttestationImage, error) {
|
||||||
// extract attestation manifests from index
|
// extract attestation manifests from index
|
||||||
attestationManifests, err := attestation.GetAttestationManifestsFromIndex(idx)
|
attestationManifests, err := attestation.GetAttestationManifestsFromIndex(idx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get attestation manifests: %w", err)
|
return nil, fmt.Errorf("failed to get attestation manifests: %w", err)
|
||||||
}
|
}
|
||||||
|
if len(attestationManifests) == 0 {
|
||||||
|
return nil, fmt.Errorf("no attestation manifests found")
|
||||||
|
}
|
||||||
|
images := []*attestation.SignedAttestationImage{}
|
||||||
// sign every attestation layer in each manifest
|
// sign every attestation layer in each manifest
|
||||||
for _, manifest := range attestationManifests {
|
for _, manifest := range attestationManifests {
|
||||||
idx, err = signLayersAndAddToIndex(ctx, idx, manifest.Attestation.Layers, manifest, signer, opts)
|
newImg, newDescriptor, err := SignLayersAndAddToImage(ctx, manifest.Attestation.Layers, manifest, signer, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to add signed layers: %w", err)
|
return nil, fmt.Errorf("failed to add signed layers to image: %w", err)
|
||||||
}
|
}
|
||||||
|
images = append(images, &attestation.SignedAttestationImage{
|
||||||
|
Image: newImg,
|
||||||
|
Descriptor: newDescriptor,
|
||||||
|
AttestationManifest: manifest,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return idx, nil
|
return images, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddAttestation(ctx context.Context, idx v1.ImageIndex, statement *intoto.Statement, signer dsse.SignerVerifier) (v1.ImageIndex, error) {
|
func AddAttestation(ctx context.Context, idx v1.ImageIndex, statement *intoto.Statement, signer dsse.SignerVerifier) (v1.ImageIndex, error) {
|
||||||
@@ -52,7 +74,7 @@ func AddAttestation(ctx context.Context, idx v1.ImageIndex, statement *intoto.St
|
|||||||
}
|
}
|
||||||
updatedIndex := false
|
updatedIndex := false
|
||||||
for _, manifest := range attestationManifests {
|
for _, manifest := range attestationManifests {
|
||||||
if subjectDigests[manifest.Annotations[oci.DockerReferenceDigest]] {
|
if subjectDigests[manifest.Annotations[attestation.DockerReferenceDigest]] {
|
||||||
attestationLayers := []attestation.AttestationLayer{
|
attestationLayers := []attestation.AttestationLayer{
|
||||||
{
|
{
|
||||||
Statement: statement,
|
Statement: statement,
|
||||||
@@ -63,9 +85,13 @@ func AddAttestation(ctx context.Context, idx v1.ImageIndex, statement *intoto.St
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
// hard-coding replace to false here, because if it's true we will remove any unsigned statements, even unrelated ones
|
// hard-coding replace to false here, because if it's true we will remove any unsigned statements, even unrelated ones
|
||||||
idx, err = signLayersAndAddToIndex(ctx, idx, attestationLayers, manifest, signer, &SigningOptions{Replace: false})
|
newImg, newDec, err := SignLayersAndAddToImage(ctx, attestationLayers, manifest, signer, &attestation.SigningOptions{Replace: false})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to add signed layers: %w", err)
|
return nil, fmt.Errorf("failed to add signed layers to image: %w", err)
|
||||||
|
}
|
||||||
|
idx, err = addImageToIndex(idx, newImg, newDec, manifest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to add attestation image to index: %w", err)
|
||||||
}
|
}
|
||||||
updatedIndex = true
|
updatedIndex = true
|
||||||
}
|
}
|
||||||
@@ -74,33 +100,34 @@ func AddAttestation(ctx context.Context, idx v1.ImageIndex, statement *intoto.St
|
|||||||
return nil, fmt.Errorf("no attestation manifest found for statement")
|
return nil, fmt.Errorf("no attestation manifest found for statement")
|
||||||
}
|
}
|
||||||
return idx, nil
|
return idx, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func signLayersAndAddToIndex(
|
func SignLayersAndAddToImage(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
idx v1.ImageIndex,
|
|
||||||
attestationLayers []attestation.AttestationLayer,
|
attestationLayers []attestation.AttestationLayer,
|
||||||
manifest attestation.AttestationManifest,
|
manifest attestation.AttestationManifest,
|
||||||
signer dsse.SignerVerifier,
|
signer dsse.SignerVerifier,
|
||||||
opts *SigningOptions) (v1.ImageIndex, error) {
|
opts *attestation.SigningOptions) (v1.Image, *v1.Descriptor, error) {
|
||||||
|
|
||||||
signedLayers, err := signLayers(ctx, attestationLayers, signer)
|
signedLayers, err := signLayers(ctx, attestationLayers, signer, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to sign attestations: %w", err)
|
return nil, nil, fmt.Errorf("failed to sign attestations: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
newImg, err := addSignedLayers(signedLayers, manifest, opts)
|
newImg, err := addSignedLayers(signedLayers, manifest, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to add signed layers: %w", err)
|
return nil, nil, fmt.Errorf("failed to add signed layers: %w", err)
|
||||||
|
}
|
||||||
|
if !opts.SkipSubject {
|
||||||
|
newImg = mutate.Subject(newImg, *manifest.SubjectDescriptor).(v1.Image)
|
||||||
}
|
}
|
||||||
newDesc, err := partial.Descriptor(newImg)
|
newDesc, err := partial.Descriptor(newImg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get descriptor: %w", err)
|
return nil, nil, fmt.Errorf("failed to get descriptor: %w", err)
|
||||||
}
|
}
|
||||||
cf, err := manifest.Attestation.Image.ConfigFile()
|
cf, err := manifest.Attestation.Image.ConfigFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get config file: %w", err)
|
return nil, nil, fmt.Errorf("failed to get config file: %w", err)
|
||||||
}
|
}
|
||||||
newDesc.Platform = cf.Platform()
|
newDesc.Platform = cf.Platform()
|
||||||
if newDesc.Platform == nil {
|
if newDesc.Platform == nil {
|
||||||
@@ -111,16 +138,27 @@ func signLayersAndAddToIndex(
|
|||||||
}
|
}
|
||||||
newDesc.MediaType = manifest.MediaType
|
newDesc.MediaType = manifest.MediaType
|
||||||
newDesc.Annotations = manifest.Annotations
|
newDesc.Annotations = manifest.Annotations
|
||||||
|
|
||||||
|
return newImg, newDesc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addImageToIndex(
|
||||||
|
idx v1.ImageIndex,
|
||||||
|
img v1.Image,
|
||||||
|
desc *v1.Descriptor,
|
||||||
|
manifest attestation.AttestationManifest,
|
||||||
|
) (v1.ImageIndex, error) {
|
||||||
|
|
||||||
idx = mutate.RemoveManifests(idx, match.Digests(manifest.Digest))
|
idx = mutate.RemoveManifests(idx, match.Digests(manifest.Digest))
|
||||||
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
||||||
Add: newImg,
|
Add: img,
|
||||||
Descriptor: *newDesc,
|
Descriptor: *desc,
|
||||||
})
|
})
|
||||||
return idx, nil
|
return idx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// signLayers signs each intoto attestation layer with the given signer
|
// signLayers signs each intoto attestation layer with the given signer
|
||||||
func signLayers(ctx context.Context, layers []attestation.AttestationLayer, signer dsse.SignerVerifier) ([]mutate.Addendum, error) {
|
func signLayers(ctx context.Context, layers []attestation.AttestationLayer, signer dsse.SignerVerifier, opts *attestation.SigningOptions) ([]mutate.Addendum, error) {
|
||||||
var signedLayers []mutate.Addendum
|
var signedLayers []mutate.Addendum
|
||||||
for _, layer := range layers {
|
for _, layer := range layers {
|
||||||
// only sign intoto layers
|
// only sign intoto layers
|
||||||
@@ -131,10 +169,11 @@ func signLayers(ctx context.Context, layers []attestation.AttestationLayer, sign
|
|||||||
layer.Annotations[InTotoReferenceLifecycleStage] = LifecycleStageExperimental
|
layer.Annotations[InTotoReferenceLifecycleStage] = LifecycleStageExperimental
|
||||||
|
|
||||||
// sign the statement
|
// sign the statement
|
||||||
env, err := signInTotoStatement(ctx, layer.Statement, signer)
|
env, err := signInTotoStatement(ctx, layer.Statement, signer, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to sign statement: %w", err)
|
return nil, fmt.Errorf("failed to sign statement: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaType, err := attestation.DSSEMediaType(layer.Statement.PredicateType)
|
mediaType, err := attestation.DSSEMediaType(layer.Statement.PredicateType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get DSSE media type: %w", err)
|
return nil, fmt.Errorf("failed to get DSSE media type: %w", err)
|
||||||
@@ -153,12 +192,12 @@ func signLayers(ctx context.Context, layers []attestation.AttestationLayer, sign
|
|||||||
return signedLayers, nil
|
return signedLayers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func signInTotoStatement(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier) (*attestation.Envelope, error) {
|
func signInTotoStatement(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *attestation.SigningOptions) (*attestation.Envelope, error) {
|
||||||
payload, err := json.Marshal(statement)
|
payload, err := json.Marshal(statement)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to marshal statement: %w", err)
|
return nil, fmt.Errorf("failed to marshal statement: %w", err)
|
||||||
}
|
}
|
||||||
env, err := attestation.SignDSSE(ctx, payload, intoto.PayloadType, signer)
|
env, err := attestation.SignDSSE(ctx, payload, signer, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to sign statement: %w", err)
|
return nil, fmt.Errorf("failed to sign statement: %w", err)
|
||||||
}
|
}
|
||||||
@@ -166,7 +205,14 @@ func signInTotoStatement(ctx context.Context, statement *intoto.Statement, signe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addSignedLayers adds signed layers to a new or existing attestation image
|
// addSignedLayers adds signed layers to a new or existing attestation image
|
||||||
func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.AttestationManifest, opts *SigningOptions) (v1.Image, error) {
|
func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.AttestationManifest, opts *attestation.SigningOptions) (v1.Image, error) {
|
||||||
|
withAnnotations := func(img v1.Image) v1.Image {
|
||||||
|
// this is handy when dealing with referrers
|
||||||
|
return mutate.Annotations(img, map[string]string{
|
||||||
|
attestation.DockerReferenceType: attestation.AttestationManifestType,
|
||||||
|
attestation.DockerReferenceDigest: manifest.SubjectDescriptor.Digest.String(),
|
||||||
|
}).(v1.Image)
|
||||||
|
}
|
||||||
var err error
|
var err error
|
||||||
if opts.Replace {
|
if opts.Replace {
|
||||||
// create a new attestation image with only signed layers
|
// create a new attestation image with only signed layers
|
||||||
@@ -188,7 +234,7 @@ func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.Attest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return newImg, nil
|
return withAnnotations(newImg), nil
|
||||||
}
|
}
|
||||||
// Add signed layers to the existing image
|
// Add signed layers to the existing image
|
||||||
for _, layer := range signedLayers {
|
for _, layer := range signedLayers {
|
||||||
@@ -197,5 +243,5 @@ func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.Attest
|
|||||||
return nil, fmt.Errorf("failed to append layer: %w", err)
|
return nil, fmt.Errorf("failed to append layer: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return manifest.Attestation.Image, nil
|
return withAnnotations(manifest.Attestation.Image), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ var (
|
|||||||
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||||
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
|
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
|
||||||
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
|
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
|
||||||
|
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
|
||||||
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
|
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
|
||||||
TestTempDir = "attest-sign-test"
|
TestTempDir = "attest-sign-test"
|
||||||
)
|
)
|
||||||
@@ -41,22 +42,22 @@ func TestSignVerifyOCILayout(t *testing.T) {
|
|||||||
replace bool
|
replace bool
|
||||||
}{
|
}{
|
||||||
|
|
||||||
{"signed replaced (does nothing)", UnsignedTestImage, 0, 4, true},
|
{"signed replaced", UnsignedTestImage, 0, 4, true},
|
||||||
{"without replace", UnsignedTestImage, 4, 4, false},
|
{"without replace", UnsignedTestImage, 4, 4, false},
|
||||||
// image without provenance doesn't fail
|
// image without provenance doesn't fail
|
||||||
{"no provenance (replace)", NoProvenanceImage, 0, 2, true},
|
{"no provenance (replace)", NoProvenanceImage, 0, 2, true},
|
||||||
{"no provenance (no replace)", NoProvenanceImage, 2, 2, false},
|
{"no provenance (no replace)", NoProvenanceImage, 2, 2, false},
|
||||||
}
|
}
|
||||||
policyResolver := &policy.PolicyOptions{
|
policyOpts := &policy.PolicyOptions{
|
||||||
LocalPolicyDir: PassPolicyDir,
|
LocalPolicyDir: PassPolicyDir,
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
||||||
opts := &SigningOptions{
|
opts := &attestation.SigningOptions{
|
||||||
Replace: tc.replace,
|
Replace: tc.replace,
|
||||||
}
|
}
|
||||||
attIdx, err := oci.AttestationIndexFromPath(tc.TestImage)
|
attIdx, err := oci.SubjectIndexFromPath(tc.TestImage)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
|
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -73,12 +74,9 @@ func TestSignVerifyOCILayout(t *testing.T) {
|
|||||||
})
|
})
|
||||||
_, err = layout.Write(outputLayout, idx)
|
_, err = layout.Write(outputLayout, idx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
src, err := oci.ParseImageSpec("oci://" + outputLayout)
|
||||||
resolver := &oci.OCILayoutResolver{
|
require.NoError(t, err)
|
||||||
Path: outputLayout,
|
policy, err := Verify(ctx, src, policyOpts)
|
||||||
Platform: "",
|
|
||||||
}
|
|
||||||
policy, err := Verify(ctx, policyResolver, resolver)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equalf(t, OutcomeSuccess, policy.Outcome, "Policy should have been found")
|
assert.Equalf(t, OutcomeSuccess, policy.Outcome, "Policy should have been found")
|
||||||
|
|
||||||
@@ -109,7 +107,7 @@ func TestAddAttestation(t *testing.T) {
|
|||||||
expectedStatements := 4
|
expectedStatements := 4
|
||||||
|
|
||||||
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
||||||
attIdx, err := oci.AttestationIndexFromPath(UnsignedTestImage)
|
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
statementToAdd := &intoto.Statement{
|
statementToAdd := &intoto.Statement{
|
||||||
@@ -188,7 +186,7 @@ func TestAddSignedLayerAnnotations(t *testing.T) {
|
|||||||
data = []byte("test")
|
data = []byte("test")
|
||||||
testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType))
|
testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType))
|
||||||
mediaType := types.OCIManifestSchema1
|
mediaType := types.OCIManifestSchema1
|
||||||
opts := &SigningOptions{
|
opts := &attestation.SigningOptions{
|
||||||
Replace: tc.replace,
|
Replace: tc.replace,
|
||||||
}
|
}
|
||||||
manifest := attestation.AttestationManifest{
|
manifest := attestation.AttestationManifest{
|
||||||
@@ -202,6 +200,7 @@ func TestAddSignedLayerAnnotations(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
SubjectDescriptor: &v1.Descriptor{},
|
||||||
}
|
}
|
||||||
newImg, err := addSignedLayers(signedLayers, manifest, opts)
|
newImg, err := addSignedLayers(signedLayers, manifest, opts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ const (
|
|||||||
LifecycleStageExperimental = "experimental"
|
LifecycleStageExperimental = "experimental"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SigningOptions struct {
|
|
||||||
Replace bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Outcome string
|
type Outcome string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -6,13 +6,25 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/attest/pkg/attestation"
|
"github.com/docker/attest/pkg/attestation"
|
||||||
|
"github.com/docker/attest/pkg/config"
|
||||||
"github.com/docker/attest/pkg/oci"
|
"github.com/docker/attest/pkg/oci"
|
||||||
"github.com/docker/attest/pkg/policy"
|
"github.com/docker/attest/pkg/policy"
|
||||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Verify(ctx context.Context, opts *policy.PolicyOptions, resolver oci.AttestationResolver) (result *VerificationResult, err error) {
|
func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.PolicyOptions) (result *VerificationResult, err error) {
|
||||||
pctx, err := policy.ResolvePolicy(ctx, resolver, opts)
|
// so that we can resolve mapping from the image name earlier
|
||||||
|
detailsResolver, err := policy.CreateImageDetailsResolver(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create image details resolver: %w", err)
|
||||||
|
}
|
||||||
|
if opts.AttestationStyle == "" {
|
||||||
|
opts.AttestationStyle = config.AttestationStyleReferrers
|
||||||
|
}
|
||||||
|
if opts.ReferrersRepo != "" && opts.AttestationStyle != config.AttestationStyleReferrers {
|
||||||
|
return nil, fmt.Errorf("referrers repo specified but attestation source not set to referrers")
|
||||||
|
}
|
||||||
|
pctx, err := policy.ResolvePolicy(ctx, detailsResolver, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to resolve policy: %w", err)
|
return nil, fmt.Errorf("failed to resolve policy: %w", err)
|
||||||
}
|
}
|
||||||
@@ -22,7 +34,23 @@ func Verify(ctx context.Context, opts *policy.PolicyOptions, resolver oci.Attest
|
|||||||
Outcome: OutcomeNoPolicy,
|
Outcome: OutcomeNoPolicy,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
// this is overriding the mapping with a referrers config. Useful for testing if nothing else
|
||||||
|
if opts.ReferrersRepo != "" {
|
||||||
|
pctx.Mapping.Attestations = &config.ReferrersConfig{
|
||||||
|
Repo: opts.ReferrersRepo,
|
||||||
|
Style: config.AttestationStyleReferrers,
|
||||||
|
}
|
||||||
|
} else if opts.AttestationStyle == config.AttestationStyleAttached {
|
||||||
|
pctx.Mapping.Attestations = &config.ReferrersConfig{
|
||||||
|
Repo: opts.ReferrersRepo,
|
||||||
|
Style: config.AttestationStyleAttached,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// because we have a mapping now, we can select a resolver based on its contents (ie. referrers or attached)
|
||||||
|
resolver, err := policy.CreateAttestationResolver(detailsResolver, pctx.Mapping)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create attestation resolver: %w", err)
|
||||||
|
}
|
||||||
result, err = VerifyAttestations(ctx, resolver, pctx)
|
result, err = VerifyAttestations(ctx, resolver, pctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to evaluate policy: %w", err)
|
return nil, fmt.Errorf("failed to evaluate policy: %w", err)
|
||||||
@@ -37,7 +65,7 @@ func ToPolicyResult(p *policy.Policy, input *policy.PolicyInput, result *policy.
|
|||||||
}
|
}
|
||||||
subject := intoto.Subject{
|
subject := intoto.Subject{
|
||||||
Name: input.Purl,
|
Name: input.Purl,
|
||||||
Digest: *dgst,
|
Digest: dgst,
|
||||||
}
|
}
|
||||||
resourceUri, err := attestation.ToVSAResourceURI(subject)
|
resourceUri, err := attestation.ToVSAResourceURI(subject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -60,6 +88,7 @@ func ToPolicyResult(p *policy.Policy, input *policy.PolicyInput, result *policy.
|
|||||||
Policy: p,
|
Policy: p,
|
||||||
Outcome: outcome,
|
Outcome: outcome,
|
||||||
Violations: result.Violations,
|
Violations: result.Violations,
|
||||||
|
Input: input,
|
||||||
VSA: &intoto.Statement{
|
VSA: &intoto.Statement{
|
||||||
StatementHeader: intoto.StatementHeader{
|
StatementHeader: intoto.StatementHeader{
|
||||||
PredicateType: attestation.VSAPredicateType,
|
PredicateType: attestation.VSAPredicateType,
|
||||||
@@ -89,7 +118,11 @@ func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, p
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get image name: %w", err)
|
return nil, fmt.Errorf("failed to get image name: %w", err)
|
||||||
}
|
}
|
||||||
purl, canonical, err := oci.RefToPURL(name, resolver.ImagePlatformStr())
|
platform, err := resolver.ImagePlatform(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
purl, canonical, err := oci.RefToPURL(name, platform)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to convert ref to purl: %w", err)
|
return nil, fmt.Errorf("failed to convert ref to purl: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ var (
|
|||||||
ExampleAttestation = filepath.Join("..", "..", "test", "testdata", "example_attestation.json")
|
ExampleAttestation = filepath.Join("..", "..", "test", "testdata", "example_attestation.json")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LinuxAMD64 = "linux/amd64"
|
||||||
|
)
|
||||||
|
|
||||||
func TestVerifyAttestations(t *testing.T) {
|
func TestVerifyAttestations(t *testing.T) {
|
||||||
ex, err := os.ReadFile(ExampleAttestation)
|
ex, err := os.ReadFile(ExampleAttestation)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -73,10 +77,10 @@ func TestVSA(t *testing.T) {
|
|||||||
// setup an image with signed attestations
|
// setup an image with signed attestations
|
||||||
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
||||||
|
|
||||||
opts := &SigningOptions{
|
opts := &attestation.SigningOptions{
|
||||||
Replace: true,
|
Replace: true,
|
||||||
}
|
}
|
||||||
attIdx, err := oci.AttestationIndexFromPath(UnsignedTestImage)
|
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
|
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -94,21 +98,22 @@ func TestVSA(t *testing.T) {
|
|||||||
_, err = layout.Write(outputLayout, idx)
|
_, err = layout.Write(outputLayout, idx)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
//verify (without vsa should fail)
|
|
||||||
resolver := &oci.OCILayoutResolver{
|
|
||||||
Path: outputLayout,
|
|
||||||
Platform: "linux/amd64",
|
|
||||||
}
|
|
||||||
|
|
||||||
// mocked vsa query should pass
|
// mocked vsa query should pass
|
||||||
policyOpts := &policy.PolicyOptions{
|
policyOpts := &policy.PolicyOptions{
|
||||||
LocalPolicyDir: PassPolicyDir,
|
LocalPolicyDir: PassPolicyDir,
|
||||||
}
|
}
|
||||||
results, err := Verify(ctx, policyOpts, resolver)
|
src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64))
|
||||||
|
require.NoError(t, err)
|
||||||
|
results, err := Verify(ctx, src, policyOpts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, OutcomeSuccess, results.Outcome)
|
assert.Equal(t, OutcomeSuccess, results.Outcome)
|
||||||
assert.Empty(t, results.Violations)
|
assert.Empty(t, results.Violations)
|
||||||
|
|
||||||
|
if assert.NotNil(t, results.Input) {
|
||||||
|
assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", results.Input.Digest)
|
||||||
|
assert.False(t, results.Input.IsCanonical)
|
||||||
|
}
|
||||||
|
|
||||||
assert.Equal(t, intoto.StatementInTotoV01, results.VSA.Type)
|
assert.Equal(t, intoto.StatementInTotoV01, results.VSA.Type)
|
||||||
assert.Equal(t, attestation.VSAPredicateType, results.VSA.PredicateType)
|
assert.Equal(t, attestation.VSAPredicateType, results.VSA.PredicateType)
|
||||||
assert.Len(t, results.VSA.Subject, 1)
|
assert.Len(t, results.VSA.Subject, 1)
|
||||||
@@ -128,10 +133,10 @@ func TestVerificationFailure(t *testing.T) {
|
|||||||
// setup an image with signed attestations
|
// setup an image with signed attestations
|
||||||
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
||||||
|
|
||||||
opts := &SigningOptions{
|
opts := &attestation.SigningOptions{
|
||||||
Replace: true,
|
Replace: true,
|
||||||
}
|
}
|
||||||
attIdx, err := oci.AttestationIndexFromPath(UnsignedTestImage)
|
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
|
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -149,17 +154,13 @@ func TestVerificationFailure(t *testing.T) {
|
|||||||
_, err = layout.Write(outputLayout, idx)
|
_, err = layout.Write(outputLayout, idx)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
//verify (without vsa should fail)
|
// mocked vsa query should fail
|
||||||
resolver := &oci.OCILayoutResolver{
|
|
||||||
Path: outputLayout,
|
|
||||||
Platform: "linux/amd64",
|
|
||||||
}
|
|
||||||
|
|
||||||
// mocked vsa query should pass
|
|
||||||
policyOpts := &policy.PolicyOptions{
|
policyOpts := &policy.PolicyOptions{
|
||||||
LocalPolicyDir: FailPolicyDir,
|
LocalPolicyDir: FailPolicyDir,
|
||||||
}
|
}
|
||||||
results, err := Verify(ctx, policyOpts, resolver)
|
src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64))
|
||||||
|
require.NoError(t, err)
|
||||||
|
results, err := Verify(ctx, src, policyOpts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, OutcomeFailure, results.Outcome)
|
assert.Equal(t, OutcomeFailure, results.Outcome)
|
||||||
assert.Len(t, results.Violations, 1)
|
assert.Len(t, results.Violations, 1)
|
||||||
@@ -181,3 +182,59 @@ func TestVerificationFailure(t *testing.T) {
|
|||||||
assert.Equal(t, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels)
|
assert.Equal(t, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels)
|
||||||
assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI)
|
assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// test signing without a TL entry
|
||||||
|
func TestSignVerifyNoTL(t *testing.T) {
|
||||||
|
ctx, signer := test.Setup(t)
|
||||||
|
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
|
||||||
|
// setup an image with signed attestations
|
||||||
|
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
signTL bool
|
||||||
|
policyDir string
|
||||||
|
success bool
|
||||||
|
}{
|
||||||
|
{name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir, success: true},
|
||||||
|
{name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir, success: false},
|
||||||
|
{name: "no tl", signTL: false, policyDir: PassPolicyDir, success: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
opts := &attestation.SigningOptions{
|
||||||
|
Replace: true,
|
||||||
|
SkipTL: tc.signTL,
|
||||||
|
}
|
||||||
|
|
||||||
|
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// output signed attestations
|
||||||
|
idx := v1.ImageIndex(empty.Index)
|
||||||
|
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
||||||
|
Add: signedIndex,
|
||||||
|
Descriptor: v1.Descriptor{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
oci.OciReferenceTarget: attIdx.Name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
_, err = layout.Write(outputLayout, idx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
policyOpts := &policy.PolicyOptions{
|
||||||
|
LocalPolicyDir: tc.policyDir,
|
||||||
|
}
|
||||||
|
src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64))
|
||||||
|
require.NoError(t, err)
|
||||||
|
results, err := Verify(ctx, src, policyOpts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, OutcomeSuccess, results.Outcome)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,9 +17,19 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]AttestationManifes
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
||||||
}
|
}
|
||||||
|
subjects := make(map[string]*v1.Descriptor)
|
||||||
|
for _, subject := range idx.Manifests {
|
||||||
|
subjects[subject.Digest.String()] = &subject
|
||||||
|
}
|
||||||
|
|
||||||
var attestationManifests []AttestationManifest
|
var attestationManifests []AttestationManifest
|
||||||
for _, manifest := range idx.Manifests {
|
for _, manifest := range idx.Manifests {
|
||||||
|
|
||||||
if manifest.Annotations[DockerReferenceType] == AttestationManifestType {
|
if manifest.Annotations[DockerReferenceType] == AttestationManifestType {
|
||||||
|
subject := subjects[manifest.Annotations[DockerReferenceDigest]]
|
||||||
|
if subject == nil {
|
||||||
|
return nil, fmt.Errorf("failed to find subject for attestation manifest: %w", err)
|
||||||
|
}
|
||||||
attestationImage, err := index.Image(manifest.Digest)
|
attestationImage, err := index.Image(manifest.Digest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", manifest.Digest.String(), err)
|
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", manifest.Digest.String(), err)
|
||||||
@@ -30,7 +40,8 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]AttestationManifes
|
|||||||
}
|
}
|
||||||
attestationManifests = append(attestationManifests,
|
attestationManifests = append(attestationManifests,
|
||||||
AttestationManifest{
|
AttestationManifest{
|
||||||
Manifest: manifest,
|
Descriptor: manifest,
|
||||||
|
SubjectDescriptor: subject,
|
||||||
Attestation: AttestationImage{
|
Attestation: AttestationImage{
|
||||||
Layers: attestationLayers,
|
Layers: attestationLayers,
|
||||||
Image: attestationImage},
|
Image: attestationImage},
|
||||||
|
|||||||
253
pkg/attestation/referrers_test.go
Normal file
253
pkg/attestation/referrers_test.go
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
package attestation_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/attest/internal/test"
|
||||||
|
"github.com/docker/attest/pkg/attest"
|
||||||
|
"github.com/docker/attest/pkg/attestation"
|
||||||
|
"github.com/docker/attest/pkg/config"
|
||||||
|
"github.com/docker/attest/pkg/mirror"
|
||||||
|
"github.com/docker/attest/pkg/oci"
|
||||||
|
"github.com/docker/attest/pkg/policy"
|
||||||
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
|
"github.com/google/go-containerregistry/pkg/registry"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||||
|
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
|
||||||
|
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
|
||||||
|
LocalPolicy = filepath.Join("..", "..", "test", "testdata", "local-policy")
|
||||||
|
LocalPolicyAttached = filepath.Join("..", "..", "test", "testdata", "local-policy-attached")
|
||||||
|
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
|
||||||
|
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
|
||||||
|
TestTempDir = "attest-sign-test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAttestationReferenceTypes(t *testing.T) {
|
||||||
|
ctx, signer := test.Setup(t)
|
||||||
|
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
|
||||||
|
platforms := []string{"linux/amd64", "linux/arm64"}
|
||||||
|
for _, tc := range []struct {
|
||||||
|
server *httptest.Server
|
||||||
|
referrersServer *httptest.Server
|
||||||
|
skipSubject bool
|
||||||
|
useDigest bool
|
||||||
|
referrersRepo string
|
||||||
|
attestationSource config.AttestationStyle
|
||||||
|
expectFailure bool
|
||||||
|
policyDir string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: httptest.NewServer(registry.New()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
|
skipSubject: true,
|
||||||
|
attestationSource: config.AttestationStyleAttached,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
|
useDigest: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
|
expectFailure: true, //mismatched args
|
||||||
|
attestationSource: config.AttestationStyleAttached,
|
||||||
|
referrersRepo: "referrers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
|
expectFailure: true, // no policy
|
||||||
|
attestationSource: config.AttestationStyleReferrers,
|
||||||
|
referrersRepo: "referrers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
|
attestationSource: config.AttestationStyleReferrers,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(false))),
|
||||||
|
attestationSource: config.AttestationStyleReferrers,
|
||||||
|
referrersServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(fmt.Sprint(tc), func(t *testing.T) {
|
||||||
|
s := tc.server
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
if tc.referrersServer != nil {
|
||||||
|
defer tc.referrersServer.Close()
|
||||||
|
}
|
||||||
|
u, err := url.Parse(s.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
opts := &attestation.SigningOptions{
|
||||||
|
Replace: true,
|
||||||
|
SkipSubject: tc.skipSubject,
|
||||||
|
}
|
||||||
|
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if tc.referrersServer != nil {
|
||||||
|
ru, err := url.Parse(s.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := fmt.Sprintf("%s/referrers", ru.Host)
|
||||||
|
tc.referrersRepo = repo
|
||||||
|
images, err := attest.SignedAttestationImages(ctx, attIdx.Index, signer, opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
|
||||||
|
for _, img := range images {
|
||||||
|
err = mirror.PushImageToRegistry(img.Image, fmt.Sprintf("%s:tag-does-not-matter", repo))
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
signedIndex, err := attest.Sign(ctx, attIdx.Index, signer, opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = mirror.PushIndexToRegistry(signedIndex, indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, platform := range platforms {
|
||||||
|
// can eval policy in the normal way
|
||||||
|
ref := indexName
|
||||||
|
if tc.useDigest {
|
||||||
|
options := oci.WithOptions(ctx, nil)
|
||||||
|
subjectRef, err := name.ParseReference(indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
desc, err := remote.Index(subjectRef, options...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
idxDigest, err := desc.Digest()
|
||||||
|
require.NoError(t, err)
|
||||||
|
ref = fmt.Sprintf("%s/repo@%s", u.Host, idxDigest.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
policyOpts := &policy.PolicyOptions{
|
||||||
|
LocalPolicyDir: LocalPolicy,
|
||||||
|
}
|
||||||
|
if tc.policyDir != "" {
|
||||||
|
policyOpts.LocalPolicyDir = tc.policyDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.referrersRepo != "" {
|
||||||
|
policyOpts.ReferrersRepo = tc.referrersRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.attestationSource != "" {
|
||||||
|
policyOpts.AttestationStyle = tc.attestationSource
|
||||||
|
}
|
||||||
|
src, err := oci.ParseImageSpec(ref, oci.WithPlatform(platform))
|
||||||
|
require.NoError(t, err)
|
||||||
|
results, err := attest.Verify(ctx, src, policyOpts)
|
||||||
|
if tc.expectFailure {
|
||||||
|
require.Error(t, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
|
||||||
|
|
||||||
|
if !tc.skipSubject {
|
||||||
|
// can evaluate policy using referrers
|
||||||
|
if tc.useDigest {
|
||||||
|
p, err := oci.ParsePlatform(platform)
|
||||||
|
require.NoError(t, err)
|
||||||
|
options := oci.WithOptions(ctx, p)
|
||||||
|
subjectRef, err := name.ParseReference(indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
desc, err := remote.Image(subjectRef, options...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
subjectDigest, err := desc.Digest()
|
||||||
|
require.NoError(t, err)
|
||||||
|
ref = fmt.Sprintf("%s/repo@%s", u.Host, subjectDigest.String())
|
||||||
|
}
|
||||||
|
src, err := oci.ParseImageSpec(ref, oci.WithPlatform(platform))
|
||||||
|
require.NoError(t, err)
|
||||||
|
results, err = attest.Verify(ctx, src, policyOpts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReferencesInDifferentRepo(t *testing.T) {
|
||||||
|
ctx, signer := test.Setup(t)
|
||||||
|
repoName := "repo"
|
||||||
|
for _, tc := range []struct {
|
||||||
|
server *httptest.Server
|
||||||
|
refServer *httptest.Server
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
|
refServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: httptest.NewServer(registry.New()),
|
||||||
|
refServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
server := tc.server
|
||||||
|
defer server.Close()
|
||||||
|
serverUrl, err := url.Parse(server.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
refServer := tc.refServer
|
||||||
|
defer refServer.Close()
|
||||||
|
refServerUrl, err := url.Parse(refServer.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
opts := &attestation.SigningOptions{
|
||||||
|
Replace: true,
|
||||||
|
SkipTL: true,
|
||||||
|
}
|
||||||
|
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
indexName := fmt.Sprintf("%s/%s:latest", serverUrl.Host, repoName)
|
||||||
|
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
signedImages, err := attest.SignedAttestationImages(ctx, attIdx.Index, signer, opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// push signed attestation image to the ref server
|
||||||
|
for _, img := range signedImages {
|
||||||
|
// push references using subject-digest.att convention
|
||||||
|
err = mirror.PushImageToRegistry(img.Image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerUrl.Host, repoName))
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
mfs2, err := attIdx.Index.IndexManifest()
|
||||||
|
require.NoError(t, err)
|
||||||
|
for _, mf := range mfs2.Manifests {
|
||||||
|
//skip signed/unsigned attestations
|
||||||
|
if mf.Annotations[attestation.DockerReferenceType] == attestation.AttestationManifestType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// can evaluate policy using referrers in a different repo
|
||||||
|
referencedImage := fmt.Sprintf("%s@%s", indexName, mf.Digest.String())
|
||||||
|
policyOpts := &policy.PolicyOptions{
|
||||||
|
LocalPolicyDir: PassPolicyDir,
|
||||||
|
}
|
||||||
|
src, err := oci.ParseImageSpec(referencedImage)
|
||||||
|
require.NoError(t, err)
|
||||||
|
results, err := attest.Verify(ctx, src, policyOpts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,13 +6,13 @@ import (
|
|||||||
|
|
||||||
"github.com/docker/attest/internal/util"
|
"github.com/docker/attest/internal/util"
|
||||||
"github.com/docker/attest/pkg/tlog"
|
"github.com/docker/attest/pkg/tlog"
|
||||||
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SignDSSE signs a payload with a given signer and uploads the signature to the transparency log
|
// SignDSSE signs a payload with a given signer and uploads the signature to the transparency log
|
||||||
func SignDSSE(ctx context.Context, payload []byte, payloadType string, signer dsse.SignerVerifier) (*Envelope, error) {
|
func SignDSSE(ctx context.Context, payload []byte, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) {
|
||||||
t := tlog.GetTL(ctx)
|
payloadType := intoto.PayloadType
|
||||||
|
|
||||||
env := new(Envelope)
|
env := new(Envelope)
|
||||||
env.Payload = base64Encoding.EncodeToString(payload)
|
env.Payload = base64Encoding.EncodeToString(payload)
|
||||||
env.PayloadType = payloadType
|
env.PayloadType = payloadType
|
||||||
@@ -33,8 +33,31 @@ func SignDSSE(ctx context.Context, payload []byte, payloadType string, signer ds
|
|||||||
return nil, fmt.Errorf("error getting public key ID: %w", err)
|
return nil, fmt.Errorf("error getting public key ID: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload to TL
|
dsseSig := Signature{
|
||||||
entry, err := t.UploadLogEntry(ctx, keyId, encPayload, sig, signer)
|
KeyID: keyId,
|
||||||
|
Sig: base64Encoding.EncodeToString(sig),
|
||||||
|
}
|
||||||
|
if !opts.SkipTL {
|
||||||
|
ext, err := logSignature(ctx, tlog.GetTL(ctx), &sig, &encPayload, signer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to log to rekor: %w", err)
|
||||||
|
}
|
||||||
|
dsseSig.Extension = *ext
|
||||||
|
}
|
||||||
|
// add signature to dsse envelope
|
||||||
|
env.Signatures = []Signature{dsseSig}
|
||||||
|
|
||||||
|
return env, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a new envelope with the transparency log entry added to the signature extension
|
||||||
|
func logSignature(ctx context.Context, t tlog.TL, sig *[]byte, encPayload *[]byte, signer dsse.SignerVerifier) (*Extension, error) {
|
||||||
|
// get Key ID from signer
|
||||||
|
keyId, err := signer.KeyID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting public key ID: %w", err)
|
||||||
|
}
|
||||||
|
entry, err := t.UploadLogEntry(ctx, keyId, *encPayload, *sig, signer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error uploading TL entry: %w", err)
|
return nil, fmt.Errorf("error uploading TL entry: %w", err)
|
||||||
}
|
}
|
||||||
@@ -42,21 +65,13 @@ func SignDSSE(ctx context.Context, payload []byte, payloadType string, signer ds
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error unmarshaling tl entry: %w", err)
|
return nil, fmt.Errorf("error unmarshaling tl entry: %w", err)
|
||||||
}
|
}
|
||||||
|
return &Extension{
|
||||||
// add signature w/ tl extension to dsse envelope
|
Kind: DockerDsseExtKind,
|
||||||
env.Signatures = append(env.Signatures, Signature{
|
Ext: DockerDsseExtension{
|
||||||
KeyID: keyId,
|
Tl: DockerTlExtension{
|
||||||
Sig: base64Encoding.EncodeToString(sig),
|
Kind: RekorTlExtKind,
|
||||||
Extension: Extension{
|
Data: entryObj, // transparency log entry metadata
|
||||||
Kind: DockerDsseExtKind,
|
|
||||||
Ext: DockerDsseExtension{
|
|
||||||
Tl: DockerTlExtension{
|
|
||||||
Kind: RekorTlExtKind,
|
|
||||||
Data: entryObj, // transparency log entry metadata
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}, nil
|
||||||
|
|
||||||
return env, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/docker/attest/pkg/signerverifier"
|
"github.com/docker/attest/pkg/signerverifier"
|
||||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSignVerifyAttestation(t *testing.T) {
|
func TestSignVerifyAttestation(t *testing.T) {
|
||||||
@@ -27,17 +28,17 @@ func TestSignVerifyAttestation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
payload, err := json.Marshal(stmt)
|
payload, err := json.Marshal(stmt)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
opts := &attestation.SigningOptions{}
|
||||||
env, err := attestation.SignDSSE(ctx, payload, intoto.PayloadType, signer)
|
env, err := attestation.SignDSSE(ctx, payload, signer, opts)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// marshal envelope to json to test for bugs when marshaling envelope data
|
// marshal envelope to json to test for bugs when marshaling envelope data
|
||||||
serializedEnv, err := json.Marshal(env)
|
serializedEnv, err := json.Marshal(env)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
deserializedEnv := new(attestation.Envelope)
|
deserializedEnv := new(attestation.Envelope)
|
||||||
err = json.Unmarshal(serializedEnv, deserializedEnv)
|
err = json.Unmarshal(serializedEnv, deserializedEnv)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// signer.Public() calls AWS API when using AWS signer, use attestation.GetPublicVerificationKey() to get key from TUF repo
|
// signer.Public() calls AWS API when using AWS signer, use attestation.GetPublicVerificationKey() to get key from TUF repo
|
||||||
// signer.Public() used here for test purposes
|
// signer.Public() used here for test purposes
|
||||||
@@ -49,10 +50,10 @@ func TestSignVerifyAttestation(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
badKeyPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
badKeyPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
badKey := &badKeyPriv.PublicKey
|
badKey := &badKeyPriv.PublicKey
|
||||||
badPEM, err := signerverifier.ToPEM(badKey)
|
badPEM, err := signerverifier.ToPEM(badKey)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -136,7 +137,10 @@ func TestSignVerifyAttestation(t *testing.T) {
|
|||||||
To: tc.to,
|
To: tc.to,
|
||||||
Status: tc.status,
|
Status: tc.status,
|
||||||
}
|
}
|
||||||
_, err = attestation.VerifyDSSE(ctx, deserializedEnv, attestation.KeysMap{tc.keyId: keyMeta})
|
opts := &attestation.VerifyOptions{
|
||||||
|
Keys: attestation.Keys{keyMeta},
|
||||||
|
}
|
||||||
|
_, err = attestation.VerifyDSSE(ctx, deserializedEnv, opts)
|
||||||
if tc.expectedError != "" {
|
if tc.expectedError != "" {
|
||||||
assert.Contains(t, err.Error(), tc.expectedError)
|
assert.Contains(t, err.Error(), tc.expectedError)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
DockerReferenceType = "vnd.docker.reference.type"
|
DockerReferenceType = "vnd.docker.reference.type"
|
||||||
AttestationManifestType = "attestation-manifest"
|
AttestationManifestType = "attestation-manifest"
|
||||||
|
DockerReferenceDigest = "vnd.docker.reference.digest"
|
||||||
DockerDsseExtKind = "application/vnd.docker.attestation-verification.v1+json"
|
DockerDsseExtKind = "application/vnd.docker.attestation-verification.v1+json"
|
||||||
RekorTlExtKind = "Rekor"
|
RekorTlExtKind = "Rekor"
|
||||||
OCIDescriptorDSSEMediaType = ociv1.MediaTypeDescriptor + "+dsse"
|
OCIDescriptorDSSEMediaType = ociv1.MediaTypeDescriptor + "+dsse"
|
||||||
@@ -33,12 +34,19 @@ type AttestationImage struct {
|
|||||||
Image v1.Image
|
Image v1.Image
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SignedAttestationImage struct {
|
||||||
|
Image v1.Image
|
||||||
|
Descriptor *v1.Descriptor
|
||||||
|
AttestationManifest AttestationManifest
|
||||||
|
}
|
||||||
|
|
||||||
type AttestationManifest struct {
|
type AttestationManifest struct {
|
||||||
Manifest v1.Descriptor
|
Descriptor v1.Descriptor
|
||||||
Attestation AttestationImage
|
Attestation AttestationImage
|
||||||
MediaType types.MediaType
|
MediaType types.MediaType
|
||||||
Annotations map[string]string
|
Annotations map[string]string
|
||||||
Digest v1.Hash
|
Digest v1.Hash
|
||||||
|
SubjectDescriptor *v1.Descriptor
|
||||||
}
|
}
|
||||||
|
|
||||||
// the following types are needed until https://github.com/secure-systems-lab/dsse/pull/61 is merged
|
// the following types are needed until https://github.com/secure-systems-lab/dsse/pull/61 is merged
|
||||||
@@ -66,6 +74,20 @@ type DockerTlExtension struct {
|
|||||||
Data any `json:"data"`
|
Data any `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VerifyOptions struct {
|
||||||
|
Keys []KeyMetadata `json:"keys"`
|
||||||
|
SkipTL bool `json:"skip_tl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SigningOptions struct {
|
||||||
|
// replace unsigned statements with signed attestations
|
||||||
|
Replace bool
|
||||||
|
// don't log to the configured transparency log
|
||||||
|
SkipTL bool
|
||||||
|
// don't add OCI subject field to attestation image
|
||||||
|
SkipSubject bool
|
||||||
|
}
|
||||||
|
|
||||||
func DSSEMediaType(predicateType string) (string, error) {
|
func DSSEMediaType(predicateType string) (string, error) {
|
||||||
var predicateName string
|
var predicateName string
|
||||||
switch predicateType {
|
switch predicateType {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type KeyMetadata struct {
|
|||||||
type Keys []KeyMetadata
|
type Keys []KeyMetadata
|
||||||
type KeysMap map[string]KeyMetadata
|
type KeysMap map[string]KeyMetadata
|
||||||
|
|
||||||
func VerifyDSSE(ctx context.Context, env *Envelope, keys KeysMap) ([]byte, error) {
|
func VerifyDSSE(ctx context.Context, env *Envelope, opts *VerifyOptions) ([]byte, error) {
|
||||||
// enforce payload type
|
// enforce payload type
|
||||||
if !ValidPayloadType(env.PayloadType) {
|
if !ValidPayloadType(env.PayloadType) {
|
||||||
return nil, fmt.Errorf("unsupported payload type %s", env.PayloadType)
|
return nil, fmt.Errorf("unsupported payload type %s", env.PayloadType)
|
||||||
@@ -49,7 +49,7 @@ func VerifyDSSE(ctx context.Context, env *Envelope, keys KeysMap) ([]byte, error
|
|||||||
|
|
||||||
// verify signatures and transparency log entry
|
// verify signatures and transparency log entry
|
||||||
for _, sig := range env.Signatures {
|
for _, sig := range env.Signatures {
|
||||||
err := verifySignature(ctx, sig, encPayload, keys)
|
err := verifySignature(ctx, sig, encPayload, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -58,31 +58,11 @@ func VerifyDSSE(ctx context.Context, env *Envelope, keys KeysMap) ([]byte, error
|
|||||||
return payload, nil
|
return payload, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifySignature(ctx context.Context, sig Signature, payload []byte, keys KeysMap) error {
|
func verifySignature(ctx context.Context, sig Signature, payload []byte, opts *VerifyOptions) error {
|
||||||
t := tlog.GetTL(ctx)
|
keys := make(map[string]KeyMetadata, len(opts.Keys))
|
||||||
|
for _, key := range opts.Keys {
|
||||||
if sig.Extension.Kind == "" {
|
keys[key.ID] = key
|
||||||
return fmt.Errorf("error missing signature extension kind")
|
|
||||||
}
|
}
|
||||||
if sig.Extension.Kind != DockerDsseExtKind {
|
|
||||||
return fmt.Errorf("error unsupported signature extension kind: %s", sig.Extension.Kind)
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify TL entry
|
|
||||||
if sig.Extension.Ext.Tl.Kind != RekorTlExtKind {
|
|
||||||
return fmt.Errorf("error unsupported TL extension kind: %s", sig.Extension.Ext.Tl.Kind)
|
|
||||||
}
|
|
||||||
entry := sig.Extension.Ext.Tl.Data
|
|
||||||
entryBytes, err := json.Marshal(entry)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal TL entry: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
integratedTime, err := t.VerifyLogEntry(ctx, entryBytes)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("TL entry failed verification: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
keyMeta, ok := keys[sig.KeyID]
|
keyMeta, ok := keys[sig.KeyID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("error key not found: %s", sig.KeyID)
|
return fmt.Errorf("error key not found: %s", sig.KeyID)
|
||||||
@@ -91,30 +71,53 @@ func verifySignature(ctx context.Context, sig Signature, payload []byte, keys Ke
|
|||||||
if keyMeta.Distrust {
|
if keyMeta.Distrust {
|
||||||
return fmt.Errorf("key %s is distrusted", keyMeta.ID)
|
return fmt.Errorf("key %s is distrusted", keyMeta.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if integratedTime.Before(keyMeta.From) {
|
|
||||||
return fmt.Errorf("key %s was not yet valid at TL log time %s (key valid from %s)", keyMeta.ID, integratedTime, keyMeta.From)
|
|
||||||
}
|
|
||||||
|
|
||||||
if keyMeta.To != nil && !integratedTime.Before(*keyMeta.To) {
|
|
||||||
return fmt.Errorf("key %s was already %s at TL log time %s (key %s at %s)", keyMeta.ID, keyMeta.Status, integratedTime, keyMeta.Status, *keyMeta.To)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this is unmarshalling with MarshalPKIXPublicKey only for us to marshal it again
|
// TODO: this is unmarshalling with MarshalPKIXPublicKey only for us to marshal it again
|
||||||
publicKey, err := signerverifier.Parse([]byte(keyMeta.PEM))
|
publicKey, err := signerverifier.Parse([]byte(keyMeta.PEM))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse public key: %w", err)
|
return fmt.Errorf("failed to parse public key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify TL entry payload
|
if !opts.SkipTL {
|
||||||
encodedPub, err := x509.MarshalPKIXPublicKey(publicKey)
|
t := tlog.GetTL(ctx)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error failed to marshal public key: %w", err)
|
if sig.Extension.Kind == "" {
|
||||||
}
|
return fmt.Errorf("error missing signature extension kind")
|
||||||
err = t.VerifyEntryPayload(entryBytes, payload, encodedPub)
|
}
|
||||||
if err != nil {
|
if sig.Extension.Kind != DockerDsseExtKind {
|
||||||
return fmt.Errorf("TL entry failed payload verification: %w", err)
|
return fmt.Errorf("error unsupported signature extension kind: %s", sig.Extension.Kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify TL entry
|
||||||
|
if sig.Extension.Ext.Tl.Kind != RekorTlExtKind {
|
||||||
|
return fmt.Errorf("error unsupported TL extension kind: %s", sig.Extension.Ext.Tl.Kind)
|
||||||
|
}
|
||||||
|
entry := sig.Extension.Ext.Tl.Data
|
||||||
|
entryBytes, err := json.Marshal(entry)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal TL entry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
integratedTime, err := t.VerifyLogEntry(ctx, entryBytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("TL entry failed verification: %w", err)
|
||||||
|
}
|
||||||
|
if integratedTime.Before(keyMeta.From) {
|
||||||
|
return fmt.Errorf("key %s was not yet valid at TL log time %s (key valid from %s)", keyMeta.ID, integratedTime, keyMeta.From)
|
||||||
|
}
|
||||||
|
if keyMeta.To != nil && !integratedTime.Before(*keyMeta.To) {
|
||||||
|
return fmt.Errorf("key %s was already %s at TL log time %s (key %s at %s)", keyMeta.ID, keyMeta.Status, integratedTime, keyMeta.Status, *keyMeta.To)
|
||||||
|
}
|
||||||
|
// verify TL entry payload
|
||||||
|
encodedPub, err := x509.MarshalPKIXPublicKey(publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error failed to marshal public key: %w", err)
|
||||||
|
}
|
||||||
|
err = t.VerifyEntryPayload(entryBytes, payload, encodedPub)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("TL entry failed payload verification: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode signature
|
// decode signature
|
||||||
signature, err := base64.StdEncoding.Strict().DecodeString(sig.Sig)
|
signature, err := base64.StdEncoding.Strict().DecodeString(sig.Sig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -39,8 +39,11 @@ func TestVerifyUnsignedAttestation(t *testing.T) {
|
|||||||
Payload: base64.StdEncoding.EncodeToString(payload),
|
Payload: base64.StdEncoding.EncodeToString(payload),
|
||||||
PayloadType: intoto.PayloadType,
|
PayloadType: intoto.PayloadType,
|
||||||
}
|
}
|
||||||
|
opts := &attestation.VerifyOptions{
|
||||||
|
Keys: attestation.Keys{},
|
||||||
|
}
|
||||||
|
|
||||||
_, err := attestation.VerifyDSSE(ctx, env, attestation.KeysMap{})
|
_, err := attestation.VerifyDSSE(ctx, env, opts)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "no signatures")
|
assert.Contains(t, err.Error(), "no signatures")
|
||||||
}
|
}
|
||||||
|
|||||||
49
pkg/config/config.go
Normal file
49
pkg/config/config.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/docker/attest/pkg/tuf"
|
||||||
|
goyaml "gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MappingFilename = "mapping.yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadLocalMappings(configDir string) (*PolicyMappings, error) {
|
||||||
|
if configDir == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
mappings := &PolicyMappings{}
|
||||||
|
path := filepath.Join(configDir, MappingFilename)
|
||||||
|
mappingFile, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read local policy mapping file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
err = goyaml.Unmarshal(mappingFile, mappings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
return mappings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMappings, error) {
|
||||||
|
if tufClient == nil {
|
||||||
|
return nil, fmt.Errorf("tuf client not set")
|
||||||
|
}
|
||||||
|
filename := MappingFilename
|
||||||
|
_, fileContents, err := tufClient.DownloadTarget(filename, filepath.Join(localTargetsDir, filename))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to download policy mapping file %s: %w", filename, err)
|
||||||
|
}
|
||||||
|
mappings := &PolicyMappings{}
|
||||||
|
|
||||||
|
err = goyaml.Unmarshal(fileContents, mappings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", filename, err)
|
||||||
|
}
|
||||||
|
return mappings, nil
|
||||||
|
}
|
||||||
48
pkg/config/types.go
Normal file
48
pkg/config/types.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type PolicyMappings struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Policies []*PolicyMapping `json:"policies"`
|
||||||
|
Mirrors []*PolicyMirror `json:"mirrors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttestationStyle string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AttestationStyleAttached AttestationStyle = "attached"
|
||||||
|
AttestationStyleReferrers AttestationStyle = "referrers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PolicyMapping struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Origin *PolicyOrigin `json:"origin"`
|
||||||
|
Files []PolicyMappingFile `json:"files"`
|
||||||
|
Attestations *ReferrersConfig `json:"attestations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReferrersConfig struct {
|
||||||
|
Style AttestationStyle `json:"style"`
|
||||||
|
Repo string `json:"repo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyMappingFile struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyMirror struct {
|
||||||
|
PolicyId string `yaml:"policy-id"`
|
||||||
|
Mirror MirrorSpec `json:"mirror"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MirrorSpec struct {
|
||||||
|
Domains []string `json:"domains"`
|
||||||
|
Prefix string `json:"prefix"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyOrigin struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Prefix string `json:"prefix"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type TufMirrorOutput struct {
|
type TufMirrorOutput struct {
|
||||||
metadata *v1.Image
|
metadata v1.Image
|
||||||
delegatedMetadata []*mirror.MirrorImage
|
delegatedMetadata []*mirror.MirrorImage
|
||||||
targets []*mirror.MirrorImage
|
targets []*mirror.MirrorImage
|
||||||
delegatedTargets []*mirror.MirrorIndex
|
delegatedTargets []*mirror.MirrorIndex
|
||||||
@@ -80,7 +80,7 @@ func ExampleNewTufMirror() {
|
|||||||
func mirrorToRegistry(o *TufMirrorOutput) error {
|
func mirrorToRegistry(o *TufMirrorOutput) error {
|
||||||
// push metadata to registry
|
// push metadata to registry
|
||||||
metadataRepo := "registry-1.docker.io/docker/tuf-metadata:latest"
|
metadataRepo := "registry-1.docker.io/docker/tuf-metadata:latest"
|
||||||
err := mirror.PushToRegistry(o.metadata, metadataRepo)
|
err := mirror.PushImageToRegistry(o.metadata, metadataRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ func mirrorToRegistry(o *TufMirrorOutput) error {
|
|||||||
return fmt.Errorf("failed to get repo without tag: %s", metadataRepo)
|
return fmt.Errorf("failed to get repo without tag: %s", metadataRepo)
|
||||||
}
|
}
|
||||||
imageName := fmt.Sprintf("%s:%s", repo, metadata.Tag)
|
imageName := fmt.Sprintf("%s:%s", repo, metadata.Tag)
|
||||||
err = mirror.PushToRegistry(metadata.Image, imageName)
|
err = mirror.PushImageToRegistry(metadata.Image, imageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ func mirrorToRegistry(o *TufMirrorOutput) error {
|
|||||||
targetsRepo := "registry-1.docker.io/docker/tuf-targets"
|
targetsRepo := "registry-1.docker.io/docker/tuf-targets"
|
||||||
for _, target := range o.targets {
|
for _, target := range o.targets {
|
||||||
imageName := fmt.Sprintf("%s:%s", targetsRepo, target.Tag)
|
imageName := fmt.Sprintf("%s:%s", targetsRepo, target.Tag)
|
||||||
err = mirror.PushToRegistry(target.Image, imageName)
|
err = mirror.PushImageToRegistry(target.Image, imageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,7 @@ func mirrorToRegistry(o *TufMirrorOutput) error {
|
|||||||
// push delegated targets to registry
|
// push delegated targets to registry
|
||||||
for _, target := range o.delegatedTargets {
|
for _, target := range o.delegatedTargets {
|
||||||
imageName := fmt.Sprintf("%s:%s", targetsRepo, target.Tag)
|
imageName := fmt.Sprintf("%s:%s", targetsRepo, target.Tag)
|
||||||
err = mirror.PushToRegistry(target.Index, imageName)
|
err = mirror.PushIndexToRegistry(target.Index, imageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -119,14 +119,14 @@ func mirrorToRegistry(o *TufMirrorOutput) error {
|
|||||||
|
|
||||||
func mirrorToLocal(o *TufMirrorOutput, outputPath string) error {
|
func mirrorToLocal(o *TufMirrorOutput, outputPath string) error {
|
||||||
// output metadata to local directory
|
// output metadata to local directory
|
||||||
err := mirror.SaveAsOCILayout(o.metadata, outputPath)
|
err := mirror.SaveImageAsOCILayout(o.metadata, outputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// output delegated metadata to local directory
|
// output delegated metadata to local directory
|
||||||
for _, metadata := range o.delegatedMetadata {
|
for _, metadata := range o.delegatedMetadata {
|
||||||
path := filepath.Join(outputPath, metadata.Tag)
|
path := filepath.Join(outputPath, metadata.Tag)
|
||||||
err = mirror.SaveAsOCILayout(metadata.Image, path)
|
err = mirror.SaveImageAsOCILayout(metadata.Image, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -135,7 +135,7 @@ func mirrorToLocal(o *TufMirrorOutput, outputPath string) error {
|
|||||||
// output top-level targets to local directory
|
// output top-level targets to local directory
|
||||||
for _, target := range o.targets {
|
for _, target := range o.targets {
|
||||||
path := filepath.Join(outputPath, target.Tag)
|
path := filepath.Join(outputPath, target.Tag)
|
||||||
err = mirror.SaveAsOCILayout(target.Image, path)
|
err = mirror.SaveImageAsOCILayout(target.Image, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -143,7 +143,7 @@ func mirrorToLocal(o *TufMirrorOutput, outputPath string) error {
|
|||||||
// output delegated targets to local directory
|
// output delegated targets to local directory
|
||||||
for _, target := range o.delegatedTargets {
|
for _, target := range o.delegatedTargets {
|
||||||
path := filepath.Join(outputPath, target.Tag)
|
path := filepath.Join(outputPath, target.Tag)
|
||||||
err = mirror.SaveAsOCILayout(target.Index, path)
|
err = mirror.SaveIndexAsOCILayout(target.Index, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
// -----------------
|
// -----------------
|
||||||
|
|
||||||
// GetMetadataManifest returns an image with TUF root metadata as layers
|
// GetMetadataManifest returns an image with TUF root metadata as layers
|
||||||
func (m *TufMirror) GetMetadataManifest(metadataURL string) (*v1.Image, error) {
|
func (m *TufMirror) GetMetadataManifest(metadataURL string) (v1.Image, error) {
|
||||||
metadata, err := m.getTufMetadataMirror(metadataURL)
|
metadata, err := m.getTufMetadataMirror(metadataURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get metadata: %w", err)
|
return nil, fmt.Errorf("failed to get metadata: %w", err)
|
||||||
@@ -78,7 +78,7 @@ func (m *TufMirror) getTufMetadataMirror(metadataURL string) (*TufMetadata, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildMetadataManifest returns an OCI image with TUF metadata as layers with annotations
|
// buildMetadataManifest returns an OCI image with TUF metadata as layers with annotations
|
||||||
func (m *TufMirror) buildMetadataManifest(metadata *TufMetadata) (*v1.Image, error) {
|
func (m *TufMirror) buildMetadataManifest(metadata *TufMetadata) (v1.Image, error) {
|
||||||
img := empty.Image
|
img := empty.Image
|
||||||
img = mutate.MediaType(img, types.OCIManifestSchema1)
|
img = mutate.MediaType(img, types.OCIManifestSchema1)
|
||||||
img = mutate.ConfigMediaType(img, types.OCIConfigJSON)
|
img = mutate.ConfigMediaType(img, types.OCIConfigJSON)
|
||||||
@@ -87,17 +87,17 @@ func (m *TufMirror) buildMetadataManifest(metadata *TufMetadata) (*v1.Image, err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to make role layer: %w", err)
|
return nil, fmt.Errorf("failed to make role layer: %w", err)
|
||||||
}
|
}
|
||||||
img, err = mutate.Append(img, *layers...)
|
img, err = mutate.Append(img, layers...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to append role layer to image: %w", err)
|
return nil, fmt.Errorf("failed to append role layer to image: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &img, nil
|
return img, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeRoleLayers returns a list of layers for a given TUF role
|
// makeRoleLayers returns a list of layers for a given TUF role
|
||||||
func (m *TufMirror) makeRoleLayers(role TufRole, tufMetadata *TufMetadata) (*[]mutate.Addendum, error) {
|
func (m *TufMirror) makeRoleLayers(role TufRole, tufMetadata *TufMetadata) ([]mutate.Addendum, error) {
|
||||||
layers := new([]mutate.Addendum)
|
var layers []mutate.Addendum
|
||||||
ann := map[string]string{tufFileAnnotation: ""}
|
ann := map[string]string{tufFileAnnotation: ""}
|
||||||
switch role {
|
switch role {
|
||||||
case metadata.ROOT:
|
case metadata.ROOT:
|
||||||
@@ -108,7 +108,7 @@ func (m *TufMirror) makeRoleLayers(role TufRole, tufMetadata *TufMetadata) (*[]m
|
|||||||
layers = m.annotatedMetaLayers(tufMetadata.Targets)
|
layers = m.annotatedMetaLayers(tufMetadata.Targets)
|
||||||
case metadata.TIMESTAMP:
|
case metadata.TIMESTAMP:
|
||||||
ann[tufFileAnnotation] = fmt.Sprintf("%s.json", role)
|
ann[tufFileAnnotation] = fmt.Sprintf("%s.json", role)
|
||||||
*layers = append(*layers, mutate.Addendum{Layer: static.NewLayer(tufMetadata.Timestamp, tufMetadataMediaType), Annotations: ann})
|
layers = append(layers, mutate.Addendum{Layer: static.NewLayer(tufMetadata.Timestamp, tufMetadataMediaType), Annotations: ann})
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported TUF role: %s", role)
|
return nil, fmt.Errorf("unsupported TUF role: %s", role)
|
||||||
}
|
}
|
||||||
@@ -116,11 +116,11 @@ func (m *TufMirror) makeRoleLayers(role TufRole, tufMetadata *TufMetadata) (*[]m
|
|||||||
}
|
}
|
||||||
|
|
||||||
// annotatedMetaLayers returns a list of layers with annotations for each TUF metadata file
|
// annotatedMetaLayers returns a list of layers with annotations for each TUF metadata file
|
||||||
func (m *TufMirror) annotatedMetaLayers(meta map[string][]byte) *[]mutate.Addendum {
|
func (m *TufMirror) annotatedMetaLayers(meta map[string][]byte) []mutate.Addendum {
|
||||||
layers := new([]mutate.Addendum)
|
var layers []mutate.Addendum
|
||||||
for name, data := range meta {
|
for name, data := range meta {
|
||||||
ann := map[string]string{tufFileAnnotation: name}
|
ann := map[string]string{tufFileAnnotation: name}
|
||||||
*layers = append(*layers, mutate.Addendum{Layer: static.NewLayer(data, tufMetadataMediaType), Annotations: ann})
|
layers = append(layers, mutate.Addendum{Layer: static.NewLayer(data, tufMetadataMediaType), Annotations: ann})
|
||||||
}
|
}
|
||||||
return layers
|
return layers
|
||||||
}
|
}
|
||||||
@@ -144,8 +144,8 @@ func (m *TufMirror) GetDelegatedMetadataMirrors() ([]*MirrorImage, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getDelegatedTargetsMetadata returns delegated targets metadata as a list of DelegatedTargetMetadata (role name and data)
|
// getDelegatedTargetsMetadata returns delegated targets metadata as a list of DelegatedTargetMetadata (role name and data)
|
||||||
func (m *TufMirror) getDelegatedTargetsMetadata() (*[]DelegatedTargetMetadata, error) {
|
func (m *TufMirror) getDelegatedTargetsMetadata() ([]DelegatedTargetMetadata, error) {
|
||||||
delegatedTargets := new([]DelegatedTargetMetadata)
|
var delegatedTargets []DelegatedTargetMetadata
|
||||||
md := m.TufClient.GetMetadata()
|
md := m.TufClient.GetMetadata()
|
||||||
for _, role := range md.Targets[metadata.TARGETS].Signed.Delegations.Roles {
|
for _, role := range md.Targets[metadata.TARGETS].Signed.Delegations.Roles {
|
||||||
roleMetadata, err := m.TufClient.LoadDelegatedTargets(role.Name, metadata.TARGETS)
|
roleMetadata, err := m.TufClient.LoadDelegatedTargets(role.Name, metadata.TARGETS)
|
||||||
@@ -165,15 +165,15 @@ func (m *TufMirror) getDelegatedTargetsMetadata() (*[]DelegatedTargetMetadata, e
|
|||||||
if md.Root.Signed.ConsistentSnapshot {
|
if md.Root.Signed.ConsistentSnapshot {
|
||||||
version = strconv.FormatInt(meta.Version, 10)
|
version = strconv.FormatInt(meta.Version, 10)
|
||||||
}
|
}
|
||||||
*delegatedTargets = append(*delegatedTargets, DelegatedTargetMetadata{Name: role.Name, Version: version, Data: roleBytes})
|
delegatedTargets = append(delegatedTargets, DelegatedTargetMetadata{Name: role.Name, Version: version, Data: roleBytes})
|
||||||
}
|
}
|
||||||
return delegatedTargets, nil
|
return delegatedTargets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildDelegatedMetadataManifests returns a list of mirrors (image/tag pairs) for each delegated target role metadata
|
// buildDelegatedMetadataManifests returns a list of mirrors (image/tag pairs) for each delegated target role metadata
|
||||||
func (m *TufMirror) buildDelegatedMetadataManifests(delegated *[]DelegatedTargetMetadata) ([]*MirrorImage, error) {
|
func (m *TufMirror) buildDelegatedMetadataManifests(delegated []DelegatedTargetMetadata) ([]*MirrorImage, error) {
|
||||||
manifests := []*MirrorImage{}
|
manifests := []*MirrorImage{}
|
||||||
for _, role := range *delegated {
|
for _, role := range delegated {
|
||||||
img := empty.Image
|
img := empty.Image
|
||||||
img = mutate.MediaType(img, types.OCIManifestSchema1)
|
img = mutate.MediaType(img, types.OCIManifestSchema1)
|
||||||
img = mutate.ConfigMediaType(img, types.OCIConfigJSON)
|
img = mutate.ConfigMediaType(img, types.OCIConfigJSON)
|
||||||
@@ -183,7 +183,7 @@ func (m *TufMirror) buildDelegatedMetadataManifests(delegated *[]DelegatedTarget
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to append delegated targets layer to image: %w", err)
|
return nil, fmt.Errorf("failed to append delegated targets layer to image: %w", err)
|
||||||
}
|
}
|
||||||
manifests = append(manifests, &MirrorImage{Image: &img, Tag: role.Name})
|
manifests = append(manifests, &MirrorImage{Image: img, Tag: role.Name})
|
||||||
}
|
}
|
||||||
return manifests, nil
|
return manifests, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,8 +46,7 @@ func TestGetMetadataManifest(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, img)
|
assert.NotNil(t, img)
|
||||||
|
|
||||||
image := *img
|
mf, err := img.RawManifest()
|
||||||
mf, err := image.RawManifest()
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
type Annotations struct {
|
type Annotations struct {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package mirror
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/docker/attest/internal/embed"
|
"github.com/docker/attest/internal/embed"
|
||||||
@@ -26,62 +25,60 @@ func NewTufMirror(root []byte, tufPath, metadataURL, targetsURL string, versionC
|
|||||||
return &TufMirror{TufClient: tufClient, tufPath: tufPath, metadataURL: metadataURL, targetsURL: targetsURL}, nil
|
return &TufMirror{TufClient: tufClient, tufPath: tufPath, metadataURL: metadataURL, targetsURL: targetsURL}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PushToRegistry(image any, imageName string) error {
|
func PushImageToRegistry(image v1.Image, imageName string) error {
|
||||||
// Parse the image name
|
// Parse the image name
|
||||||
ref, err := name.ParseReference(imageName)
|
ref, err := name.ParseReference(imageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to parse image name: %v", err)
|
return fmt.Errorf("Failed to parse image name '%s': %w", imageName, err)
|
||||||
}
|
}
|
||||||
// Get the authenticator from the default Docker keychain
|
// Get the authenticator from the default Docker keychain
|
||||||
auth, err := authn.DefaultKeychain.Resolve(ref.Context())
|
auth, err := authn.DefaultKeychain.Resolve(ref.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to get authenticator: %v", err)
|
return fmt.Errorf("Failed to get authenticator: %w", err)
|
||||||
}
|
}
|
||||||
// Push the image to the registry
|
// Push the image to the registry
|
||||||
switch image := image.(type) {
|
return remote.Write(ref, image, remote.WithAuth(auth))
|
||||||
case *v1.Image:
|
|
||||||
if err := remote.Write(ref, *image, remote.WithAuth(auth)); err != nil {
|
|
||||||
return fmt.Errorf("failed to push image %s: %w", imageName, err)
|
|
||||||
}
|
|
||||||
case *v1.ImageIndex:
|
|
||||||
if err := remote.WriteIndex(ref, *image, remote.WithAuth(auth)); err != nil {
|
|
||||||
return fmt.Errorf("failed to push image index %s: %w", imageName, err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if err := remote.WriteIndex(ref, image.(v1.ImageIndex), remote.WithAuth(auth)); err != nil {
|
|
||||||
return fmt.Errorf("failed to push image index %s: %w", imageName, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveAsOCILayout(image any, path string) error {
|
func PushIndexToRegistry(image v1.ImageIndex, imageName string) error {
|
||||||
|
// Parse the index name
|
||||||
|
ref, err := name.ParseReference(imageName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to parse image name: %w", err)
|
||||||
|
}
|
||||||
|
// Get the authenticator from the default Docker keychain
|
||||||
|
auth, err := authn.DefaultKeychain.Resolve(ref.Context())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to get authenticator: %w", err)
|
||||||
|
}
|
||||||
|
// Push the index to the registry
|
||||||
|
return remote.WriteIndex(ref, image, remote.WithAuth(auth))
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveImageAsOCILayout(image v1.Image, path string) error {
|
||||||
// Save the image to the local filesystem
|
// Save the image to the local filesystem
|
||||||
err := os.MkdirAll(path, os.FileMode(0744))
|
err := os.MkdirAll(path, os.FileMode(0777))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create directory: %w", err)
|
return fmt.Errorf("failed to create directory: %w", err)
|
||||||
}
|
}
|
||||||
switch image := image.(type) {
|
index := empty.Index
|
||||||
case *v1.Image:
|
l, err := layout.Write(path, index)
|
||||||
index := empty.Index
|
if err != nil {
|
||||||
l, err := layout.Write(path, index)
|
return fmt.Errorf("failed to create index: %w", err)
|
||||||
if err != nil {
|
}
|
||||||
return fmt.Errorf("failed to create index: %w", err)
|
return l.AppendImage(image)
|
||||||
}
|
}
|
||||||
err = l.AppendImage(*image)
|
|
||||||
if err != nil {
|
func SaveIndexAsOCILayout(image v1.ImageIndex, path string) error {
|
||||||
return fmt.Errorf("failed to append image to index: %w", err)
|
// Save the index to the local filesystem
|
||||||
}
|
err := os.MkdirAll(path, os.FileMode(0777))
|
||||||
case *v1.ImageIndex:
|
if err != nil {
|
||||||
_, err := layout.Write(path, *image)
|
return fmt.Errorf("failed to create directory: %w", err)
|
||||||
if err != nil {
|
}
|
||||||
return fmt.Errorf("failed to create index: %w", err)
|
|
||||||
}
|
_, err = layout.Write(path, image)
|
||||||
default:
|
if err != nil {
|
||||||
_, err := layout.Write(path, image.(v1.ImageIndex))
|
return fmt.Errorf("failed to create index: %w", err)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create index: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func (m *TufMirror) GetTufTargetMirrors() ([]*MirrorImage, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to append role layer to image: %w", err)
|
return nil, fmt.Errorf("failed to append role layer to image: %w", err)
|
||||||
}
|
}
|
||||||
targetMirrors = append(targetMirrors, &MirrorImage{Image: &img, Tag: name})
|
targetMirrors = append(targetMirrors, &MirrorImage{Image: img, Tag: name})
|
||||||
}
|
}
|
||||||
return targetMirrors, nil
|
return targetMirrors, nil
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ func (m *TufMirror) GetDelegatedTargetMirrors() ([]*MirrorIndex, error) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
mirror = append(mirror, &MirrorIndex{Index: &index, Tag: role.Name})
|
mirror = append(mirror, &MirrorIndex{Index: index, Tag: role.Name})
|
||||||
}
|
}
|
||||||
return mirror, nil
|
return mirror, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func TestGetTufTargetsMirror(t *testing.T) {
|
|||||||
|
|
||||||
// check for image layer annotations
|
// check for image layer annotations
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
img := *target.Image
|
img := target.Image
|
||||||
mf, err := img.RawManifest()
|
mf, err := img.RawManifest()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ func TestGetDelegatedTargetMirrors(t *testing.T) {
|
|||||||
|
|
||||||
// check for index image annotations
|
// check for index image annotations
|
||||||
for _, mirror := range mirrors {
|
for _, mirror := range mirrors {
|
||||||
idx := *mirror.Index
|
idx := mirror.Index
|
||||||
mf, err := idx.RawManifest()
|
mf, err := idx.RawManifest()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
@@ -32,12 +32,12 @@ type DelegatedTargetMetadata struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MirrorImage struct {
|
type MirrorImage struct {
|
||||||
Image *v1.Image
|
Image v1.Image
|
||||||
Tag string
|
Tag string
|
||||||
}
|
}
|
||||||
|
|
||||||
type MirrorIndex struct {
|
type MirrorIndex struct {
|
||||||
Index *v1.ImageIndex
|
Index v1.ImageIndex
|
||||||
Tag string
|
Tag string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
151
pkg/oci/layout.go
Normal file
151
pkg/oci/layout.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
att "github.com/docker/attest/pkg/attestation"
|
||||||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// implementation of AttestationResolver that closes over attestations from an oci layout
|
||||||
|
type OCILayoutResolver struct {
|
||||||
|
*AttestationManifest
|
||||||
|
*ImageSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOCILayoutAttestationResolver(src *ImageSpec) (*OCILayoutResolver, error) {
|
||||||
|
r := &OCILayoutResolver{
|
||||||
|
ImageSpec: src,
|
||||||
|
}
|
||||||
|
_, err := r.fetchAttestationManifest()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OCILayoutResolver) fetchAttestationManifest() (*AttestationManifest, error) {
|
||||||
|
if r.AttestationManifest == nil {
|
||||||
|
m, err := attestationManifestFromOCILayout(r.Identifier, r.ImageSpec.Platform)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r.AttestationManifest = m
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.AttestationManifest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
||||||
|
attestationImage := r.AttestationManifest.Image
|
||||||
|
layers, err := attestationImage.Layers()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err)
|
||||||
|
}
|
||||||
|
var envs []*att.Envelope
|
||||||
|
manifest := r.AttestationManifest.Manifest
|
||||||
|
for i, l := range manifest.Layers {
|
||||||
|
if l.Annotations[InTotoPredicateType] != predicateType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
layer := layers[i]
|
||||||
|
mt, err := layer.MediaType()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||||
|
}
|
||||||
|
mts := string(mt)
|
||||||
|
if !strings.HasSuffix(mts, "+dsse") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var env = new(att.Envelope)
|
||||||
|
// parse layer blob as json
|
||||||
|
r, err := layer.Uncompressed()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
err = json.NewDecoder(r).Decode(env)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode envelope: %w", err)
|
||||||
|
}
|
||||||
|
envs = append(envs, env)
|
||||||
|
}
|
||||||
|
return envs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OCILayoutResolver) ImageName(ctx context.Context) (string, error) {
|
||||||
|
return r.Name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OCILayoutResolver) ImageDigest(ctx context.Context) (string, error) {
|
||||||
|
return r.Digest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OCILayoutResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
|
||||||
|
return r.ImageSpec.Platform, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*AttestationManifest, error) {
|
||||||
|
idx, err := layout.ImageIndexFromPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
idxm, err := idx.IndexManifest()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get digest: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idxDescriptor := idxm.Manifests[0]
|
||||||
|
name := idxDescriptor.Annotations["org.opencontainers.image.ref.name"]
|
||||||
|
idxDigest := idxDescriptor.Digest
|
||||||
|
|
||||||
|
mfs, err := idx.ImageIndex(idxDigest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
|
||||||
|
}
|
||||||
|
mfs2, err := mfs.IndexManifest()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
||||||
|
}
|
||||||
|
var imageDigest string
|
||||||
|
for _, mf := range mfs2.Manifests {
|
||||||
|
if mf.Platform.Equals(*platform) {
|
||||||
|
imageDigest = mf.Digest.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, mf := range mfs2.Manifests {
|
||||||
|
if mf.Annotations[att.DockerReferenceType] != AttestationManifestType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if mf.Annotations[att.DockerReferenceDigest] != imageDigest {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
attestationImage, err := mfs.Image(mf.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
|
||||||
|
}
|
||||||
|
manifest, err := attestationImage.Manifest()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
||||||
|
}
|
||||||
|
attest := &AttestationManifest{
|
||||||
|
Name: name,
|
||||||
|
Image: attestationImage,
|
||||||
|
Manifest: manifest,
|
||||||
|
Descriptor: &mf,
|
||||||
|
Digest: imageDigest,
|
||||||
|
Platform: platform,
|
||||||
|
}
|
||||||
|
return attest, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("attestation manifest not found")
|
||||||
|
}
|
||||||
280
pkg/oci/oci.go
280
pkg/oci/oci.go
@@ -10,9 +10,7 @@ import (
|
|||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
att "github.com/docker/attest/pkg/attestation"
|
att "github.com/docker/attest/pkg/attestation"
|
||||||
"github.com/google/go-containerregistry/pkg/authn"
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
"github.com/google/go-containerregistry/pkg/name"
|
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
|
||||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
|
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
@@ -20,9 +18,9 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// parsePlatform parses the provided platform string or attempts to obtain
|
// ParsePlatform parses the provided platform string or attempts to obtain
|
||||||
// the platform of the current host system
|
// the platform of the current host system
|
||||||
func parsePlatform(platformStr string) (*v1.Platform, error) {
|
func ParsePlatform(platformStr string) (*v1.Platform, error) {
|
||||||
if platformStr == "" {
|
if platformStr == "" {
|
||||||
cdp := platforms.Normalize(platforms.DefaultSpec())
|
cdp := platforms.Normalize(platforms.DefaultSpec())
|
||||||
if cdp.OS != "windows" {
|
if cdp.OS != "windows" {
|
||||||
@@ -38,255 +36,7 @@ func parsePlatform(platformStr string) (*v1.Platform, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func attestationManifestFromOCILayout(path string, platformStr string) (*AttestationManifest, error) {
|
func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
|
||||||
idx, err := layout.ImageIndexFromPath(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load image index: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
idxm, err := idx.IndexManifest()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get digest: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
idxDescriptor := idxm.Manifests[0]
|
|
||||||
name := idxDescriptor.Annotations["org.opencontainers.image.ref.name"]
|
|
||||||
idxDigest := idxDescriptor.Digest
|
|
||||||
|
|
||||||
mfs, err := idx.ImageIndex(idxDigest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
|
|
||||||
}
|
|
||||||
mfs2, err := mfs.IndexManifest()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
|
||||||
}
|
|
||||||
platform, err := parsePlatform(platformStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse platform: %w", err)
|
|
||||||
}
|
|
||||||
var imageDigest string
|
|
||||||
for _, mf := range mfs2.Manifests {
|
|
||||||
if mf.Platform.Equals(*platform) {
|
|
||||||
imageDigest = mf.Digest.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, mf := range mfs2.Manifests {
|
|
||||||
if mf.Annotations[att.DockerReferenceType] != AttestationManifestType {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if mf.Annotations[DockerReferenceDigest] != imageDigest {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
attestationImage, err := mfs.Image(mf.Digest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
|
|
||||||
}
|
|
||||||
manifest, err := attestationImage.Manifest()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
|
||||||
}
|
|
||||||
attest := &AttestationManifest{
|
|
||||||
Name: name,
|
|
||||||
Image: attestationImage,
|
|
||||||
Manifest: manifest,
|
|
||||||
Descriptor: &mf,
|
|
||||||
Digest: imageDigest,
|
|
||||||
Platform: platform,
|
|
||||||
}
|
|
||||||
return attest, nil
|
|
||||||
}
|
|
||||||
return nil, errors.New("attestation manifest not found")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// implementation of AttestationResolver that closes over attestations from an oci layout
|
|
||||||
type OCILayoutResolver struct {
|
|
||||||
Path string
|
|
||||||
Platform string
|
|
||||||
*AttestationManifest
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *OCILayoutResolver) ImagePlatformStr() string {
|
|
||||||
return r.Platform
|
|
||||||
}
|
|
||||||
func (r *OCILayoutResolver) fetchAttestationManifest() (*AttestationManifest, error) {
|
|
||||||
if r.AttestationManifest == nil {
|
|
||||||
m, err := attestationManifestFromOCILayout(r.Path, r.Platform)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get attestation manifest: %w", err)
|
|
||||||
}
|
|
||||||
r.AttestationManifest = m
|
|
||||||
}
|
|
||||||
return r.AttestationManifest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
|
||||||
if r.AttestationManifest == nil {
|
|
||||||
_, err := r.fetchAttestationManifest()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get attestation manifest: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
attestationImage := r.AttestationManifest.Image
|
|
||||||
layers, err := attestationImage.Layers()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err)
|
|
||||||
}
|
|
||||||
var envs []*att.Envelope
|
|
||||||
manifest := r.AttestationManifest.Manifest
|
|
||||||
for i, l := range manifest.Layers {
|
|
||||||
if l.Annotations[InTotoPredicateType] != predicateType {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
layer := layers[i]
|
|
||||||
mt, err := layer.MediaType()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
|
||||||
}
|
|
||||||
mts := string(mt)
|
|
||||||
if !strings.HasSuffix(mts, "+dsse") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var env = new(att.Envelope)
|
|
||||||
// parse layer blob as json
|
|
||||||
r, err := layer.Uncompressed()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
err = json.NewDecoder(r).Decode(env)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to decode envelope: %w", err)
|
|
||||||
}
|
|
||||||
envs = append(envs, env)
|
|
||||||
}
|
|
||||||
return envs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *OCILayoutResolver) ImageName(ctx context.Context) (string, error) {
|
|
||||||
if r.AttestationManifest == nil {
|
|
||||||
_, err := r.fetchAttestationManifest()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to get attestation manifest: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.Name, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *OCILayoutResolver) ImageDigest(ctx context.Context) (string, error) {
|
|
||||||
if r.AttestationManifest == nil {
|
|
||||||
_, err := r.fetchAttestationManifest()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to get attestation manifest: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r.Digest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type RegistryResolver struct {
|
|
||||||
Image string
|
|
||||||
Platform string
|
|
||||||
*AttestationManifest
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RegistryResolver) ImageName(ctx context.Context) (string, error) {
|
|
||||||
return r.Image, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RegistryResolver) ImagePlatformStr() string {
|
|
||||||
return r.Platform
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RegistryResolver) ImageDigest(ctx context.Context) (string, error) {
|
|
||||||
if r.AttestationManifest == nil {
|
|
||||||
attest, err := FetchAttestationManifest(ctx, r.Image, r.Platform)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to get attestation manifest: %w", err)
|
|
||||||
}
|
|
||||||
r.AttestationManifest = attest
|
|
||||||
}
|
|
||||||
return r.Digest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RegistryResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
|
||||||
if r.AttestationManifest == nil {
|
|
||||||
attest, err := FetchAttestationManifest(ctx, r.Image, r.Platform)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get attestation manifest: %w", err)
|
|
||||||
}
|
|
||||||
r.AttestationManifest = attest
|
|
||||||
}
|
|
||||||
return ExtractEnvelopes(r.AttestationManifest, predicateType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func FetchAttestationManifest(ctx context.Context, image, platformStr string) (*AttestationManifest, error) {
|
|
||||||
platform, err := parsePlatform(platformStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse platform %s: %w", platform, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// we want to get to the image index, so ignoring platform for now
|
|
||||||
options := withOptions(ctx, nil)
|
|
||||||
ref, err := name.ParseReference(image)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse reference: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
desc, err := remote.Index(ref, options...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to obtain index manifest: %w", err)
|
|
||||||
}
|
|
||||||
ix, err := desc.IndexManifest()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to obtain index manifest: %w", err)
|
|
||||||
}
|
|
||||||
digest, err := imageDigestForPlatform(ix, platform)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to obtain image for platform: %w", err)
|
|
||||||
}
|
|
||||||
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), digest))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
attestationDigest, err := attestationDigestForDigest(ix, digest, "attestation-manifest")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to obtain attestation for image: %w", err)
|
|
||||||
}
|
|
||||||
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), attestationDigest))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
|
||||||
}
|
|
||||||
remoteDescriptor, err := remote.Get(ref, options...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get attestation: %w", err)
|
|
||||||
}
|
|
||||||
manifest := new(v1.Manifest)
|
|
||||||
err = json.Unmarshal(remoteDescriptor.Manifest, manifest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal attestation: %w", err)
|
|
||||||
}
|
|
||||||
attestationImage, err := remoteDescriptor.Image()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get attestation image: %w", err)
|
|
||||||
}
|
|
||||||
attest := &AttestationManifest{
|
|
||||||
Name: image,
|
|
||||||
Image: attestationImage,
|
|
||||||
Manifest: manifest,
|
|
||||||
Descriptor: &remoteDescriptor.Descriptor,
|
|
||||||
Digest: digest,
|
|
||||||
Platform: platform,
|
|
||||||
}
|
|
||||||
return attest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func withOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
|
|
||||||
// prepare options
|
// prepare options
|
||||||
options := []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain), remote.WithTransport(HttpTransport()), remote.WithContext(ctx)}
|
options := []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain), remote.WithTransport(HttpTransport()), remote.WithContext(ctx)}
|
||||||
|
|
||||||
@@ -299,11 +49,9 @@ func withOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
|
|||||||
|
|
||||||
func ExtractEnvelopes(ia *AttestationManifest, predicateType string) ([]*att.Envelope, error) {
|
func ExtractEnvelopes(ia *AttestationManifest, predicateType string) ([]*att.Envelope, error) {
|
||||||
manifest := ia.Manifest
|
manifest := ia.Manifest
|
||||||
im := ia.Image
|
image := ia.Image
|
||||||
|
|
||||||
var envs []*att.Envelope
|
var envs []*att.Envelope
|
||||||
|
layers, err := image.Layers()
|
||||||
ls, err := im.Layers()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get layers: %w", err)
|
return nil, fmt.Errorf("failed to get layers: %w", err)
|
||||||
}
|
}
|
||||||
@@ -311,7 +59,7 @@ func ExtractEnvelopes(ia *AttestationManifest, predicateType string) ([]*att.Env
|
|||||||
if (strings.HasPrefix(string(l.MediaType), "application/vnd.in-toto.")) &&
|
if (strings.HasPrefix(string(l.MediaType), "application/vnd.in-toto.")) &&
|
||||||
strings.HasSuffix(string(l.MediaType), "+dsse") &&
|
strings.HasSuffix(string(l.MediaType), "+dsse") &&
|
||||||
l.Annotations[InTotoPredicateType] == predicateType {
|
l.Annotations[InTotoPredicateType] == predicateType {
|
||||||
reader, err := ls[i].Uncompressed()
|
reader, err := layers[i].Uncompressed()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||||
}
|
}
|
||||||
@@ -340,7 +88,7 @@ func imageDigestForPlatform(ix *v1.IndexManifest, platform *v1.Platform) (string
|
|||||||
func attestationDigestForDigest(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) {
|
func attestationDigestForDigest(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) {
|
||||||
for _, m := range ix.Manifests {
|
for _, m := range ix.Manifests {
|
||||||
if v, ok := m.Annotations[att.DockerReferenceType]; ok && v == attestType {
|
if v, ok := m.Annotations[att.DockerReferenceType]; ok && v == attestType {
|
||||||
if d, ok := m.Annotations[DockerReferenceDigest]; ok && d == imageDigest {
|
if d, ok := m.Annotations[att.DockerReferenceDigest]; ok && d == imageDigest {
|
||||||
return m.Digest.String(), nil
|
return m.Digest.String(), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,7 +96,7 @@ func attestationDigestForDigest(ix *v1.IndexManifest, imageDigest string, attest
|
|||||||
return "", errors.New(fmt.Sprintf("no attestation found for image %s", imageDigest))
|
return "", errors.New(fmt.Sprintf("no attestation found for image %s", imageDigest))
|
||||||
}
|
}
|
||||||
|
|
||||||
func RefToPURL(ref string, platform string) (string, bool, error) {
|
func RefToPURL(ref string, platform *v1.Platform) (string, bool, error) {
|
||||||
var isCanonical bool
|
var isCanonical bool
|
||||||
named, err := reference.ParseNormalizedNamed(ref)
|
named, err := reference.ParseNormalizedNamed(ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -380,14 +128,10 @@ func RefToPURL(ref string, platform string) (string, bool, error) {
|
|||||||
}
|
}
|
||||||
name = parts[len(parts)-1]
|
name = parts[len(parts)-1]
|
||||||
|
|
||||||
pf, err := parsePlatform(platform)
|
if platform != nil {
|
||||||
if err != nil {
|
|
||||||
return "", false, fmt.Errorf("failed to parse platform %q: %w", platform, err)
|
|
||||||
}
|
|
||||||
if pf != nil {
|
|
||||||
qualifiers = append(qualifiers, packageurl.Qualifier{
|
qualifiers = append(qualifiers, packageurl.Qualifier{
|
||||||
Key: "platform",
|
Key: "platform",
|
||||||
Value: pf.String(),
|
Value: platform.String(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,12 +139,12 @@ func RefToPURL(ref string, platform string) (string, bool, error) {
|
|||||||
return p.ToString(), isCanonical, nil
|
return p.ToString(), isCanonical, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SplitDigest(digest string) (*common.DigestSet, error) {
|
func SplitDigest(digest string) (common.DigestSet, error) {
|
||||||
parts := strings.SplitN(digest, ":", 2)
|
parts := strings.SplitN(digest, ":", 2)
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return nil, fmt.Errorf("invalid digest %q", digest)
|
return nil, fmt.Errorf("invalid digest %q", digest)
|
||||||
}
|
}
|
||||||
return &common.DigestSet{
|
return common.DigestSet{
|
||||||
parts[0]: parts[1],
|
parts[0]: parts[1],
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,45 +6,48 @@ import (
|
|||||||
|
|
||||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRefToPurl(t *testing.T) {
|
func TestRefToPurl(t *testing.T) {
|
||||||
purl, canonical, err := RefToPURL("alpine", "arm64/linux")
|
arm, err := ParsePlatform("arm64/linux")
|
||||||
|
require.NoError(t, err)
|
||||||
|
purl, canonical, err := RefToPURL("alpine", arm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "pkg:docker/alpine@latest?platform=arm64%2Flinux", purl)
|
assert.Equal(t, "pkg:docker/alpine@latest?platform=arm64%2Flinux", purl)
|
||||||
assert.False(t, canonical)
|
assert.False(t, canonical)
|
||||||
|
|
||||||
purl, canonical, err = RefToPURL("alpine:123", "arm64/linux")
|
purl, canonical, err = RefToPURL("alpine:123", arm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
||||||
assert.False(t, canonical)
|
assert.False(t, canonical)
|
||||||
|
|
||||||
purl, canonical, err = RefToPURL("google/alpine:123", "arm64/linux")
|
purl, canonical, err = RefToPURL("google/alpine:123", arm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "pkg:docker/google/alpine@123?platform=arm64%2Flinux", purl)
|
assert.Equal(t, "pkg:docker/google/alpine@123?platform=arm64%2Flinux", purl)
|
||||||
assert.False(t, canonical)
|
assert.False(t, canonical)
|
||||||
|
|
||||||
purl, canonical, err = RefToPURL("library/alpine:123", "arm64/linux")
|
purl, canonical, err = RefToPURL("library/alpine:123", arm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
||||||
assert.False(t, canonical)
|
assert.False(t, canonical)
|
||||||
|
|
||||||
purl, canonical, err = RefToPURL("docker.io/library/alpine:123", "arm64/linux")
|
purl, canonical, err = RefToPURL("docker.io/library/alpine:123", arm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
||||||
assert.False(t, canonical)
|
assert.False(t, canonical)
|
||||||
|
|
||||||
purl, canonical, err = RefToPURL("localhost:5001/library/alpine:123", "arm64/linux")
|
purl, canonical, err = RefToPURL("localhost:5001/library/alpine:123", arm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "pkg:docker/localhost%3A5001/library/alpine@123?platform=arm64%2Flinux", purl)
|
assert.Equal(t, "pkg:docker/localhost%3A5001/library/alpine@123?platform=arm64%2Flinux", purl)
|
||||||
assert.False(t, canonical)
|
assert.False(t, canonical)
|
||||||
|
|
||||||
purl, canonical, err = RefToPURL("localhost:5001/alpine:123", "arm64/linux")
|
purl, canonical, err = RefToPURL("localhost:5001/alpine:123", arm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine@123?platform=arm64%2Flinux", purl)
|
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine@123?platform=arm64%2Flinux", purl)
|
||||||
assert.False(t, canonical)
|
assert.False(t, canonical)
|
||||||
|
|
||||||
purl, canonical, err = RefToPURL("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b", "arm64/linux")
|
purl, canonical, err = RefToPURL("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b", arm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine?digest=sha256%3Ac5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b&platform=arm64%2Flinux", purl)
|
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine?digest=sha256%3Ac5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b&platform=arm64%2Flinux", purl)
|
||||||
assert.True(t, canonical)
|
assert.True(t, canonical)
|
||||||
@@ -70,15 +73,36 @@ func TestImageDigestForPlatform(t *testing.T) {
|
|||||||
mfs2, err := mfs.IndexManifest()
|
mfs2, err := mfs.IndexManifest()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
p, err := parsePlatform("linux/amd64")
|
p, err := ParsePlatform("linux/amd64")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
digest, err := imageDigestForPlatform(mfs2, p)
|
digest, err := imageDigestForPlatform(mfs2, p)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", digest)
|
assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", digest)
|
||||||
|
|
||||||
p, err = parsePlatform("linux/arm64")
|
p, err = ParsePlatform("linux/arm64")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
digest, err = imageDigestForPlatform(mfs2, p)
|
digest, err = imageDigestForPlatform(mfs2, p)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "sha256:7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e", digest)
|
assert.Equal(t, "sha256:7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e", digest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWithoutTag(t *testing.T) {
|
||||||
|
tc := []struct {
|
||||||
|
name string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{name: "image:tag", expected: "index.docker.io/library/image"},
|
||||||
|
{name: "image", expected: "index.docker.io/library/image"},
|
||||||
|
{name: "image:sha256-digest.att", expected: "index.docker.io/library/image"},
|
||||||
|
{name: "docker://image:tag", expected: "docker://index.docker.io/library/image"},
|
||||||
|
{name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image"},
|
||||||
|
{name: "docker://image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "docker://index.docker.io/library/image"},
|
||||||
|
{name: "docker://127.0.0.1:36555/repo:latest", expected: "docker://127.0.0.1:36555/repo"},
|
||||||
|
}
|
||||||
|
for _, c := range tc {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
notag, _ := WithoutTag(c.name)
|
||||||
|
assert.Equal(t, c.expected, notag)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
121
pkg/oci/referrers.go
Normal file
121
pkg/oci/referrers.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
att "github.com/docker/attest/pkg/attestation"
|
||||||
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReferrersResolver struct {
|
||||||
|
digest string
|
||||||
|
referrersRepo string
|
||||||
|
manifests []*AttestationManifest
|
||||||
|
*RegistryImageDetailsResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReferrersAttestationResolver(src *RegistryImageDetailsResolver, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) {
|
||||||
|
res := &ReferrersResolver{
|
||||||
|
RegistryImageDetailsResolver: src,
|
||||||
|
}
|
||||||
|
for _, opt := range options {
|
||||||
|
err := opt(res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithReferrersRepo(repo string) func(*ReferrersResolver) error {
|
||||||
|
return func(r *ReferrersResolver) error {
|
||||||
|
r.referrersRepo = repo
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error {
|
||||||
|
if r.manifests == nil {
|
||||||
|
subjectRef, err := name.ParseReference(r.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse reference: %w", err)
|
||||||
|
}
|
||||||
|
subjectDigest, err := r.ImageDigest(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get digest: %w", err)
|
||||||
|
}
|
||||||
|
var referrersSubjectRef name.Digest
|
||||||
|
if r.referrersRepo != "" {
|
||||||
|
referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", r.referrersRepo, subjectDigest))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create referrers reference: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
referrersSubjectRef = subjectRef.Context().Digest(subjectDigest)
|
||||||
|
}
|
||||||
|
referrersIndex, err := remote.Referrers(referrersSubjectRef)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get referrers: %w", err)
|
||||||
|
}
|
||||||
|
referrersIndexManifest, err := referrersIndex.IndexManifest()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get index manifest: %w", err)
|
||||||
|
}
|
||||||
|
if len(referrersIndexManifest.Manifests) == 0 {
|
||||||
|
return errors.New("no referrers found")
|
||||||
|
}
|
||||||
|
aManifests := make([]*AttestationManifest, 0)
|
||||||
|
for _, m := range referrersIndexManifest.Manifests {
|
||||||
|
|
||||||
|
remoteRef := referrersSubjectRef.Context().Digest(m.Digest.String())
|
||||||
|
attestationImage, err := remote.Image(remoteRef)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get referred image: %w", err)
|
||||||
|
}
|
||||||
|
manifest, err := attestationImage.Manifest()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get manifest: %w", err)
|
||||||
|
}
|
||||||
|
if manifest.Annotations[att.DockerReferenceType] != AttestationManifestType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if manifest.Annotations[att.DockerReferenceDigest] != subjectDigest {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
attest := &AttestationManifest{
|
||||||
|
Name: r.Identifier,
|
||||||
|
Image: attestationImage,
|
||||||
|
Manifest: manifest,
|
||||||
|
Descriptor: &m,
|
||||||
|
Digest: subjectDigest,
|
||||||
|
Platform: r.Platform,
|
||||||
|
}
|
||||||
|
aManifests = append(aManifests, attest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(aManifests) == 0 {
|
||||||
|
return errors.New("no attestation manifests found")
|
||||||
|
}
|
||||||
|
r.manifests = aManifests
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReferrersResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
||||||
|
err := r.resolveAttestations(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to resolve attestations: %w", err)
|
||||||
|
}
|
||||||
|
var envs []*att.Envelope
|
||||||
|
for _, attest := range r.manifests {
|
||||||
|
es, err := ExtractEnvelopes(attest, predicateType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to extract envelopes: %w", err)
|
||||||
|
}
|
||||||
|
envs = append(envs, es...)
|
||||||
|
}
|
||||||
|
return envs, nil
|
||||||
|
}
|
||||||
129
pkg/oci/registry.go
Normal file
129
pkg/oci/registry.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
att "github.com/docker/attest/pkg/attestation"
|
||||||
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RegistryResolver struct {
|
||||||
|
*RegistryImageDetailsResolver
|
||||||
|
*AttestationManifest
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegistryImageDetailsResolver struct {
|
||||||
|
*ImageSpec
|
||||||
|
digest string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistryImageDetailsResolver(src *ImageSpec) (*RegistryImageDetailsResolver, error) {
|
||||||
|
return &RegistryImageDetailsResolver{
|
||||||
|
ImageSpec: src,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistryAttestationResolver(src *RegistryImageDetailsResolver) (*RegistryResolver, error) {
|
||||||
|
return &RegistryResolver{
|
||||||
|
RegistryImageDetailsResolver: src,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegistryImageDetailsResolver) ImageName(ctx context.Context) (string, error) {
|
||||||
|
return r.Identifier, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegistryImageDetailsResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
|
||||||
|
return r.Platform, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegistryImageDetailsResolver) ImageDigest(ctx context.Context) (string, error) {
|
||||||
|
if r.digest == "" {
|
||||||
|
subjectRef, err := name.ParseReference(r.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to parse reference: %w", err)
|
||||||
|
}
|
||||||
|
options := WithOptions(ctx, r.Platform)
|
||||||
|
desc, err := remote.Image(subjectRef, options...)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get image manifest: %w", err)
|
||||||
|
}
|
||||||
|
subjectDigest, err := desc.Digest()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get image digest: %w", err)
|
||||||
|
}
|
||||||
|
r.digest = subjectDigest.String()
|
||||||
|
}
|
||||||
|
return r.digest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegistryResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
||||||
|
if r.AttestationManifest == nil {
|
||||||
|
attest, err := FetchAttestationManifest(ctx, r.Identifier, r.ImageSpec.Platform)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r.AttestationManifest = attest
|
||||||
|
}
|
||||||
|
return ExtractEnvelopes(r.AttestationManifest, predicateType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Platform) (*AttestationManifest, error) {
|
||||||
|
// we want to get to the image index, so ignoring platform for now
|
||||||
|
options := WithOptions(ctx, nil)
|
||||||
|
ref, err := name.ParseReference(image)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse reference: %w", err)
|
||||||
|
}
|
||||||
|
index, err := remote.Index(ref, options...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get index: %w", err)
|
||||||
|
}
|
||||||
|
indexManifest, err := index.IndexManifest()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get index manifest: %w", err)
|
||||||
|
}
|
||||||
|
digest, err := imageDigestForPlatform(indexManifest, platform)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to obtain image for platform: %w", err)
|
||||||
|
}
|
||||||
|
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), digest))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
attestationDigest, err := attestationDigestForDigest(indexManifest, digest, "attestation-manifest")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to obtain attestation for image: %w", err)
|
||||||
|
}
|
||||||
|
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), attestationDigest))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
||||||
|
}
|
||||||
|
remoteDescriptor, err := remote.Get(ref, options...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get attestation: %w", err)
|
||||||
|
}
|
||||||
|
manifest := new(v1.Manifest)
|
||||||
|
err = json.Unmarshal(remoteDescriptor.Manifest, manifest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal attestation: %w", err)
|
||||||
|
}
|
||||||
|
attestationImage, err := remoteDescriptor.Image()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get attestation image: %w", err)
|
||||||
|
}
|
||||||
|
attest := &AttestationManifest{
|
||||||
|
Name: image,
|
||||||
|
Image: attestationImage,
|
||||||
|
Manifest: manifest,
|
||||||
|
Descriptor: &remoteDescriptor.Descriptor,
|
||||||
|
Digest: digest,
|
||||||
|
Platform: platform,
|
||||||
|
}
|
||||||
|
return attest, nil
|
||||||
|
}
|
||||||
50
pkg/oci/registry_test.go
Normal file
50
pkg/oci/registry_test.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package oci_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/attest/internal/test"
|
||||||
|
"github.com/docker/attest/pkg/attest"
|
||||||
|
"github.com/docker/attest/pkg/attestation"
|
||||||
|
"github.com/docker/attest/pkg/mirror"
|
||||||
|
"github.com/docker/attest/pkg/oci"
|
||||||
|
"github.com/docker/attest/pkg/policy"
|
||||||
|
"github.com/google/go-containerregistry/pkg/registry"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRegistry(t *testing.T) {
|
||||||
|
ctx, signer := test.Setup(t)
|
||||||
|
server := httptest.NewServer(registry.New(registry.WithReferrersSupport(false)))
|
||||||
|
defer server.Close()
|
||||||
|
u, err := url.Parse(server.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
opts := &attestation.SigningOptions{
|
||||||
|
Replace: true,
|
||||||
|
SkipSubject: true,
|
||||||
|
}
|
||||||
|
attIdx, err := oci.SubjectIndexFromPath(oci.UnsignedTestImage)
|
||||||
|
require.NoError(t, err)
|
||||||
|
signedIndex, err := attest.Sign(ctx, attIdx.Index, signer, opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = mirror.PushIndexToRegistry(signedIndex, indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
spec, err := oci.ParseImageSpec(indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
resolver, err := policy.CreateImageDetailsResolver(spec)
|
||||||
|
require.NoError(t, err)
|
||||||
|
digest, err := resolver.ImageDigest(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, strings.Contains(digest, "sha256:"))
|
||||||
|
}
|
||||||
@@ -7,6 +7,10 @@ import (
|
|||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type AttestationManifests struct {
|
||||||
|
Manifests []*AttestationManifest
|
||||||
|
}
|
||||||
|
|
||||||
type AttestationManifest struct {
|
type AttestationManifest struct {
|
||||||
// attestation image details
|
// attestation image details
|
||||||
Image v1.Image
|
Image v1.Image
|
||||||
@@ -19,12 +23,16 @@ type AttestationManifest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AttestationResolver interface {
|
type AttestationResolver interface {
|
||||||
ImageName(ctx context.Context) (string, error)
|
ImageDetailsResolver
|
||||||
ImagePlatformStr() string
|
|
||||||
ImageDigest(ctx context.Context) (string, error)
|
|
||||||
Attestations(ctx context.Context, mediaType string) ([]*att.Envelope, error)
|
Attestations(ctx context.Context, mediaType string) ([]*att.Envelope, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ImageDetailsResolver interface {
|
||||||
|
ImageName(ctx context.Context) (string, error)
|
||||||
|
ImagePlatform(ctx context.Context) (*v1.Platform, error)
|
||||||
|
ImageDigest(ctx context.Context) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
type MockResolver struct {
|
type MockResolver struct {
|
||||||
Envs []*att.Envelope
|
Envs []*att.Envelope
|
||||||
}
|
}
|
||||||
@@ -41,6 +49,6 @@ func (r MockResolver) ImageDigest(ctx context.Context) (string, error) {
|
|||||||
return "sha256:test-digest", nil
|
return "sha256:test-digest", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r MockResolver) ImagePlatformStr() string {
|
func (r MockResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
|
||||||
return "linux/amd64"
|
return ParsePlatform("linux/amd64")
|
||||||
}
|
}
|
||||||
|
|||||||
144
pkg/oci/types.go
144
pkg/oci/types.go
@@ -2,7 +2,7 @@ package oci
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/go-containerregistry/pkg/authn"
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
"github.com/google/go-containerregistry/pkg/name"
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
@@ -12,18 +12,38 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DockerReferenceDigest = "vnd.docker.reference.digest"
|
AttestationManifestType = "attestation-manifest"
|
||||||
AttestationManifestType = "attestation-manifest"
|
InTotoPredicateType = "in-toto.io/predicate-type"
|
||||||
InTotoPredicateType = "in-toto.io/predicate-type"
|
OciReferenceTarget = "org.opencontainers.image.ref.name"
|
||||||
OciReferenceTarget = "org.opencontainers.image.ref.name"
|
LocalPrefix = "oci://"
|
||||||
|
RegistryPrefix = "docker://"
|
||||||
|
OCI SourceType = "OCI"
|
||||||
|
Docker SourceType = "Docker"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AttestationIndex struct {
|
type SourceType string
|
||||||
|
type SubjectIndex struct {
|
||||||
Index v1.ImageIndex
|
Index v1.ImageIndex
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
func AttestationIndexFromPath(path string) (*AttestationIndex, error) {
|
type AttestationOptions struct {
|
||||||
|
NoReferrers bool
|
||||||
|
Attach bool
|
||||||
|
ReferrersRepo string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageSpecOption func(*ImageSpec) error
|
||||||
|
|
||||||
|
type ImageSpec struct {
|
||||||
|
// OCI or Docker
|
||||||
|
Type SourceType
|
||||||
|
// without oci:// or docker:// (name or path)
|
||||||
|
Identifier string
|
||||||
|
Platform *v1.Platform
|
||||||
|
}
|
||||||
|
|
||||||
|
func SubjectIndexFromPath(path string) (*SubjectIndex, error) {
|
||||||
wrapperIdx, err := layout.ImageIndexFromPath(path)
|
wrapperIdx, err := layout.ImageIndexFromPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load image index: %w", err)
|
return nil, fmt.Errorf("failed to load image index: %w", err)
|
||||||
@@ -40,29 +60,129 @@ func AttestationIndexFromPath(path string) (*AttestationIndex, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
|
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
|
||||||
}
|
}
|
||||||
return &AttestationIndex{
|
return &SubjectIndex{
|
||||||
Index: idx,
|
Index: idx,
|
||||||
Name: imageName,
|
Name: imageName,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func AttestationIndexFromRemote(image string) (*AttestationIndex, error) {
|
func SubjectIndexFromRemote(image string) (*SubjectIndex, error) {
|
||||||
ref, err := name.ParseReference(image)
|
ref, err := name.ParseReference(image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to parse image name: %v", err)
|
return nil, fmt.Errorf("failed to parse image reference %s: %w", image, err)
|
||||||
}
|
}
|
||||||
// Get the authenticator from the default Docker keychain
|
// Get the authenticator from the default Docker keychain
|
||||||
auth, err := authn.DefaultKeychain.Resolve(ref.Context())
|
auth, err := authn.DefaultKeychain.Resolve(ref.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to get authenticator: %v", err)
|
return nil, fmt.Errorf("failed to resolve auth for image %s: %w", image, err)
|
||||||
}
|
}
|
||||||
// Pull the image from the registry
|
// Pull the image from the registry
|
||||||
idx, err := remote.Index(ref, remote.WithAuth(auth))
|
idx, err := remote.Index(ref, remote.WithAuth(auth))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to pull image %s: %w", image, err)
|
return nil, fmt.Errorf("failed to pull image %s: %w", image, err)
|
||||||
}
|
}
|
||||||
return &AttestationIndex{
|
return &SubjectIndex{
|
||||||
Index: idx,
|
Index: idx,
|
||||||
Name: image,
|
Name: image,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LoadSubjectIndex(input *ImageSpec) (*SubjectIndex, error) {
|
||||||
|
if input.Type == OCI {
|
||||||
|
return SubjectIndexFromPath(input.Identifier)
|
||||||
|
} else {
|
||||||
|
return SubjectIndexFromRemote(input.Identifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ImageSpec) ForPlatforms(platform string) ([]*ImageSpec, error) {
|
||||||
|
platforms := strings.Split(platform, ",")
|
||||||
|
var specs []*ImageSpec
|
||||||
|
for _, pStr := range platforms {
|
||||||
|
p, err := ParsePlatform(pStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
spec := &ImageSpec{
|
||||||
|
Type: i.Type,
|
||||||
|
Identifier: i.Identifier,
|
||||||
|
Platform: p,
|
||||||
|
}
|
||||||
|
specs = append(specs, spec)
|
||||||
|
}
|
||||||
|
return specs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseImageSpec(img string, options ...ImageSpecOption) (*ImageSpec, error) {
|
||||||
|
img = strings.TrimSpace(img)
|
||||||
|
if strings.Contains(img, ",") {
|
||||||
|
return nil, fmt.Errorf("only one image is supported")
|
||||||
|
}
|
||||||
|
withoutPrefix := strings.TrimPrefix(strings.TrimPrefix(img, LocalPrefix), RegistryPrefix)
|
||||||
|
src := &ImageSpec{
|
||||||
|
Identifier: withoutPrefix,
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(img, LocalPrefix) {
|
||||||
|
src.Type = OCI
|
||||||
|
} else {
|
||||||
|
src.Type = Docker
|
||||||
|
}
|
||||||
|
for _, option := range options {
|
||||||
|
err := option(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if src.Platform == nil {
|
||||||
|
platform, err := ParsePlatform("")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
src.Platform = platform
|
||||||
|
}
|
||||||
|
return src, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPlatform(platform string) ImageSpecOption {
|
||||||
|
return func(i *ImageSpec) error {
|
||||||
|
if strings.Contains(platform, ",") {
|
||||||
|
return fmt.Errorf("only one platform is supported")
|
||||||
|
}
|
||||||
|
p, err := ParsePlatform(platform)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.Platform = p
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseImageSpecs(img string) ([]*ImageSpec, error) {
|
||||||
|
outputs := strings.Split(img, ",")
|
||||||
|
var sources []*ImageSpec
|
||||||
|
for _, output := range outputs {
|
||||||
|
src, err := ParseImageSpec(output)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sources = append(sources, src)
|
||||||
|
}
|
||||||
|
return sources, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithoutTag(image string) (string, error) {
|
||||||
|
if strings.HasPrefix(image, LocalPrefix) {
|
||||||
|
return image, nil
|
||||||
|
}
|
||||||
|
prefix := ""
|
||||||
|
if strings.HasPrefix(image, RegistryPrefix) {
|
||||||
|
image = strings.TrimPrefix(image, RegistryPrefix)
|
||||||
|
prefix = RegistryPrefix
|
||||||
|
}
|
||||||
|
ref, err := name.ParseReference(image)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
repo := ref.Context().Name()
|
||||||
|
return prefix + repo, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,94 +10,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
|
"github.com/docker/attest/pkg/config"
|
||||||
"github.com/docker/attest/pkg/oci"
|
"github.com/docker/attest/pkg/oci"
|
||||||
"github.com/docker/attest/pkg/tuf"
|
|
||||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
|
||||||
|
|
||||||
goyaml "gopkg.in/yaml.v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
func resolveLocalPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*Policy, error) {
|
||||||
PolicyMappingFileName = "mapping.yaml"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Summary struct {
|
|
||||||
Subjects []intoto.Subject `json:"subjects"`
|
|
||||||
SLSALevels []string `json:"slsa_levels"`
|
|
||||||
Verifier string `json:"verifier"`
|
|
||||||
PolicyURI string `json:"policy_uri"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Violation struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Attestation *intoto.Statement `json:"attestation"`
|
|
||||||
Details map[string]any `json:"details"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Result struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Violations []Violation `json:"violations"`
|
|
||||||
Summary Summary `json:"summary"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyMappings struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
Kind string `json:"kind"`
|
|
||||||
Policies []PolicyMapping `json:"policies"`
|
|
||||||
Mirrors []PolicyMirror `json:"mirrors"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyMapping struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Origin PolicyOrigin `json:"origin"`
|
|
||||||
Files []PolicyMappingFile `json:"files"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyMappingFile struct {
|
|
||||||
Path string `json:"path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyMirror struct {
|
|
||||||
PolicyId string `yaml:"policy-id"`
|
|
||||||
Mirror MirrorSpec `json:"mirror"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MirrorSpec struct {
|
|
||||||
Domains []string `json:"domains"`
|
|
||||||
Prefix string `json:"prefix"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyOrigin struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Prefix string `json:"prefix"`
|
|
||||||
Domain string `json:"domain"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyOptions struct {
|
|
||||||
TufClient tuf.TUFClient
|
|
||||||
LocalTargetsDir string
|
|
||||||
LocalPolicyDir string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Policy struct {
|
|
||||||
InputFiles []*PolicyFile
|
|
||||||
Query string
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyInput struct {
|
|
||||||
Digest string `json:"digest"`
|
|
||||||
Purl string `json:"purl"`
|
|
||||||
IsCanonical bool `json:"isCanonical"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyFile struct {
|
|
||||||
Path string
|
|
||||||
Content []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveLocalPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, error) {
|
|
||||||
if opts.LocalPolicyDir == "" {
|
if opts.LocalPolicyDir == "" {
|
||||||
return nil, fmt.Errorf("local policy dir not set")
|
return nil, fmt.Errorf("local policy dir not set")
|
||||||
}
|
}
|
||||||
@@ -116,28 +33,12 @@ func resolveLocalPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, e
|
|||||||
}
|
}
|
||||||
policy := &Policy{
|
policy := &Policy{
|
||||||
InputFiles: files,
|
InputFiles: files,
|
||||||
|
Mapping: mapping,
|
||||||
}
|
}
|
||||||
return policy, nil
|
return policy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadLocalMappings(opts *PolicyOptions) (*PolicyMappings, error) {
|
func resolveTufPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*Policy, error) {
|
||||||
if opts.LocalPolicyDir == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
mappings := &PolicyMappings{}
|
|
||||||
path := path.Join(opts.LocalPolicyDir, PolicyMappingFileName)
|
|
||||||
mappingFile, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read policy mapping file %s: %w", path, err)
|
|
||||||
}
|
|
||||||
err = goyaml.Unmarshal(mappingFile, mappings)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", path, err)
|
|
||||||
}
|
|
||||||
return mappings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveTufPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, error) {
|
|
||||||
files := make([]*PolicyFile, 0, len(mapping.Files))
|
files := make([]*PolicyFile, 0, len(mapping.Files))
|
||||||
for _, f := range mapping.Files {
|
for _, f := range mapping.Files {
|
||||||
filename := f.Path
|
filename := f.Path
|
||||||
@@ -152,51 +53,74 @@ func resolveTufPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, err
|
|||||||
}
|
}
|
||||||
policy := &Policy{
|
policy := &Policy{
|
||||||
InputFiles: files,
|
InputFiles: files,
|
||||||
|
Mapping: mapping,
|
||||||
}
|
}
|
||||||
return policy, nil
|
return policy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMappings, error) {
|
func findPolicyMatch(named reference.Named, mappings *config.PolicyMappings) (*config.PolicyMapping, *config.PolicyMirror) {
|
||||||
filename := PolicyMappingFileName
|
|
||||||
_, fileContents, err := tufClient.DownloadTarget(filename, filepath.Join(localTargetsDir, filename))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to download policy file %s: %w", filename, err)
|
|
||||||
}
|
|
||||||
mappings := &PolicyMappings{}
|
|
||||||
|
|
||||||
err = goyaml.Unmarshal(fileContents, mappings)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", filename, err)
|
|
||||||
}
|
|
||||||
return mappings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findPolicyMatch(named reference.Named, mappings *PolicyMappings) (*PolicyMapping, *PolicyMirror) {
|
|
||||||
if mappings != nil {
|
if mappings != nil {
|
||||||
for _, mapping := range mappings.Policies {
|
for _, mapping := range mappings.Policies {
|
||||||
if mapping.Origin.Domain == reference.Domain(named) &&
|
if mapping.Origin.Domain == reference.Domain(named) &&
|
||||||
strings.HasPrefix(reference.Path(named), mapping.Origin.Prefix) {
|
strings.HasPrefix(reference.Path(named), mapping.Origin.Prefix) {
|
||||||
return &mapping, nil
|
return mapping, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// now search mirrors
|
// now search mirrors
|
||||||
for _, mirror := range mappings.Mirrors {
|
for _, mirror := range mappings.Mirrors {
|
||||||
if slices.Contains(mirror.Mirror.Domains, reference.Domain(named)) &&
|
if (slices.Contains(mirror.Mirror.Domains, reference.Domain(named)) ||
|
||||||
|
slices.Contains(mirror.Mirror.Domains, "*")) &&
|
||||||
strings.HasPrefix(reference.Path(named), mirror.Mirror.Prefix) {
|
strings.HasPrefix(reference.Path(named), mirror.Mirror.Prefix) {
|
||||||
for _, mapping := range mappings.Policies {
|
for _, mapping := range mappings.Policies {
|
||||||
if mapping.Id == mirror.PolicyId {
|
if mapping.Id == mirror.PolicyId {
|
||||||
return &mapping, nil
|
return mapping, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, &mirror
|
return nil, mirror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *PolicyOptions) (*Policy, error) {
|
func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
|
||||||
imageName, err := resolver.ImageName(ctx)
|
if opts.PolicyId != "" {
|
||||||
|
localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
|
||||||
|
}
|
||||||
|
if localMappings != nil {
|
||||||
|
for _, mapping := range localMappings.Policies {
|
||||||
|
if mapping.Id == opts.PolicyId {
|
||||||
|
return resolveLocalPolicy(opts, mapping)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// must check tuf
|
||||||
|
tufMappings, err := config.LoadTufMappings(opts.TufClient, opts.LocalTargetsDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err)
|
||||||
|
}
|
||||||
|
for _, mapping := range tufMappings.Policies {
|
||||||
|
if mapping.Id == opts.PolicyId {
|
||||||
|
return resolveTufPolicy(opts, mapping)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("policy with id %s not found", opts.PolicyId)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver, opts *PolicyOptions) (*Policy, error) {
|
||||||
|
p, err := resolvePolicyById(opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to resolve policy by id: %w", err)
|
||||||
|
}
|
||||||
|
if p != nil {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
imageName, err := detailsResolver.ImageName(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get image name: %w", err)
|
return nil, fmt.Errorf("failed to get image name: %w", err)
|
||||||
}
|
}
|
||||||
@@ -204,7 +128,7 @@ func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse image name: %w", err)
|
return nil, fmt.Errorf("failed to parse image name: %w", err)
|
||||||
}
|
}
|
||||||
localMappings, err := LoadLocalMappings(opts)
|
localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
|
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
|
||||||
}
|
}
|
||||||
@@ -213,16 +137,16 @@ func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *
|
|||||||
return resolveLocalPolicy(opts, mapping)
|
return resolveLocalPolicy(opts, mapping)
|
||||||
}
|
}
|
||||||
// must check tuf
|
// must check tuf
|
||||||
tufMappings, err := loadTufMappings(opts.TufClient, opts.LocalTargetsDir)
|
tufMappings, err := config.LoadTufMappings(opts.TufClient, opts.LocalTargetsDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load tuf policy mappings: %w", err)
|
return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// it's a mirror of a tuf policy
|
// it's a mirror of a tuf policy
|
||||||
if mirror != nil {
|
if mirror != nil {
|
||||||
for _, mapping := range tufMappings.Policies {
|
for _, mapping := range tufMappings.Policies {
|
||||||
if mapping.Id == mirror.PolicyId {
|
if mapping.Id == mirror.PolicyId {
|
||||||
return resolveTufPolicy(opts, &mapping)
|
return resolveTufPolicy(opts, mapping)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,3 +158,32 @@ func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *
|
|||||||
}
|
}
|
||||||
return resolveTufPolicy(opts, mapping)
|
return resolveTufPolicy(opts, mapping)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsResolver, error) {
|
||||||
|
switch imageSource.Type {
|
||||||
|
case oci.OCI:
|
||||||
|
return oci.NewOCILayoutAttestationResolver(imageSource)
|
||||||
|
case oci.Docker:
|
||||||
|
return oci.NewRegistryImageDetailsResolver(imageSource)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unsupported image source type: %s", imageSource.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *config.PolicyMapping) (oci.AttestationResolver, error) {
|
||||||
|
switch resolver := resolver.(type) {
|
||||||
|
case *oci.RegistryImageDetailsResolver:
|
||||||
|
if mapping.Attestations != nil && mapping.Attestations.Style == config.AttestationStyleAttached {
|
||||||
|
return oci.NewRegistryAttestationResolver(resolver)
|
||||||
|
} else {
|
||||||
|
if mapping.Attestations != nil && mapping.Attestations.Repo != "" {
|
||||||
|
return oci.NewReferrersAttestationResolver(resolver, oci.WithReferrersRepo(mapping.Attestations.Repo))
|
||||||
|
} else {
|
||||||
|
return oci.NewReferrersAttestationResolver(resolver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *oci.OCILayoutResolver:
|
||||||
|
return resolver, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/docker/attest/internal/test"
|
"github.com/docker/attest/internal/test"
|
||||||
"github.com/docker/attest/pkg/attestation"
|
"github.com/docker/attest/pkg/attestation"
|
||||||
|
"github.com/docker/attest/pkg/config"
|
||||||
"github.com/docker/attest/pkg/oci"
|
"github.com/docker/attest/pkg/oci"
|
||||||
"github.com/docker/attest/pkg/policy"
|
"github.com/docker/attest/pkg/policy"
|
||||||
"github.com/docker/attest/pkg/tuf"
|
"github.com/docker/attest/pkg/tuf"
|
||||||
@@ -31,7 +32,7 @@ func loadAttestation(t *testing.T, path string) *attestation.Envelope {
|
|||||||
|
|
||||||
func TestRegoEvaluator_Evaluate(t *testing.T) {
|
func TestRegoEvaluator_Evaluate(t *testing.T) {
|
||||||
ctx, _ := test.Setup(t)
|
ctx, _ := test.Setup(t)
|
||||||
|
errorStr := "failed to resolve policy by id: policy with id non-existent-policy-id not found"
|
||||||
TestDataPath := filepath.Join("..", "..", "test", "testdata")
|
TestDataPath := filepath.Join("..", "..", "test", "testdata")
|
||||||
ExampleAttestation := filepath.Join(TestDataPath, "example_attestation.json")
|
ExampleAttestation := filepath.Join(TestDataPath, "example_attestation.json")
|
||||||
|
|
||||||
@@ -47,8 +48,12 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
|||||||
isCanonical bool
|
isCanonical bool
|
||||||
resolver oci.AttestationResolver
|
resolver oci.AttestationResolver
|
||||||
policy *policy.PolicyOptions
|
policy *policy.PolicyOptions
|
||||||
|
policyId string
|
||||||
|
errorStr string
|
||||||
}{
|
}{
|
||||||
{repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver},
|
{repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver},
|
||||||
|
{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-deny", expectSuccess: false, isCanonical: false, resolver: defaultResolver},
|
||||||
{repo: "testdata/mock-tuf-verify-sig", expectSuccess: true, 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-wrong-key", expectSuccess: false, isCanonical: false, resolver: defaultResolver},
|
||||||
@@ -69,11 +74,23 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
|||||||
tc.policy = &policy.PolicyOptions{
|
tc.policy = &policy.PolicyOptions{
|
||||||
TufClient: tufClient,
|
TufClient: tufClient,
|
||||||
LocalTargetsDir: test.CreateTempDir(t, "", "tuf-targets"),
|
LocalTargetsDir: test.CreateTempDir(t, "", "tuf-targets"),
|
||||||
|
PolicyId: tc.policyId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
imageName, err := tc.resolver.ImageName(ctx)
|
||||||
policy, err := policy.ResolvePolicy(ctx, tc.resolver, tc.policy)
|
require.NoError(t, err)
|
||||||
assert.NoErrorf(t, err, "failed to resolve policy")
|
platform, err := tc.resolver.ImagePlatform(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
src, err := oci.ParseImageSpec(imageName, oci.WithPlatform(platform.String()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
resolver, err := policy.CreateImageDetailsResolver(src)
|
||||||
|
policy, err := policy.ResolvePolicy(ctx, resolver, tc.policy)
|
||||||
|
if tc.errorStr != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), tc.errorStr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoErrorf(t, err, "failed to resolve policy")
|
||||||
result, err := re.Evaluate(ctx, tc.resolver, policy, input)
|
result, err := re.Evaluate(ctx, tc.resolver, policy, input)
|
||||||
require.NoErrorf(t, err, "Evaluate failed")
|
require.NoErrorf(t, err, "Evaluate failed")
|
||||||
|
|
||||||
@@ -88,10 +105,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadingMappings(t *testing.T) {
|
func TestLoadingMappings(t *testing.T) {
|
||||||
opts := &policy.PolicyOptions{
|
policyMappings, err := config.LoadLocalMappings(filepath.Join("testdata", "mock-tuf-allow"))
|
||||||
LocalPolicyDir: filepath.Join("testdata", "mock-tuf-allow"),
|
|
||||||
}
|
|
||||||
policyMappings, err := policy.LoadLocalMappings(opts)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, len(policyMappings.Mirrors), 1)
|
assert.Equal(t, len(policyMappings.Mirrors), 1)
|
||||||
for _, mirror := range policyMappings.Mirrors {
|
for _, mirror := range policyMappings.Mirrors {
|
||||||
|
|||||||
@@ -133,21 +133,42 @@ func jsonGenerator[T any]() func(t *ast.Term, ec *rego.EvalContext) (any, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var dynamicObj = types.NewObject(nil, types.NewDynamicProperty(types.S, types.A))
|
var dynamicObj = types.NewObject(nil, types.NewDynamicProperty(types.A, types.A))
|
||||||
var arrayObj = types.NewArray(nil, dynamicObj)
|
|
||||||
var setObj = types.NewSet(dynamicObj)
|
|
||||||
|
|
||||||
var verifyDecl = &ast.Builtin{
|
var verifyDecl = &ast.Builtin{
|
||||||
Name: "attestations.verify_envelope",
|
Name: "attest.verify",
|
||||||
Decl: types.NewFunction(types.Args(dynamicObj, arrayObj), dynamicObj),
|
Decl: types.NewFunction(types.Args(dynamicObj, dynamicObj), dynamicObj),
|
||||||
Nondeterministic: true,
|
Nondeterministic: true,
|
||||||
}
|
}
|
||||||
var attestDecl = &ast.Builtin{
|
var attestDecl = &ast.Builtin{
|
||||||
Name: "attestations.attestation",
|
Name: "attest.fetch",
|
||||||
Decl: types.NewFunction(types.Args(types.S), setObj),
|
Decl: types.NewFunction(types.Args(types.S), dynamicObj),
|
||||||
Nondeterministic: true,
|
Nondeterministic: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func wrapFunctionResult(value *ast.Term, err error) (*ast.Term, error) {
|
||||||
|
var terms [][2]*ast.Term
|
||||||
|
if err != nil {
|
||||||
|
terms = append(terms, [2]*ast.Term{ast.StringTerm("error"), ast.StringTerm(err.Error())})
|
||||||
|
}
|
||||||
|
if value != nil {
|
||||||
|
terms = append(terms, [2]*ast.Term{ast.StringTerm("value"), value})
|
||||||
|
}
|
||||||
|
return ast.ObjectTerm(terms...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleErrors1(f func(rCtx rego.BuiltinContext, a *ast.Term) (*ast.Term, error)) rego.Builtin1 {
|
||||||
|
return func(rCtx rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
|
||||||
|
return wrapFunctionResult(f(rCtx, a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleErrors2(f func(rCtx rego.BuiltinContext, a, b *ast.Term) (*ast.Term, error)) rego.Builtin2 {
|
||||||
|
return func(rCtx rego.BuiltinContext, a, b *ast.Term) (*ast.Term, error) {
|
||||||
|
return wrapFunctionResult(f(rCtx, a, b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin {
|
func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin {
|
||||||
return []*tester.Builtin{
|
return []*tester.Builtin{
|
||||||
{
|
{
|
||||||
@@ -159,7 +180,7 @@ func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin {
|
|||||||
Memoize: true,
|
Memoize: true,
|
||||||
Nondeterministic: verifyDecl.Nondeterministic,
|
Nondeterministic: verifyDecl.Nondeterministic,
|
||||||
},
|
},
|
||||||
verifyIntotoEnvelope),
|
handleErrors2(verifyIntotoEnvelope)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Decl: attestDecl,
|
Decl: attestDecl,
|
||||||
@@ -170,12 +191,12 @@ func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin {
|
|||||||
Memoize: true,
|
Memoize: true,
|
||||||
Nondeterministic: attestDecl.Nondeterministic,
|
Nondeterministic: attestDecl.Nondeterministic,
|
||||||
},
|
},
|
||||||
fetchIntotoAttestations(resolver)),
|
handleErrors1(fetchIntotoAttestations(resolver))),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchIntotoAttestations(resolver oci.AttestationResolver) func(rego.BuiltinContext, *ast.Term) (*ast.Term, error) {
|
func fetchIntotoAttestations(resolver oci.AttestationResolver) rego.Builtin1 {
|
||||||
return func(rCtx rego.BuiltinContext, predicateTypeTerm *ast.Term) (*ast.Term, error) {
|
return func(rCtx rego.BuiltinContext, predicateTypeTerm *ast.Term) (*ast.Term, error) {
|
||||||
predicateTypeStr, ok := predicateTypeTerm.Value.(ast.String)
|
predicateTypeStr, ok := predicateTypeTerm.Value.(ast.String)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -205,22 +226,19 @@ func fetchIntotoAttestations(resolver oci.AttestationResolver) func(rego.Builtin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyIntotoEnvelope(rCtx rego.BuiltinContext, envTerm, keysTerm *ast.Term) (*ast.Term, error) {
|
func verifyIntotoEnvelope(rCtx rego.BuiltinContext, envTerm, optsTerm *ast.Term) (*ast.Term, error) {
|
||||||
env := new(att.Envelope)
|
env := new(att.Envelope)
|
||||||
var keys att.Keys
|
opts := new(att.VerifyOptions)
|
||||||
err := ast.As(envTerm.Value, env)
|
err := ast.As(envTerm.Value, env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to cast envelope: %w", err)
|
return nil, fmt.Errorf("failed to cast envelope: %w", err)
|
||||||
}
|
}
|
||||||
err = ast.As(keysTerm.Value, &keys)
|
err = ast.As(optsTerm.Value, &opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to cast keys: %w", err)
|
return nil, fmt.Errorf("failed to cast verifier options: %w", err)
|
||||||
}
|
}
|
||||||
keysmap := make(map[string]att.KeyMetadata, len(keys))
|
|
||||||
for _, key := range keys {
|
payload, err := att.VerifyDSSE(rCtx.Context, env, opts)
|
||||||
keysmap[key.ID] = key
|
|
||||||
}
|
|
||||||
payload, err := att.VerifyDSSE(rCtx.Context, env, keysmap)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -242,7 +260,6 @@ func verifyIntotoEnvelope(rCtx rego.BuiltinContext, envTerm, keysTerm *ast.Term)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return ast.NewTerm(value), nil
|
return ast.NewTerm(value), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ policies:
|
|||||||
prefix: library/
|
prefix: library/
|
||||||
id: docker-official-images
|
id: docker-official-images
|
||||||
description: Docker Official Images
|
description: Docker Official Images
|
||||||
|
attestations:
|
||||||
|
repo: "localhost:5001/library-refs"
|
||||||
files:
|
files:
|
||||||
- path: doi/policy.rego
|
- path: doi/policy.rego
|
||||||
mirrors:
|
mirrors:
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ package attest
|
|||||||
import rego.v1
|
import rego.v1
|
||||||
|
|
||||||
keys := [{
|
keys := [{
|
||||||
"id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4",
|
"id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4",
|
||||||
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgH23D1i2+ZIOtVjmfB7iFvX8AhVN\n9CPJ4ie9axw+WRHozGnRy99U2dRge3zueBBg2MweF0zrToXGig2v3YOrdw==\n-----END PUBLIC KEY-----",
|
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgH23D1i2+ZIOtVjmfB7iFvX8AhVN\n9CPJ4ie9axw+WRHozGnRy99U2dRge3zueBBg2MweF0zrToXGig2v3YOrdw==\n-----END PUBLIC KEY-----",
|
||||||
"from": "2023-12-15T14:00:00Z",
|
"from": "2023-12-15T14:00:00Z",
|
||||||
"to": null
|
"to": null,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
opts := {"keys": keys}
|
||||||
|
|
||||||
success if {
|
success if {
|
||||||
some env in attestations.attestation("foo")
|
some env in attest.fetch("foo")
|
||||||
statement := attestations.verify_envelope(env, keys)
|
statement := attest.verify(env, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
result := {
|
result := {"success": success}
|
||||||
"success": success
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,19 +3,28 @@ package attest
|
|||||||
import rego.v1
|
import rego.v1
|
||||||
|
|
||||||
keys := [{
|
keys := [{
|
||||||
"id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4",
|
"id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4",
|
||||||
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHyZpSgzvqFqNv7f3x7865OS38rAb\nQMcff55zM2UH/KR3Pr84a8QsGDNgaNGzJQJWjtMSgfV8WnNoffNK+svFNg==\n-----END PUBLIC KEY-----",
|
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHyZpSgzvqFqNv7f3x7865OS38rAb\nQMcff55zM2UH/KR3Pr84a8QsGDNgaNGzJQJWjtMSgfV8WnNoffNK+svFNg==\n-----END PUBLIC KEY-----",
|
||||||
"from": "2023-12-15T14:00:00Z",
|
"from": "2023-12-15T14:00:00Z",
|
||||||
"to": null,
|
"to": null,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
default success := false
|
default success := false
|
||||||
|
|
||||||
success if {
|
provs(pred) := p if {
|
||||||
some env in attestations.attestation("foo")
|
res := attest.fetch(pred)
|
||||||
statement := attestations.verify_envelope(env, keys)
|
not res.error
|
||||||
|
p := res.value
|
||||||
}
|
}
|
||||||
|
|
||||||
result := {
|
atts := union({provs("foo")})
|
||||||
"success": success
|
|
||||||
|
opts := {"keys": keys}
|
||||||
|
|
||||||
|
success if {
|
||||||
|
some env in atts
|
||||||
|
res := attest.verify(env, opts)
|
||||||
|
not res.error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result := {"success": success}
|
||||||
|
|||||||
53
pkg/policy/types.go
Normal file
53
pkg/policy/types.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/attest/pkg/config"
|
||||||
|
"github.com/docker/attest/pkg/tuf"
|
||||||
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Summary struct {
|
||||||
|
Subjects []intoto.Subject `json:"subjects"`
|
||||||
|
SLSALevels []string `json:"slsa_levels"`
|
||||||
|
Verifier string `json:"verifier"`
|
||||||
|
PolicyURI string `json:"policy_uri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Violation struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Attestation *intoto.Statement `json:"attestation"`
|
||||||
|
Details map[string]any `json:"details"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Violations []Violation `json:"violations"`
|
||||||
|
Summary Summary `json:"summary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyOptions struct {
|
||||||
|
TufClient tuf.TUFClient
|
||||||
|
LocalTargetsDir string
|
||||||
|
LocalPolicyDir string
|
||||||
|
PolicyId string
|
||||||
|
ReferrersRepo string
|
||||||
|
AttestationStyle config.AttestationStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
type Policy struct {
|
||||||
|
InputFiles []*PolicyFile
|
||||||
|
Query string
|
||||||
|
Mapping *config.PolicyMapping
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyInput struct {
|
||||||
|
Digest string `json:"digest"`
|
||||||
|
Purl string `json:"purl"`
|
||||||
|
IsCanonical bool `json:"isCanonical"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyFile struct {
|
||||||
|
Path string
|
||||||
|
Content []byte
|
||||||
|
}
|
||||||
16
test/testdata/local-policy-fail/doi/policy.rego
vendored
16
test/testdata/local-policy-fail/doi/policy.rego
vendored
@@ -12,14 +12,24 @@ keys := [{
|
|||||||
"signing-format": "dssev1",
|
"signing-format": "dssev1",
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
provs(pred) := p if {
|
||||||
|
res := attest.fetch(pred)
|
||||||
|
not res.error
|
||||||
|
p := res.value
|
||||||
|
}
|
||||||
|
|
||||||
atts := union({
|
atts := union({
|
||||||
attestations.attestation("https://slsa.dev/provenance/v0.2"),
|
provs("https://slsa.dev/provenance/v0.2"),
|
||||||
attestations.attestation("https://spdx.dev/Document"),
|
provs("https://spdx.dev/Document"),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
opts := {"keys": keys}
|
||||||
|
|
||||||
statements contains s if {
|
statements contains s if {
|
||||||
some att in atts
|
some att in atts
|
||||||
s := attestations.verify_envelope(att, keys)
|
res := attest.verify(att, opts)
|
||||||
|
not res.error
|
||||||
|
s := res.value
|
||||||
}
|
}
|
||||||
|
|
||||||
subjects contains subject if {
|
subjects contains subject if {
|
||||||
|
|||||||
49
test/testdata/local-policy-no-tl/doi/policy.rego
vendored
Normal file
49
test/testdata/local-policy-no-tl/doi/policy.rego
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package attest
|
||||||
|
|
||||||
|
import rego.v1
|
||||||
|
|
||||||
|
keys := [{
|
||||||
|
"id": "6b241993defaba26558c64f94a94303ce860e7ad9163d801495c91cf57197c75",
|
||||||
|
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZmicqYSY38DprGr42jU0V3ND0ROj\nzSRH1+yjsxhh0bi52Hh/DuOhrSq2KJ5a09lW3ybnDjljowbkof0Y1i9Oow==\n-----END PUBLIC KEY-----",
|
||||||
|
"from": "2023-12-15T14:00:00Z",
|
||||||
|
"to": null,
|
||||||
|
# this key is still active
|
||||||
|
"status": "active",
|
||||||
|
"signing-format": "dssev1",
|
||||||
|
}]
|
||||||
|
|
||||||
|
provs(pred) := p if {
|
||||||
|
res := attest.fetch(pred)
|
||||||
|
not res.error
|
||||||
|
p := res.value
|
||||||
|
}
|
||||||
|
|
||||||
|
atts := union({
|
||||||
|
provs("https://slsa.dev/provenance/v0.2"),
|
||||||
|
provs("https://spdx.dev/Document"),
|
||||||
|
})
|
||||||
|
|
||||||
|
opts := {"keys": keys, "skip_tl": true}
|
||||||
|
|
||||||
|
statements contains s if {
|
||||||
|
some att in atts
|
||||||
|
res := attest.verify(att, opts)
|
||||||
|
not res.error
|
||||||
|
s := res.value
|
||||||
|
}
|
||||||
|
|
||||||
|
subjects contains subject if {
|
||||||
|
some statement in statements
|
||||||
|
some subject in statement.subject
|
||||||
|
}
|
||||||
|
|
||||||
|
result := {
|
||||||
|
"success": true,
|
||||||
|
"violations": set(),
|
||||||
|
"summary": {
|
||||||
|
"subjects": subjects,
|
||||||
|
"slsa_levels": ["SLSA_BUILD_LEVEL_3"],
|
||||||
|
"verifier": "docker-official-images",
|
||||||
|
"policy_uri": "https://docker.com/official/policy/v0.1",
|
||||||
|
},
|
||||||
|
}
|
||||||
11
test/testdata/local-policy-no-tl/mapping.yaml
vendored
Normal file
11
test/testdata/local-policy-no-tl/mapping.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# map repos to policies
|
||||||
|
version: v1
|
||||||
|
kind: policy-mapping
|
||||||
|
policies:
|
||||||
|
- origin:
|
||||||
|
domain: docker.io
|
||||||
|
prefix: library/
|
||||||
|
id: test-images
|
||||||
|
description: Local test images
|
||||||
|
files:
|
||||||
|
- path: doi/policy.rego
|
||||||
16
test/testdata/local-policy-pass/doi/policy.rego
vendored
16
test/testdata/local-policy-pass/doi/policy.rego
vendored
@@ -12,14 +12,24 @@ keys := [{
|
|||||||
"signing-format": "dssev1",
|
"signing-format": "dssev1",
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
provs(pred) := p if {
|
||||||
|
res := attest.fetch(pred)
|
||||||
|
not res.error
|
||||||
|
p := res.value
|
||||||
|
}
|
||||||
|
|
||||||
atts := union({
|
atts := union({
|
||||||
attestations.attestation("https://slsa.dev/provenance/v0.2"),
|
provs("https://slsa.dev/provenance/v0.2"),
|
||||||
attestations.attestation("https://spdx.dev/Document"),
|
provs("https://spdx.dev/Document"),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
opts := {"keys": keys}
|
||||||
|
|
||||||
statements contains s if {
|
statements contains s if {
|
||||||
some att in atts
|
some att in atts
|
||||||
s := attestations.verify_envelope(att, keys)
|
res := attest.verify(att, opts)
|
||||||
|
not res.error
|
||||||
|
s := res.value
|
||||||
}
|
}
|
||||||
|
|
||||||
subjects contains subject if {
|
subjects contains subject if {
|
||||||
|
|||||||
5
test/testdata/local-policy-pass/mapping.yaml
vendored
5
test/testdata/local-policy-pass/mapping.yaml
vendored
@@ -9,3 +9,8 @@ policies:
|
|||||||
description: Local test images
|
description: Local test images
|
||||||
files:
|
files:
|
||||||
- path: doi/policy.rego
|
- path: doi/policy.rego
|
||||||
|
mirrors:
|
||||||
|
- policy-id: test-images
|
||||||
|
mirror:
|
||||||
|
domains: ["*"]
|
||||||
|
prefix: ""
|
||||||
|
|||||||
49
test/testdata/local-policy/doi/policy.rego
vendored
Normal file
49
test/testdata/local-policy/doi/policy.rego
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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"),
|
||||||
|
})
|
||||||
|
|
||||||
|
opts := {"keys": keys}
|
||||||
|
|
||||||
|
statements contains s if {
|
||||||
|
some att in atts
|
||||||
|
res := attest.verify(att, opts)
|
||||||
|
not res.error
|
||||||
|
s := res.value
|
||||||
|
}
|
||||||
|
|
||||||
|
subjects contains subject if {
|
||||||
|
some statement in statements
|
||||||
|
some subject in statement.subject
|
||||||
|
}
|
||||||
|
|
||||||
|
result := {
|
||||||
|
"success": count(atts) > 0,
|
||||||
|
"violations": set(),
|
||||||
|
"attestations": statements,
|
||||||
|
"summary": {
|
||||||
|
"subjects": subjects,
|
||||||
|
"slsa_level": "SLSA_BUILD_LEVEL_3",
|
||||||
|
"verifier": "docker-official-images",
|
||||||
|
"policy_uri": "https://docker.com/official/policy/v0.1",
|
||||||
|
},
|
||||||
|
}
|
||||||
29
test/testdata/local-policy/mapping.yaml
vendored
Normal file
29
test/testdata/local-policy/mapping.yaml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# map repos to policies
|
||||||
|
version: v1
|
||||||
|
kind: policy-mapping
|
||||||
|
policies:
|
||||||
|
- origin:
|
||||||
|
domain: docker.io
|
||||||
|
prefix: library/
|
||||||
|
id: test-images
|
||||||
|
description: Local test images
|
||||||
|
files:
|
||||||
|
- path: "doi/policy.rego"
|
||||||
|
|
||||||
|
mirrors:
|
||||||
|
- policy-id: test-images
|
||||||
|
mirror:
|
||||||
|
domains: ["*"]
|
||||||
|
prefix: "repo"
|
||||||
|
- policy-id: test-images
|
||||||
|
mirror:
|
||||||
|
domains: ["*"]
|
||||||
|
prefix: "library/"
|
||||||
|
- policy-id: test-images
|
||||||
|
mirror:
|
||||||
|
domains: ["*"]
|
||||||
|
prefix: "test-image"
|
||||||
|
- policy-id: test-images
|
||||||
|
mirror:
|
||||||
|
domains: ["*"]
|
||||||
|
prefix: "image-signer-verifier-test"
|
||||||
15
test/testdata/unsigned-test-image/index.json
vendored
15
test/testdata/unsigned-test-image/index.json
vendored
@@ -1 +1,14 @@
|
|||||||
{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:db8f2a6e112ea6396f57d073269ecfac61e8dcdad3a4a643dcb577522492f898","size":1607,"annotations":{"org.opencontainers.image.created":"2024-04-29T10:23:46Z","org.opencontainers.image.ref.name":"docker.io/library/test-image:test"}}]}
|
{
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"manifests": [
|
||||||
|
{
|
||||||
|
"mediaType": "application/vnd.oci.image.index.v1+json",
|
||||||
|
"digest": "sha256:db8f2a6e112ea6396f57d073269ecfac61e8dcdad3a4a643dcb577522492f898",
|
||||||
|
"size": 1607,
|
||||||
|
"annotations": {
|
||||||
|
"org.opencontainers.image.created": "2024-04-29T10:23:46Z",
|
||||||
|
"org.opencontainers.image.ref.name": "docker.io/library/test-image:test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user