diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 88bdbf8a..46791280 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,7 +50,7 @@ jobs: token: ${{ secrets.TC_CLOUD_TOKEN }} - name: go test including e2e if: matrix.os == 'ubuntu-latest' && github.actor != 'dependabot[bot]' - run: go test -tags=e2e -v ./... -coverprofile=coverage.out -covermode=atomic + run: go test -tags=e2e -v ./... -coverpkg=./... -coverprofile=coverage.out -covermode=atomic - name: go test excluding e2e if: matrix.os == 'macos-latest' || github.actor == 'dependabot[bot]' run: go test -v ./... diff --git a/README.md b/README.md index 06561188..a4fababf 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,10 @@ The input to the policy is an object with the following fields: - `digest` (string): the digest of the image being verified - `purl` (string): the package URL of the image being verified -- `is_canonical` (bool): whether the image being verified was referenced by a 'canonical' name, i.e. one that contains a digest +- `platform` (string): the platform of the image being verified +- `normalized_name` (string): defaults are filled out. e.g. if the image is `alpine`, this would be `library/alpine` +- `familiar_name` (string): short version of above (e.g. `alpine`) +- `tag`: (string): tag of the image being verified (if present) ### Builtin Functions diff --git a/go.mod b/go.mod index ca5f64dc..3f4244a4 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/docker/attest go 1.22.5 require ( - github.com/Masterminds/semver/v3 v3.2.1 - github.com/aws/aws-sdk-go-v2/config v1.27.28 + github.com/Masterminds/semver/v3 v3.3.0 + github.com/aws/aws-sdk-go-v2/config v1.27.31 github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8 github.com/containerd/platforms v0.2.1 github.com/distribution/reference v0.6.0 @@ -24,7 +24,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go/modules/registry v0.33.0 github.com/theupdateframework/go-tuf/v2 v2.0.0 - google.golang.org/api v0.192.0 + google.golang.org/api v0.194.0 sigs.k8s.io/yaml v1.4.0 ) @@ -32,9 +32,9 @@ require ( replace github.com/google/go-containerregistry => github.com/docker/go-containerregistry v0.0.0-20240808132857-c8bfc44af7c8 require ( - cloud.google.com/go v0.115.0 // indirect - cloud.google.com/go/auth v0.8.1 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect + cloud.google.com/go v0.115.1 // indirect + cloud.google.com/go/auth v0.9.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect cloud.google.com/go/compute/metadata v0.5.0 // indirect cloud.google.com/go/iam v1.1.12 // indirect cloud.google.com/go/kms v1.18.4 // indirect @@ -47,7 +47,7 @@ require ( github.com/agnivade/levenshtein v1.1.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go-v2 v1.30.4 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.28 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.30 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect @@ -59,7 +59,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.30.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.30.5 // indirect github.com/aws/smithy-go v1.20.4 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect @@ -181,19 +181,19 @@ require ( go.opentelemetry.io/otel/trace v1.28.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.25.0 // indirect + golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/mod v0.19.0 // indirect - golang.org/x/net v0.27.0 // indirect + golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.6.0 // indirect - google.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect + google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 6478850d..0139af4a 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,10 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= -cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= -cloud.google.com/go/auth v0.8.1 h1:QZW9FjC5lZzN864p13YxvAtGUlQ+KgRL+8Sg45Z6vxo= -cloud.google.com/go/auth v0.8.1/go.mod h1:qGVp/Y3kDRSDZ5gFD/XPUfYQ9xW1iI7q8RIRoCyBbJc= -cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI= -cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I= +cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ= +cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc= +cloud.google.com/go/auth v0.9.1 h1:+pMtLEV2k0AXKvs/tGZojuj6QaioxfUjOpMsG5Gtx+w= +cloud.google.com/go/auth v0.9.1/go.mod h1:Sw8ocT5mhhXxFklyhT12Eiy0ed6tTrPMCJjSI8KhYLk= +cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= +cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= cloud.google.com/go/iam v1.1.12 h1:JixGLimRrNGcxvJEQ8+clfLxPlbeZA6MuRJ+qJNQ5Xw= @@ -60,8 +60,8 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= @@ -104,10 +104,10 @@ github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8= github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= -github.com/aws/aws-sdk-go-v2/config v1.27.28 h1:OTxWGW/91C61QlneCtnD62NLb4W616/NM1jA8LhJqbg= -github.com/aws/aws-sdk-go-v2/config v1.27.28/go.mod h1:uzVRVtJSU5EFv6Fu82AoVFKozJi2ZCY6WRCXj06rbvs= -github.com/aws/aws-sdk-go-v2/credentials v1.17.28 h1:m8+AHY/ND8CMHJnPoH7PJIRakWGa4gbfbxuY9TGTUXM= -github.com/aws/aws-sdk-go-v2/credentials v1.17.28/go.mod h1:6TF7dSc78ehD1SL6KpRIPKMA1GyyWflIkjqg+qmf4+c= +github.com/aws/aws-sdk-go-v2/config v1.27.31 h1:kxBoRsjhT3pq0cKthgj6RU6bXTm/2SgdoUMyrVw0rAI= +github.com/aws/aws-sdk-go-v2/config v1.27.31/go.mod h1:z04nZdSWFPaDwK3DdJOG2r+scLQzMYuJeW0CujEm9FM= +github.com/aws/aws-sdk-go-v2/credentials v1.17.30 h1:aau/oYFtibVovr2rDt8FHlU17BTicFEMAi29V1U+L5Q= +github.com/aws/aws-sdk-go-v2/credentials v1.17.30/go.mod h1:BPJ/yXV92ZVq6G8uYvbU0gSl8q94UB63nMT5ctNO38g= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 h1:yjwoSyDZF8Jth+mUk5lSPJCkMC0lMy6FaCD51jm6ayE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12/go.mod h1:fuR57fAgMk7ot3WcNQfb6rSEn+SUffl7ri+aa8uKysI= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY= @@ -130,8 +130,8 @@ github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 h1:zCsFCKvbj25i7p1u94imVoO447I/ github.com/aws/aws-sdk-go-v2/service/sso v1.22.5/go.mod h1:ZeDX1SnKsVlejeuz41GiajjZpRSWR7/42q/EyA/QEiM= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 h1:SKvPgvdvmiTWoi0GAJ7AsJfOz3ngVkD/ERbs5pUnHNI= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5/go.mod h1:20sz31hv/WsPa3HhU3hfrIet2kxM4Pe0r20eBZ20Tac= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.4 h1:iAckBT2OeEK/kBDyN/jDtpEExhjeeA/Im2q4X0rJZT8= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.4/go.mod h1:vmSqFK+BVIwVpDAGZB3CoCXHzurt4qBE8lf+I/kRTh0= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.5 h1:OMsEmCyz2i89XwRwPouAJvhj81wINh+4UK+k/0Yo/q8= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.5/go.mod h1:vmSqFK+BVIwVpDAGZB3CoCXHzurt4qBE8lf+I/kRTh0= github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4= github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8 h1:SoFYaT9UyGkR0+nogNyD/Lj+bsixB+SNuAS4ABlEs6M= @@ -692,8 +692,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= @@ -725,8 +725,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= @@ -769,15 +769,15 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -785,8 +785,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -807,19 +807,19 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/api v0.192.0 h1:PljqpNAfZaaSpS+TnANfnNAXKdzHM/B9bKhwRlo7JP0= -google.golang.org/api v0.192.0/go.mod h1:9VcphjvAxPKLmSxVSzPlSRXy/5ARMEw5bf58WoVXafQ= +google.golang.org/api v0.194.0 h1:dztZKG9HgtIpbI35FhfuSNR/zmaMVdxNlntHj1sIS4s= +google.golang.org/api v0.194.0/go.mod h1:AgvUFdojGANh3vI+P7EVnxj3AISHllxGCJSFmggmnd0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf h1:OqdXDEakZCVtDiZTjcxfwbHPCT11ycCEsTKesBVKvyY= -google.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:mCr1K1c8kX+1iSBREvU3Juo11CB+QOEWxbRS01wWl5M= -google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f h1:b1Ln/PG8orm0SsBbHZWke8dDp2lrCD4jSmfglFpTZbk= -google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142 h1:oLiyxGgE+rt22duwci1+TG7bg2/L1LQsXwfjPlmuJA0= +google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142/go.mod h1:G11eXq53iI5Q+kyNOmCvnzBaxEA2Q/Ik5Tj7nqBE8j4= +google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf h1:GillM0Ef0pkZPIB+5iO6SDK+4T9pf6TpaYR6ICD5rVE= +google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:OFMYQFHJ4TM3JRlWDZhJbZfra2uqc3WLBZiaaqP4DtU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= diff --git a/pkg/attest/example_verify_test.go b/pkg/attest/example_verify_test.go index 25e30fd5..4fa64beb 100644 --- a/pkg/attest/example_verify_test.go +++ b/pkg/attest/example_verify_test.go @@ -6,24 +6,12 @@ import ( "os" "path/filepath" - "github.com/docker/attest/internal/embed" "github.com/docker/attest/pkg/attest" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/policy" "github.com/docker/attest/pkg/tuf" ) -func createTufClient(outputPath string) (*tuf.Client, error) { - // using oci tuf metadata and targets - metadataURI := "registry-1.docker.io/docker/tuf-metadata:latest" - targetsURI := "registry-1.docker.io/docker/tuf-targets" - // example using http tuf metadata and targets - // metadataURI := "https://docker.github.io/tuf-staging/metadata" - // targetsURI := "https://docker.github.io/tuf-staging/targets" - - return tuf.NewClient(embed.RootStaging.Data, outputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker()) -} - func ExampleVerify_remote() { // create a tuf client home, err := os.UserHomeDir() @@ -31,10 +19,7 @@ func ExampleVerify_remote() { panic(err) } tufOutputPath := filepath.Join(home, ".docker", "tuf") - tufClient, err := createTufClient(tufOutputPath) - if err != nil { - panic(err) - } + tufClientOpts := tuf.NewDockerDefaultClientOptions(tufOutputPath) // create a resolver for remote attestations image := "registry-1.docker.io/library/notary:server" @@ -42,10 +27,11 @@ func ExampleVerify_remote() { // configure policy options opts := &policy.Options{ - TUFClient: tufClient, - LocalTargetsDir: filepath.Join(home, ".docker", "policy"), // location to store policy files downloaded from TUF - LocalPolicyDir: "", // overrides TUF policy for local policy files if set - PolicyID: "", // set to ignore policy mapping and select a policy by id + TUFClientOptions: tufClientOpts, + LocalTargetsDir: filepath.Join(home, ".docker", "policy"), // location to store policy files downloaded from TUF + LocalPolicyDir: "", // overrides TUF policy for local policy files if set + PolicyID: "", // set to ignore policy mapping and select a policy by id + DisableTUF: false, // set to disable TUF and rely on local policy files } src, err := oci.ParseImageSpec(image, oci.WithPlatform(platform)) diff --git a/pkg/attest/sign_test.go b/pkg/attest/sign_test.go index 3d6a602d..32f1958e 100644 --- a/pkg/attest/sign_test.go +++ b/pkg/attest/sign_test.go @@ -20,6 +20,8 @@ var ( PassMirrorPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-mirror") PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl") FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail") + InputsPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-inputs") + EmptyPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-policies") TestTempDir = "attest-sign-test" ) @@ -41,6 +43,7 @@ func TestSignVerifyOCILayout(t *testing.T) { } policyOpts := &policy.Options{ LocalPolicyDir: PassPolicyDir, + DisableTUF: true, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/pkg/attest/verify.go b/pkg/attest/verify.go index d30082e1..4d26851e 100644 --- a/pkg/attest/verify.go +++ b/pkg/attest/verify.go @@ -3,6 +3,8 @@ package attest import ( "context" "fmt" + "os" + "path/filepath" "strings" "time" @@ -11,22 +13,45 @@ import ( "github.com/docker/attest/pkg/config" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/policy" + "github.com/docker/attest/pkg/tuf" intoto "github.com/in-toto/in-toto-golang/in_toto" ) -func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.Options) (result *VerificationResult, err error) { +type Verifier interface { + Verify(ctx context.Context, src *oci.ImageSpec) (result *VerificationResult, err error) +} + +type tufVerifier struct { + opts *policy.Options + tufClient tuf.Downloader +} + +func NewVerifier(opts *policy.Options) (Verifier, error) { + err := populateDefaultOptions(opts) + if err != nil { + return nil, err + } + var tufClient tuf.Downloader + if !opts.DisableTUF { + tufClient, err = tuf.NewClient(opts.TUFClientOptions) + if err != nil { + return nil, fmt.Errorf("failed to create TUF client: %w", err) + } + } + return &tufVerifier{ + opts: opts, + tufClient: tufClient, + }, nil +} + +func (v *tufVerifier) Verify(ctx context.Context, src *oci.ImageSpec) (result *VerificationResult, err error) { // so that we can resolve mapping from the image name earlier detailsResolver, err := policy.CreateImageDetailsResolver(src) if err != nil { return nil, fmt.Errorf("failed to create image details resolver: %w", err) } - if opts.AttestationStyle == "" { - opts.AttestationStyle = config.AttestationStyleReferrers - } - 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) + + pctx, err := policy.ResolvePolicy(ctx, v.tufClient, detailsResolver, v.opts) if err != nil { return nil, fmt.Errorf("failed to resolve policy: %w", err) } @@ -37,14 +62,14 @@ func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.Options) (resu }, nil } // this is overriding the mapping with a referrers config. Useful for testing if nothing else - if opts.ReferrersRepo != "" { + if v.opts.ReferrersRepo != "" { pctx.Mapping.Attestations = &config.AttestationConfig{ - Repo: opts.ReferrersRepo, + Repo: v.opts.ReferrersRepo, Style: config.AttestationStyleReferrers, } - } else if opts.AttestationStyle == config.AttestationStyleAttached { + } else if v.opts.AttestationStyle == config.AttestationStyleAttached { pctx.Mapping.Attestations = &config.AttestationConfig{ - Repo: opts.ReferrersRepo, + Repo: v.opts.ReferrersRepo, Style: config.AttestationStyleAttached, } } @@ -60,6 +85,47 @@ func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.Options) (resu return result, nil } +func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.Options) (result *VerificationResult, err error) { + verifier, err := NewVerifier(opts) + if err != nil { + return nil, err + } + return verifier.Verify(ctx, src) +} + +func populateDefaultOptions(opts *policy.Options) (err error) { + if opts.LocalPolicyDir == "" && opts.DisableTUF { + return fmt.Errorf("local policy dir must be set if not using TUF") + } + if opts.LocalTargetsDir == "" { + opts.LocalTargetsDir, err = defaultLocalTargetsDir() + if err != nil { + return err + } + } + if opts.DisableTUF && opts.TUFClientOptions != nil { + return fmt.Errorf("TUF client options set but TUF disabled") + } else if opts.TUFClientOptions == nil && !opts.DisableTUF { + opts.TUFClientOptions = tuf.NewDockerDefaultClientOptions(opts.LocalTargetsDir) + } + + if opts.AttestationStyle == "" { + opts.AttestationStyle = config.AttestationStyleReferrers + } + if opts.ReferrersRepo != "" && opts.AttestationStyle != config.AttestationStyleReferrers { + return fmt.Errorf("referrers repo specified but attestation source not set to referrers") + } + return nil +} + +func defaultLocalTargetsDir() (string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get user home directory: %w", err) + } + return filepath.Join(homeDir, ".docker", "tuf"), nil +} + func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.Result) (*VerificationResult, error) { dgst, err := oci.SplitDigest(input.Digest) if err != nil { @@ -140,14 +206,34 @@ func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, pctx name = strings.Replace(name, oldName, pctx.ResolvedName, 1) } - purl, canonical, err := oci.RefToPURL(name, platform) + ref, err := reference.ParseNormalizedNamed(name) + if err != nil { + return nil, fmt.Errorf("failed to parse ref %q: %w", ref, err) + } + purl, canonical, err := oci.RefToPURL(ref, platform) if err != nil { return nil, fmt.Errorf("failed to convert ref to purl: %w", err) } + var tag string + if !canonical { + // unlike the function name indicates, this adds latest if no tag is present + ref = reference.TagNameOnly(ref) + } + + if tagged, ok := ref.(reference.Tagged); ok { + tag = tagged.Tag() + } input := &policy.Input{ - Digest: digest, - PURL: purl, - IsCanonical: canonical, + Digest: digest, + PURL: purl, + Platform: platform.String(), + Domain: reference.Domain(ref), + NormalizedName: reference.Path(ref), + FamiliarName: reference.FamiliarName(ref), + } + // rego has null strings + if tag != "" { + input.Tag = tag } evaluator, err := policy.GetPolicyEvaluator(ctx) diff --git a/pkg/attest/verify_test.go b/pkg/attest/verify_test.go index b386619f..923a06dd 100644 --- a/pkg/attest/verify_test.go +++ b/pkg/attest/verify_test.go @@ -8,11 +8,13 @@ import ( "path/filepath" "testing" + "github.com/distribution/reference" "github.com/docker/attest/internal/test" "github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/config" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/policy" + "github.com/docker/attest/pkg/tuf" intoto "github.com/in-toto/in-toto-golang/in_toto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -90,6 +92,7 @@ func TestVSA(t *testing.T) { policyOpts := &policy.Options{ LocalPolicyDir: PassPolicyDir, AttestationStyle: config.AttestationStyleAttached, + DisableTUF: true, } results, err := Verify(ctx, spec, policyOpts) require.NoError(t, err) @@ -98,7 +101,7 @@ func TestVSA(t *testing.T) { if assert.NotNil(t, results.Input) { assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", results.Input.Digest) - assert.False(t, results.Input.IsCanonical) + assert.NotNil(t, results.Input.Tag) } assert.Equal(t, intoto.StatementInTotoV01, results.VSA.Type) @@ -142,6 +145,7 @@ func TestVerificationFailure(t *testing.T) { policyOpts := &policy.Options{ LocalPolicyDir: FailPolicyDir, AttestationStyle: config.AttestationStyleAttached, + DisableTUF: true, } results, err := Verify(ctx, spec, policyOpts) require.NoError(t, err) @@ -176,17 +180,18 @@ func TestSignVerify(t *testing.T) { outputLayout := test.CreateTempDir(t, "", TestTempDir) testCases := []struct { - name string - signTL bool - policyDir string - imageName string - expectError bool + name string + signTL bool + policyDir string + imageName string + expectedNonSuccess Outcome }{ {name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir}, {name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir}, {name: "no tl", signTL: false, policyDir: PassPolicyDir}, {name: "mirror", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "mirror.org/library/test-image:test"}, - {name: "mirror no match", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectError: true}, + {name: "mirror no match", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectedNonSuccess: OutcomeNoPolicy}, + {name: "verify inputs", signTL: false, policyDir: InputsPolicyDir}, } attIdx, err := oci.IndexFromPath(test.UnsignedTestImage) @@ -216,19 +221,93 @@ func TestSignVerify(t *testing.T) { policyOpts := &policy.Options{ LocalPolicyDir: tc.policyDir, + DisableTUF: true, } results, err := Verify(ctx, spec, policyOpts) - if tc.expectError { - require.Error(t, err) + require.NoError(t, err) + if tc.expectedNonSuccess != "" { + assert.Equal(t, tc.expectedNonSuccess, results.Outcome) return } - require.NoError(t, err) assert.Equal(t, OutcomeSuccess, results.Outcome) platform, err := oci.ParsePlatform(LinuxAMD64) require.NoError(t, err) - expectedPURL, _, err := oci.RefToPURL(attIdx.Name, platform) + + ref, err := reference.ParseNormalizedNamed(attIdx.Name) + require.NoError(t, err) + expectedPURL, _, err := oci.RefToPURL(ref, platform) require.NoError(t, err) assert.Equal(t, expectedPURL, results.Input.PURL) }) } } + +func TestDefaultOptions(t *testing.T) { + testCases := []struct { + name string + tufOpts *tuf.ClientOptions + localTargetsDir string + attestationStyle config.AttestationStyle + referrersRepo string + expectedError string + disableTuf bool + localPolicyDir string + }{ + {name: "empty"}, + {name: "tufClient provided", tufOpts: &tuf.ClientOptions{MetadataSource: "a", TargetsSource: "b"}}, + {name: "localTargetsDir provided", localTargetsDir: test.CreateTempDir(t, "", TestTempDir)}, + {name: "attestationStyle provided", attestationStyle: config.AttestationStyleAttached}, + {name: "referrersRepo provided", referrersRepo: "referrers"}, + {name: "referrersRepo provided with attached", referrersRepo: "referrers", attestationStyle: config.AttestationStyleAttached, expectedError: "referrers repo specified but attestation source not set to referrers"}, + {name: "tuf disabled and no local-policy-dir", disableTuf: true, expectedError: "local policy dir must be set if not using TUF"}, + {name: "tuf disabled but options set", disableTuf: true, tufOpts: &tuf.ClientOptions{MetadataSource: "a", TargetsSource: "b"}, localPolicyDir: "foo", expectedError: "TUF client options set but TUF disabled"}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defaultTargets, err := defaultLocalTargetsDir() + require.NoError(t, err) + + opts := &policy.Options{ + TUFClientOptions: tc.tufOpts, + LocalTargetsDir: tc.localTargetsDir, + AttestationStyle: tc.attestationStyle, + ReferrersRepo: tc.referrersRepo, + DisableTUF: tc.disableTuf, + LocalPolicyDir: tc.localPolicyDir, + } + + err = populateDefaultOptions(opts) + if tc.expectedError != "" { + require.Error(t, err) + assert.Equal(t, tc.expectedError, err.Error()) + return + } + + require.NoError(t, err) + + if tc.localTargetsDir != "" { + assert.Equal(t, tc.localTargetsDir, opts.LocalTargetsDir) + } else { + assert.Equal(t, defaultTargets, opts.LocalTargetsDir) + } + + if tc.attestationStyle != "" { + assert.Equal(t, tc.attestationStyle, opts.AttestationStyle) + } else { + assert.Equal(t, config.AttestationStyleReferrers, opts.AttestationStyle) + } + + if tc.tufOpts != nil { + assert.Equal(t, tc.tufOpts, opts.TUFClientOptions) + } else { + assert.NotNil(t, opts.TUFClientOptions) + } + + if tc.referrersRepo != "" { + assert.Equal(t, tc.referrersRepo, opts.ReferrersRepo) + } else { + assert.Empty(t, opts.ReferrersRepo) + } + }) + } +} diff --git a/pkg/attestation/referrers_test.go b/pkg/attestation/referrers_test.go index 9d7d2727..913fec41 100644 --- a/pkg/attestation/referrers_test.go +++ b/pkg/attestation/referrers_test.go @@ -139,6 +139,7 @@ func TestAttestationReferenceTypes(t *testing.T) { policyOpts := &policy.Options{ LocalPolicyDir: LocalPolicy, + DisableTUF: true, } if tc.referrersRepo != "" { @@ -269,6 +270,7 @@ func TestReferencesInDifferentRepo(t *testing.T) { referencedImage := fmt.Sprintf("%s@%s", indexName, mf.Digest.String()) policyOpts := &policy.Options{ LocalPolicyDir: PassPolicyDir, + DisableTUF: true, } src, err := oci.ParseImageSpec(referencedImage) require.NoError(t, err) diff --git a/pkg/mirror/example_mirror_test.go b/pkg/mirror/example_mirror_test.go index 37003a24..c4c4b157 100644 --- a/pkg/mirror/example_mirror_test.go +++ b/pkg/mirror/example_mirror_test.go @@ -6,7 +6,6 @@ import ( "path/filepath" "strings" - "github.com/docker/attest/internal/embed" "github.com/docker/attest/pkg/mirror" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/tuf" @@ -30,7 +29,7 @@ func ExampleNewTUFMirror() { // configure TUF mirror metadataURI := "https://docker.github.io/tuf-staging/metadata" targetsURI := "https://docker.github.io/tuf-staging/targets" - m, err := mirror.NewTUFMirror(embed.RootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker()) + m, err := mirror.NewTUFMirror(tuf.DockerTUFRootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker()) if err != nil { panic(err) } diff --git a/pkg/mirror/metadata_test.go b/pkg/mirror/metadata_test.go index ee5417eb..861d7834 100644 --- a/pkg/mirror/metadata_test.go +++ b/pkg/mirror/metadata_test.go @@ -9,7 +9,6 @@ import ( "strings" "testing" - "github.com/docker/attest/internal/embed" "github.com/docker/attest/internal/test" "github.com/docker/attest/pkg/tuf" "github.com/stretchr/testify/assert" @@ -26,7 +25,7 @@ func TestGetTufMetadataMirror(t *testing.T) { defer server.Close() path := test.CreateTempDir(t, "", "tuf_temp") - m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) + m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) assert.NoError(t, err) tufMetadata, err := m.getMetadataMirror(server.URL + metadataPath) @@ -44,7 +43,7 @@ func TestGetMetadataManifest(t *testing.T) { defer server.Close() path := test.CreateTempDir(t, "", "tuf_temp") - m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) + m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) assert.NoError(t, err) img, err := m.GetMetadataManifest(server.URL + metadataPath) @@ -83,7 +82,7 @@ func TestGetDelegatedMetadataMirrors(t *testing.T) { defer server.Close() path := test.CreateTempDir(t, "", "tuf_temp") - m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) + m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) assert.NoError(t, err) delegations, err := m.GetDelegatedMetadataMirrors() diff --git a/pkg/mirror/mirror.go b/pkg/mirror/mirror.go index bf376bfe..55c86314 100644 --- a/pkg/mirror/mirror.go +++ b/pkg/mirror/mirror.go @@ -3,15 +3,14 @@ package mirror import ( "fmt" - "github.com/docker/attest/internal/embed" "github.com/docker/attest/pkg/tuf" ) func NewTUFMirror(root []byte, tufPath, metadataURL, targetsURL string, versionChecker tuf.VersionChecker) (*TUFMirror, error) { if root == nil { - root = embed.RootDefault.Data + root = tuf.DockerTUFRootDefault.Data } - tufClient, err := tuf.NewClient(root, tufPath, metadataURL, targetsURL, versionChecker) + tufClient, err := tuf.NewClient(&tuf.ClientOptions{InitialRoot: root, Path: tufPath, MetadataSource: metadataURL, TargetsSource: targetsURL, VersionChecker: versionChecker}) if err != nil { return nil, fmt.Errorf("failed to create TUF client: %w", err) } diff --git a/pkg/mirror/targets_test.go b/pkg/mirror/targets_test.go index c2b3ec52..b229e427 100644 --- a/pkg/mirror/targets_test.go +++ b/pkg/mirror/targets_test.go @@ -8,7 +8,6 @@ import ( "strings" "testing" - "github.com/docker/attest/internal/embed" "github.com/docker/attest/internal/test" "github.com/docker/attest/pkg/tuf" "github.com/stretchr/testify/assert" @@ -27,7 +26,7 @@ func TestGetTufTargetsMirror(t *testing.T) { defer server.Close() path := test.CreateTempDir(t, "", "tuf_temp") - m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) + m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) assert.NoError(t, err) targets, err := m.GetTUFTargetMirrors() @@ -61,7 +60,7 @@ func TestTargetDelegationMetadata(t *testing.T) { defer server.Close() path := test.CreateTempDir(t, "", "tuf_temp") - tm, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) + tm, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) assert.NoError(t, err) targets, err := tm.TUFClient.LoadDelegatedTargets("test-role", "targets") @@ -74,7 +73,7 @@ func TestGetDelegatedTargetMirrors(t *testing.T) { defer server.Close() path := test.CreateTempDir(t, "", "tuf_temp") - m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) + m, err := NewTUFMirror(tuf.DockerTUFRootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker()) assert.NoError(t, err) mirrors, err := m.GetDelegatedTargetMirrors() diff --git a/pkg/oci/oci.go b/pkg/oci/oci.go index f5cc573b..3d52ecdc 100644 --- a/pkg/oci/oci.go +++ b/pkg/oci/oci.go @@ -52,12 +52,8 @@ func ImageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descripto return nil, fmt.Errorf("no image found for platform %v", platform) } -func RefToPURL(ref string, platform *v1.Platform) (string, bool, error) { +func RefToPURL(named reference.Named, platform *v1.Platform) (string, bool, error) { var isCanonical bool - named, err := reference.ParseNormalizedNamed(ref) - if err != nil { - return "", false, fmt.Errorf("failed to parse ref %q: %w", ref, err) - } var qualifiers []packageurl.Qualifier if canonical, ok := named.(reference.Canonical); ok { diff --git a/pkg/oci/oci_test.go b/pkg/oci/oci_test.go index 03937d05..898f5afb 100644 --- a/pkg/oci/oci_test.go +++ b/pkg/oci/oci_test.go @@ -3,6 +3,7 @@ package oci_test import ( "testing" + "github.com/distribution/reference" "github.com/docker/attest/internal/test" "github.com/docker/attest/pkg/oci" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -14,42 +15,51 @@ import ( func TestRefToPurl(t *testing.T) { arm, err := oci.ParsePlatform("arm64/linux") require.NoError(t, err) - purl, canonical, err := oci.RefToPURL("alpine", arm) + ref, err := reference.ParseNormalizedNamed("alpine") + require.NoError(t, err) + purl, canonical, err := oci.RefToPURL(ref, arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/alpine@latest?platform=arm64%2Flinux", purl) assert.False(t, canonical) - - purl, canonical, err = oci.RefToPURL("alpine:123", arm) + ref, err = reference.ParseNormalizedNamed("alpine:123") + require.NoError(t, err) + purl, canonical, err = oci.RefToPURL(ref, arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - - purl, canonical, err = oci.RefToPURL("google/alpine:123", arm) + ref, err = reference.ParseNormalizedNamed("google/alpine:123") + require.NoError(t, err) + purl, canonical, err = oci.RefToPURL(ref, arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/google/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - - purl, canonical, err = oci.RefToPURL("library/alpine:123", arm) + ref, err = reference.ParseNormalizedNamed("library/alpine:123") + require.NoError(t, err) + purl, canonical, err = oci.RefToPURL(ref, arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - - purl, canonical, err = oci.RefToPURL("docker.io/library/alpine:123", arm) + ref, err = reference.ParseNormalizedNamed("docker.io/library/alpine:123") + require.NoError(t, err) + purl, canonical, err = oci.RefToPURL(ref, arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - - purl, canonical, err = oci.RefToPURL("localhost:5001/library/alpine:123", arm) + ref, err = reference.ParseNormalizedNamed("localhost:5001/library/alpine:123") + require.NoError(t, err) + purl, canonical, err = oci.RefToPURL(ref, arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/localhost%3A5001/library/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - - purl, canonical, err = oci.RefToPURL("localhost:5001/alpine:123", arm) + ref, err = reference.ParseNormalizedNamed("localhost:5001/alpine:123") + require.NoError(t, err) + purl, canonical, err = oci.RefToPURL(ref, arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/localhost%3A5001/alpine@123?platform=arm64%2Flinux", purl) assert.False(t, canonical) - - purl, canonical, err = oci.RefToPURL("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b", arm) + ref, err = reference.ParseNormalizedNamed("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b") + require.NoError(t, err) + purl, canonical, err = oci.RefToPURL(ref, arm) assert.NoError(t, err) assert.Equal(t, "pkg:docker/localhost%3A5001/alpine?digest=sha256%3Ac5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b&platform=arm64%2Flinux", purl) assert.True(t, canonical) diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index 6a82d150..03d9a9b5 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -12,6 +12,7 @@ import ( "github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/config" "github.com/docker/attest/pkg/oci" + "github.com/docker/attest/pkg/tuf" ) func resolveLocalPolicy(opts *Options, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) { @@ -57,13 +58,13 @@ func resolveLocalPolicy(opts *Options, mapping *config.PolicyMapping, imageName return policy, nil } -func resolveTUFPolicy(opts *Options, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) { +func resolveTUFPolicy(opts *Options, tufClient tuf.Downloader, mapping *config.PolicyMapping, imageName string, matchedName string) (*Policy, error) { var URI string var digest map[string]string files := make([]*File, 0, len(mapping.Files)) for _, f := range mapping.Files { filename := f.Path - file, err := opts.TUFClient.DownloadTarget(filename, filepath.Join(opts.LocalTargetsDir, filename)) + file, err := tufClient.DownloadTarget(filename, filepath.Join(opts.LocalTargetsDir, filename)) if err != nil { return nil, fmt.Errorf("failed to download policy file %s: %w", filename, err) } @@ -154,7 +155,7 @@ func findPolicyMatchImpl(imageName string, mappings *config.PolicyMappings, matc return &policyMatch{matchType: matchTypeNoMatch, matchedName: imageName}, nil } -func resolvePolicyByID(opts *Options) (*Policy, error) { +func resolvePolicyByID(opts *Options, tufClient tuf.Downloader) (*Policy, error) { if opts.PolicyID != "" { localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir) if err != nil { @@ -166,23 +167,24 @@ func resolvePolicyByID(opts *Options) (*Policy, error) { return resolveLocalPolicy(opts, policy, "", "") } } - - // must check tuf - tufMappings, err := config.LoadTUFMappings(opts.TUFClient, opts.LocalTargetsDir) - if err != nil { - return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err) - } - policy := tufMappings.Policies[opts.PolicyID] - if policy != nil { - return resolveTUFPolicy(opts, policy, "", "") + if !opts.DisableTUF { + // must check tuf + tufMappings, err := config.LoadTUFMappings(tufClient, opts.LocalTargetsDir) + if err != nil { + return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err) + } + policy := tufMappings.Policies[opts.PolicyID] + if policy != nil { + return resolveTUFPolicy(opts, tufClient, policy, "", "") + } } return nil, fmt.Errorf("policy with id %s not found", opts.PolicyID) } return nil, nil } -func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver, opts *Options) (*Policy, error) { - p, err := resolvePolicyByID(opts) +func ResolvePolicy(ctx context.Context, tufClient tuf.Downloader, detailsResolver oci.ImageDetailsResolver, opts *Options) (*Policy, error) { + p, err := resolvePolicyByID(opts, tufClient) if err != nil { return nil, fmt.Errorf("failed to resolve policy by id: %w", err) } @@ -208,29 +210,31 @@ func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver if match.matchType == matchTypePolicy { return resolveLocalPolicy(opts, match.policy, imageName, match.matchedName) } - // must check tuf - tufMappings, err := config.LoadTUFMappings(opts.TUFClient, opts.LocalTargetsDir) - if err != nil { - return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err) - } + if !opts.DisableTUF { + // must check tuf + tufMappings, err := config.LoadTUFMappings(tufClient, opts.LocalTargetsDir) + if err != nil { + return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err) + } - // it's a mirror of a tuf policy - if match.matchType == matchTypeMatchNoPolicy { - for _, mapping := range tufMappings.Policies { - if mapping.ID == match.rule.PolicyID { - return resolveTUFPolicy(opts, mapping, imageName, match.matchedName) + // it's a mirror of a tuf policy + if match.matchType == matchTypeMatchNoPolicy { + for _, mapping := range tufMappings.Policies { + if mapping.ID == match.rule.PolicyID { + return resolveTUFPolicy(opts, tufClient, mapping, imageName, match.matchedName) + } } } + // try to resolve a tuf policy directly + match, err = findPolicyMatch(imageName, tufMappings) + if err != nil { + return nil, err + } + if match.matchType == matchTypePolicy { + return resolveTUFPolicy(opts, tufClient, match.policy, imageName, match.matchedName) + } } - // try to resolve a tuf policy directly - match, err = findPolicyMatch(imageName, tufMappings) - if err != nil { - return nil, err - } - if match.matchType == matchTypePolicy { - return resolveTUFPolicy(opts, match.policy, imageName, match.matchedName) - } return nil, nil } diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index b282daf5..9812eec2 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -11,7 +11,6 @@ import ( "github.com/docker/attest/pkg/config" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/policy" - "github.com/docker/attest/pkg/tuf" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -51,31 +50,33 @@ func TestRegoEvaluator_Evaluate(t *testing.T) { policyID string resolveErrorStr string }{ - {repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver}, - {repo: "testdata/mock-tuf-allow", expectSuccess: true, isCanonical: false, resolver: defaultResolver, policyID: "docker-official-images"}, - {repo: "testdata/mock-tuf-allow", expectSuccess: false, isCanonical: false, resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr}, - {repo: "testdata/mock-tuf-deny", expectSuccess: false, isCanonical: false, resolver: defaultResolver}, - {repo: "testdata/mock-tuf-verify-sig", expectSuccess: true, isCanonical: false, resolver: defaultResolver}, - {repo: "testdata/mock-tuf-wrong-key", expectSuccess: false, isCanonical: false, resolver: defaultResolver}, - {repo: "testdata/mock-tuf-allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver}, - {repo: "testdata/mock-tuf-allow-canonical", expectSuccess: false, isCanonical: false, resolver: defaultResolver}, - {repo: "testdata/mock-tuf-no-rego", expectSuccess: false, isCanonical: false, resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"}, + {repo: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver}, + {repo: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver, policyID: "docker-official-images"}, + {repo: "testdata/policies/allow", resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr}, + {repo: "testdata/policies/deny", resolver: defaultResolver}, + {repo: "testdata/policies/verify-sig", expectSuccess: true, resolver: defaultResolver}, + {repo: "testdata/policies/wrong-key", resolver: defaultResolver}, + {repo: "testdata/policies/allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver}, + {repo: "testdata/policies/allow-canonical", resolver: defaultResolver}, + {repo: "testdata/policies/no-rego", resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"}, } for _, tc := range testCases { t.Run(tc.repo, func(t *testing.T) { input := &policy.Input{ - Digest: "sha256:test-digest", - PURL: "test-purl", - IsCanonical: tc.isCanonical, + Digest: "sha256:test-digest", + PURL: "test-purl", + } + if !tc.isCanonical { + input.Tag = "test" } - tufClient := tuf.NewMockTufClient(tc.repo, test.CreateTempDir(t, "", "tuf-dest")) if tc.policy == nil { tc.policy = &policy.Options{ - TUFClient: tufClient, LocalTargetsDir: test.CreateTempDir(t, "", "tuf-targets"), PolicyID: tc.policyID, + LocalPolicyDir: tc.repo, + DisableTUF: true, } } imageName, err := tc.resolver.ImageName(ctx) @@ -86,7 +87,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) { require.NoError(t, err) resolver, err := policy.CreateImageDetailsResolver(src) require.NoError(t, err) - policy, err := policy.ResolvePolicy(ctx, resolver, tc.policy) + policy, err := policy.ResolvePolicy(ctx, nil, resolver, tc.policy) if tc.resolveErrorStr != "" { require.Error(t, err) assert.Contains(t, err.Error(), tc.resolveErrorStr) @@ -107,7 +108,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) { } func TestLoadingMappings(t *testing.T) { - policyMappings, err := config.LoadLocalMappings(filepath.Join("testdata", "mock-tuf-allow")) + policyMappings, err := config.LoadLocalMappings(filepath.Join("testdata", "policies", "allow")) require.NoError(t, err) assert.Equal(t, len(policyMappings.Rules), 3) for _, mirror := range policyMappings.Rules { diff --git a/pkg/policy/testdata/mock-tuf-allow-canonical/doi/policy.rego b/pkg/policy/testdata/mock-tuf-allow-canonical/doi/policy.rego deleted file mode 100644 index aa197a2f..00000000 --- a/pkg/policy/testdata/mock-tuf-allow-canonical/doi/policy.rego +++ /dev/null @@ -1,7 +0,0 @@ -package attest - -import rego.v1 - -result := { - "success": input.isCanonical, -} diff --git a/pkg/policy/testdata/policies/allow-canonical/doi/policy.rego b/pkg/policy/testdata/policies/allow-canonical/doi/policy.rego new file mode 100644 index 00000000..295228bf --- /dev/null +++ b/pkg/policy/testdata/policies/allow-canonical/doi/policy.rego @@ -0,0 +1,11 @@ +package attest + +import rego.v1 + +default canonical = false + +canonical if { + not input.tag +} + +result := {"success": canonical} diff --git a/pkg/policy/testdata/mock-tuf-allow-canonical/mapping.yaml b/pkg/policy/testdata/policies/allow-canonical/mapping.yaml similarity index 100% rename from pkg/policy/testdata/mock-tuf-allow-canonical/mapping.yaml rename to pkg/policy/testdata/policies/allow-canonical/mapping.yaml diff --git a/pkg/policy/testdata/mock-tuf-allow/doi/policy.rego b/pkg/policy/testdata/policies/allow/doi/policy.rego similarity index 100% rename from pkg/policy/testdata/mock-tuf-allow/doi/policy.rego rename to pkg/policy/testdata/policies/allow/doi/policy.rego diff --git a/pkg/policy/testdata/mock-tuf-allow/mapping.yaml b/pkg/policy/testdata/policies/allow/mapping.yaml similarity index 100% rename from pkg/policy/testdata/mock-tuf-allow/mapping.yaml rename to pkg/policy/testdata/policies/allow/mapping.yaml diff --git a/pkg/policy/testdata/mock-tuf-deny/doi/policy.rego b/pkg/policy/testdata/policies/deny/doi/policy.rego similarity index 100% rename from pkg/policy/testdata/mock-tuf-deny/doi/policy.rego rename to pkg/policy/testdata/policies/deny/doi/policy.rego diff --git a/pkg/policy/testdata/mock-tuf-deny/mapping.yaml b/pkg/policy/testdata/policies/deny/mapping.yaml similarity index 100% rename from pkg/policy/testdata/mock-tuf-deny/mapping.yaml rename to pkg/policy/testdata/policies/deny/mapping.yaml diff --git a/pkg/policy/testdata/mock-tuf-no-rego/doi/policy.yaml b/pkg/policy/testdata/policies/no-rego/doi/policy.yaml similarity index 100% rename from pkg/policy/testdata/mock-tuf-no-rego/doi/policy.yaml rename to pkg/policy/testdata/policies/no-rego/doi/policy.yaml diff --git a/pkg/policy/testdata/mock-tuf-no-rego/mapping.yaml b/pkg/policy/testdata/policies/no-rego/mapping.yaml similarity index 100% rename from pkg/policy/testdata/mock-tuf-no-rego/mapping.yaml rename to pkg/policy/testdata/policies/no-rego/mapping.yaml diff --git a/pkg/policy/testdata/mock-tuf-verify-sig/doi/policy.rego b/pkg/policy/testdata/policies/verify-sig/doi/policy.rego similarity index 100% rename from pkg/policy/testdata/mock-tuf-verify-sig/doi/policy.rego rename to pkg/policy/testdata/policies/verify-sig/doi/policy.rego diff --git a/pkg/policy/testdata/mock-tuf-verify-sig/mapping.yaml b/pkg/policy/testdata/policies/verify-sig/mapping.yaml similarity index 100% rename from pkg/policy/testdata/mock-tuf-verify-sig/mapping.yaml rename to pkg/policy/testdata/policies/verify-sig/mapping.yaml diff --git a/pkg/policy/testdata/mock-tuf-wrong-key/doi/policy.rego b/pkg/policy/testdata/policies/wrong-key/doi/policy.rego similarity index 100% rename from pkg/policy/testdata/mock-tuf-wrong-key/doi/policy.rego rename to pkg/policy/testdata/policies/wrong-key/doi/policy.rego diff --git a/pkg/policy/testdata/mock-tuf-wrong-key/mapping.yaml b/pkg/policy/testdata/policies/wrong-key/mapping.yaml similarity index 100% rename from pkg/policy/testdata/mock-tuf-wrong-key/mapping.yaml rename to pkg/policy/testdata/policies/wrong-key/mapping.yaml diff --git a/pkg/policy/types.go b/pkg/policy/types.go index a122264e..bc949ecf 100644 --- a/pkg/policy/types.go +++ b/pkg/policy/types.go @@ -27,7 +27,8 @@ type Result struct { } type Options struct { - TUFClient tuf.Downloader + TUFClientOptions *tuf.ClientOptions + DisableTUF bool LocalTargetsDir string LocalPolicyDir string PolicyID string @@ -45,9 +46,13 @@ type Policy struct { } type Input struct { - Digest string `json:"digest"` - PURL string `json:"purl"` - IsCanonical bool `json:"isCanonical"` + Digest string `json:"digest"` + PURL string `json:"purl"` + Tag string `json:"tag,omitempty"` + Domain string `json:"domain"` + NormalizedName string `json:"normalized_name"` + FamiliarName string `json:"familiar_name"` + Platform string `json:"platform"` } type File struct { diff --git a/pkg/tuf/example_registry_test.go b/pkg/tuf/example_registry_test.go index b6e44495..e7efe003 100644 --- a/pkg/tuf/example_registry_test.go +++ b/pkg/tuf/example_registry_test.go @@ -4,7 +4,6 @@ import ( "os" "path/filepath" - "github.com/docker/attest/internal/embed" "github.com/docker/attest/pkg/tuf" "github.com/theupdateframework/go-tuf/v2/metadata" ) @@ -21,7 +20,7 @@ func ExampleNewClient_registry() { metadataURI := "registry-1.docker.io/docker/tuf-metadata:latest" targetsURI := "registry-1.docker.io/docker/tuf-targets" - registryClient, err := tuf.NewClient(embed.RootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker()) + registryClient, err := tuf.NewClient(&tuf.ClientOptions{tuf.DockerTUFRootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker()}) if err != nil { panic(err) } diff --git a/pkg/tuf/mock.go b/pkg/tuf/mock.go index 9e0f8707..ca742a88 100644 --- a/pkg/tuf/mock.go +++ b/pkg/tuf/mock.go @@ -1,67 +1,5 @@ package tuf -import ( - "io" - "os" - "path/filepath" - - "github.com/docker/attest/internal/util" -) - -type MockTufClient struct { - srcPath string - dstPath string -} - -func NewMockTufClient(srcPath string, dstPath string) *MockTufClient { - if srcPath == "" { - panic("srcPath must be set") - } - if dstPath == "" { - panic("dstPath must be set") - } - return &MockTufClient{ - srcPath: srcPath, - dstPath: dstPath, - } -} - -func (dc *MockTufClient) DownloadTarget(target string, filePath string) (file *TargetFile, err error) { - targetPath := filepath.Join(dc.srcPath, target) - src, err := os.Open(targetPath) - if err != nil { - return nil, err - } - defer src.Close() - - var dstFilePath string - if filePath == "" { - dstFilePath = filepath.Join(dc.dstPath, filepath.FromSlash(target)) - } else { - dstFilePath = filePath - } - - err = os.MkdirAll(filepath.Dir(dstFilePath), os.ModePerm) - if err != nil { - return nil, err - } - dst, err := os.Create(dstFilePath) - if err != nil { - return nil, err - } - defer dst.Close() - - // reading from tee will read from src and write to dst at the same time - tee := io.TeeReader(src, dst) - - b, err := io.ReadAll(tee) - if err != nil { - return nil, err - } - - return &TargetFile{ActualFilePath: dstFilePath, TargetURI: targetPath, Data: b, Digest: util.SHA256Hex(b)}, nil -} - type MockVersionChecker struct { err error } diff --git a/pkg/tuf/registry_test.go b/pkg/tuf/registry_test.go index 45ead27b..d648470f 100644 --- a/pkg/tuf/registry_test.go +++ b/pkg/tuf/registry_test.go @@ -9,7 +9,6 @@ import ( "strings" "testing" - "github.com/docker/attest/internal/embed" "github.com/docker/attest/internal/util" "github.com/docker/attest/pkg/oci" "github.com/google/go-containerregistry/pkg/crane" @@ -56,7 +55,7 @@ func TestRegistryFetcher(t *testing.T) { delegatedDir := CreateTempDir(t, dir, delegatedRole) delegatedTargetFile := fmt.Sprintf("%s/%s", delegatedRole, targetFile) - cfg, err := config.New(metadataRepo, embed.RootDev.Data) + cfg, err := config.New(metadataRepo, DockerTUFRootDev.Data) assert.NoError(t, err) cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataImgTag, targetsRepo) diff --git a/pkg/tuf/tuf.go b/pkg/tuf/tuf.go index cfcc9136..7a2b324d 100644 --- a/pkg/tuf/tuf.go +++ b/pkg/tuf/tuf.go @@ -35,6 +35,11 @@ var ( DockerTUFRootDefault = embed.RootDefault ) +const ( + defaultMetadataSource = "docker/tuf-metadata:latest" + defaultTargetsSource = "docker/tuf-targets" +) + type Downloader interface { DownloadTarget(target, filePath string) (file *TargetFile, err error) } @@ -51,19 +56,37 @@ type TargetFile struct { Data []byte } +type ClientOptions struct { + InitialRoot []byte + Path string + MetadataSource string + TargetsSource string + VersionChecker VersionChecker +} + +func NewDockerDefaultClientOptions(tufPath string) *ClientOptions { + return &ClientOptions{ + InitialRoot: DockerTUFRootDefault.Data, + Path: tufPath, + MetadataSource: defaultMetadataSource, + TargetsSource: defaultTargetsSource, + VersionChecker: NewDefaultVersionChecker(), + } +} + // NewClient creates a new TUF client. -func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string, versionChecker VersionChecker) (*Client, error) { +func NewClient(opts *ClientOptions) (*Client, error) { var tufSource Source - if strings.HasPrefix(metadataSource, "https://") || strings.HasPrefix(metadataSource, "http://") { + if strings.HasPrefix(opts.MetadataSource, "https://") || strings.HasPrefix(opts.MetadataSource, "http://") { tufSource = HTTPSource } else { tufSource = OCISource } - tufRootDigest := util.SHA256Hex(initialRoot) + tufRootDigest := util.SHA256Hex(opts.InitialRoot) // create a directory for each initial root.json - metadataPath := filepath.Join(tufPath, tufRootDigest) + metadataPath := filepath.Join(opts.Path, tufRootDigest) err := os.MkdirAll(metadataPath, os.ModePerm) if err != nil { return nil, fmt.Errorf("failed to create directory '%s': %w", metadataPath, err) @@ -76,29 +99,29 @@ func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string return nil, fmt.Errorf("failed to read root.json: %w", err) } // write the root.json file to the metadata directory - err = os.WriteFile(rootFile, initialRoot, 0o666) // #nosec G306 + err = os.WriteFile(rootFile, opts.InitialRoot, 0o666) // #nosec G306 if err != nil { return nil, fmt.Errorf("Failed to write root.json %w", err) } - rootBytes = initialRoot + rootBytes = opts.InitialRoot } // create updater configuration - cfg, err := config.New(metadataSource, rootBytes) // default config + cfg, err := config.New(opts.MetadataSource, rootBytes) // default config if err != nil { return nil, fmt.Errorf("failed to create TUF updater configuration: %w", err) } cfg.LocalMetadataDir = metadataPath cfg.LocalTargetsDir = filepath.Join(metadataPath, "download") - cfg.RemoteTargetsURL = targetsSource + cfg.RemoteTargetsURL = opts.TargetsSource if tufSource == OCISource { - metadataRepo, metadataTag, found := strings.Cut(metadataSource, ":") + metadataRepo, metadataTag, found := strings.Cut(opts.MetadataSource, ":") if !found { fmt.Printf("metadata tag not found in URL, using latest\n") metadataTag = LatestTag } - cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataTag, targetsSource) + cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataTag, opts.TargetsSource) } // create a new Updater instance @@ -118,7 +141,7 @@ func NewClient(initialRoot []byte, tufPath, metadataSource, targetsSource string cfg: cfg, } - err = versionChecker.CheckVersion(client) + err = opts.VersionChecker.CheckVersion(client) if err != nil { return nil, err } diff --git a/pkg/tuf/tuf_test.go b/pkg/tuf/tuf_test.go index 934f4dcb..6eaf2a14 100644 --- a/pkg/tuf/tuf_test.go +++ b/pkg/tuf/tuf_test.go @@ -9,8 +9,8 @@ import ( "path/filepath" "testing" - "github.com/docker/attest/internal/embed" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/theupdateframework/go-tuf/v2/metadata" ) @@ -65,18 +65,18 @@ func TestRootInit(t *testing.T) { } for _, tc := range testCases { - _, err := NewClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker) + _, err := NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker}) assert.NoErrorf(t, err, "Failed to create TUF client: %v", err) // recreation should work with same root - _, err = NewClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker) + _, err = NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker}) assert.NoErrorf(t, err, "Failed to recreate TUF client: %v", err) - _, err = NewClient([]byte("broken"), tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker) + _, err = NewClient(&ClientOptions{[]byte("broken"), tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker}) assert.Errorf(t, err, "Expected error recreating TUF client with broken root: %v", err) - _, err = NewClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysBadVersionChecker) - assert.Errorf(t, err, "Expected error creating TUF client with bad attest version: %v", err) + _, err = NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysBadVersionChecker}) + assert.Errorf(t, err, "Expected error recreating TUF client with bad version checker") } } @@ -108,11 +108,13 @@ func TestDownloadTarget(t *testing.T) { }{ {"http", server.URL + "/metadata", server.URL + "/targets"}, {"oci", regAddr.Host + "/tuf-metadata:latest", regAddr.Host + "/tuf-targets"}, + {"http, download before init", server.URL + "/metadata", server.URL + "/targets"}, } for _, tc := range testCases { - tufClient, err := NewClient(embed.RootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker) - assert.NoErrorf(t, err, "Failed to create TUF client: %v", err) + tufClient, err := NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker}) + require.NoErrorf(t, err, "Failed to create TUF client: %v", err) + require.NotNil(t, tufClient.updater, "Failed to create updater") // get trusted tuf metadata trustedMetadata := tufClient.updater.GetTrustedMetadataSet() diff --git a/test/testdata/local-policy-inputs/doi/policy.rego b/test/testdata/local-policy-inputs/doi/policy.rego new file mode 100644 index 00000000..0507909b --- /dev/null +++ b/test/testdata/local-policy-inputs/doi/policy.rego @@ -0,0 +1,43 @@ +package attest + +import rego.v1 + +keys := [{ + "id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4", + "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgH23D1i2+ZIOtVjmfB7iFvX8AhVN\n9CPJ4ie9axw+WRHozGnRy99U2dRge3zueBBg2MweF0zrToXGig2v3YOrdw==\n-----END PUBLIC KEY-----", + "from": "2023-12-15T14:00:00Z", + "to": null, + "status": "active", + "signing-format": "dssev1", +}] + +provs(pred) := p if { + res := attest.fetch(pred) + not res.error + p := res.value +} + +atts := union({ + provs("https://slsa.dev/provenance/v0.2"), + provs("https://spdx.dev/Document"), +}) + +success if { + input.domain == "docker.io" + input.familiar_name == "test-image" + input.normalized_name == "library/test-image" + input.platform == "linux/amd64" + input.tag == "test" +} + +result := { + "success": success, + "violations": set(), + "attestations": set(), + "summary": { + "subjects": set(), + "slsa_level": "SLSA_BUILD_LEVEL_3", + "verifier": "docker-official-images", + "policy_uri": "https://docker.com/official/policy/v0.1", + }, +} diff --git a/test/testdata/local-policy-inputs/mapping.yaml b/test/testdata/local-policy-inputs/mapping.yaml new file mode 100644 index 00000000..1c3bdbe0 --- /dev/null +++ b/test/testdata/local-policy-inputs/mapping.yaml @@ -0,0 +1,18 @@ +version: v1 +kind: policy-mapping +policies: + - id: docker-official-images + description: Docker Official Images + files: + - path: doi/policy.rego +rules: + - pattern: "^docker[.]io/library/(.*)$" + policy-id: docker-official-images + - pattern: "repo$" + policy-id: docker-official-images + - pattern: "test-image$" + policy-id: docker-official-images + - pattern: "image-signer-verifier-test$" + policy-id: docker-official-images + - pattern: "library/(.*)$" + rewrite: docker.io/library/$1 diff --git a/test/testdata/local-policy-mirror/policy.rego b/test/testdata/local-policy-mirror/policy.rego index 2922834d..6e36fc84 100644 --- a/test/testdata/local-policy-mirror/policy.rego +++ b/test/testdata/local-policy-mirror/policy.rego @@ -38,7 +38,7 @@ subjects contains subject if { } success if { - print("input:",input) + # print("input:",input) true } diff --git a/test/testdata/local-policy-no-policies/mapping.yaml b/test/testdata/local-policy-no-policies/mapping.yaml new file mode 100644 index 00000000..39405b7f --- /dev/null +++ b/test/testdata/local-policy-no-policies/mapping.yaml @@ -0,0 +1,4 @@ +version: v1 +kind: policy-mapping +policies: +rules: