From 50f9fdca250c76dc992f4a3a03295e85b177d61a Mon Sep 17 00:00:00 2001 From: akashsinghal Date: Thu, 18 Apr 2024 18:52:32 +0000 Subject: [PATCH 1/7] feat: add cosign trust policies --- Makefile | 36 +- charts/ratify/README.md | 4 +- charts/ratify/templates/deployment.yaml | 4 +- charts/ratify/templates/secret.yaml | 2 +- charts/ratify/templates/verifier.yaml | 37 +- charts/ratify/values.yaml | 3 +- .../config_v1beta1_verifier_cosign.yaml | 7 +- .../config_v1beta_verifier_cosign_legacy.yaml | 9 + go.mod | 1 + go.sum | 7 +- .../keymanagementprovider_controller.go | 2 +- .../azurekeyvault/provider.go | 44 +- .../keymanagementprovider.go | 35 +- .../keymanagementprovider_test.go | 18 +- pkg/referrerstore/mocks/memory_store.go | 4 +- pkg/verifier/cosign/cosign.go | 252 ++++++- pkg/verifier/cosign/cosign_test.go | 632 +++++++++++++++++- pkg/verifier/cosign/trustpolicies.go | 162 +++++ pkg/verifier/cosign/trustpolicies_test.go | 449 +++++++++++++ pkg/verifier/cosign/trustpolicy.go | 273 ++++++++ pkg/verifier/cosign/trustpolicy_test.go | 361 ++++++++++ pkg/verifier/notation/notation.go | 8 +- pkg/verifier/utils/utils.go | 27 + scripts/azure-ci-test.sh | 16 +- scripts/create-azure-resources.sh | 2 +- test/bats/azure-test.bats | 2 - test/bats/base-test.bats | 63 ++ test/bats/plugin-test.bats | 38 -- utils/utils.go | 5 + 29 files changed, 2366 insertions(+), 137 deletions(-) create mode 100644 config/samples/config_v1beta_verifier_cosign_legacy.yaml create mode 100644 pkg/verifier/cosign/trustpolicies.go create mode 100644 pkg/verifier/cosign/trustpolicies_test.go create mode 100644 pkg/verifier/cosign/trustpolicy.go create mode 100644 pkg/verifier/cosign/trustpolicy_test.go create mode 100644 pkg/verifier/utils/utils.go diff --git a/Makefile b/Makefile index 923edb697..b1a3ac0bc 100644 --- a/Makefile +++ b/Makefile @@ -62,6 +62,10 @@ TEST_REGISTRY = localhost:5000 TEST_REGISTRY_USERNAME = test_user TEST_REGISTRY_PASSWORD = test_pw +# Azure Key Vault Setup +KEYVAULT_NAME ?= ratify-akv +KEYVAULT_KEY_NAME ?= test-key + all: build test .PHONY: build @@ -340,6 +344,32 @@ e2e-cosign-setup: ./cosign-linux-amd64 sign --allow-insecure-registry --allow-http-registry --tlog-upload=false --key cosign.key ${TEST_REGISTRY}/cosign@`${GITHUB_WORKSPACE}/bin/oras manifest fetch ${TEST_REGISTRY}/cosign:signed-key --descriptor | jq .digest | xargs` && \ ./cosign-linux-amd64 sign --allow-insecure-registry --allow-http-registry --tlog-upload=false --key cosign.key ${TEST_REGISTRY}/all@`${GITHUB_WORKSPACE}/bin/oras manifest fetch ${TEST_REGISTRY}/all:v0 --descriptor | jq .digest | xargs` +e2e-cosign-akv-setup: + rm -rf .staging/cosign + mkdir -p .staging/cosign + curl -sSLO https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-amd64 + mv cosign-linux-amd64 .staging/cosign + chmod +x .staging/cosign/cosign-linux-amd64 + + # image signed with a key from azure key vault + printf 'FROM ${ALPINE_IMAGE}\nCMD ["echo", "cosign signed akv image"]' > .staging/cosign/Dockerfile + docker buildx create --use + docker buildx build --output type=oci,dest=.staging/cosign/cosign.tar -t cosign:v0 .staging/cosign + ${GITHUB_WORKSPACE}/bin/oras cp --from-oci-layout .staging/cosign/cosign.tar:v0 ${TEST_REGISTRY}/cosign:signed-key + rm .staging/cosign/cosign.tar + + printf 'FROM ${ALPINE_IMAGE}\nCMD ["echo", "cosign unsigned image"]' > .staging/cosign/Dockerfile + docker buildx create --use + docker buildx build --output type=oci,dest=.staging/cosign/cosign.tar -t cosign:v0 .staging/cosign + ${GITHUB_WORKSPACE}/bin/oras cp --from-oci-layout .staging/cosign/cosign.tar:v0 ${TEST_REGISTRY}/cosign:unsigned + rm .staging/cosign/cosign.tar + + export COSIGN_PASSWORD="test" && \ + cd .staging/cosign && \ + ./cosign-linux-amd64 login ${TEST_REGISTRY} -u ${TEST_REGISTRY_USERNAME} -p ${TEST_REGISTRY_PASSWORD} && \ + ./cosign-linux-amd64 sign --allow-insecure-registry --allow-http-registry --tlog-upload=false --key azurekms://${KEYVAULT_NAME}.vault.azure.net/${KEYVAULT_KEY_NAME} ${TEST_REGISTRY}/cosign@`${GITHUB_WORKSPACE}/bin/oras manifest fetch ${TEST_REGISTRY}/cosign:signed-key --descriptor | jq .digest | xargs` && \ + ./cosign-linux-amd64 sign --allow-insecure-registry --allow-http-registry --tlog-upload=false --key azurekms://${KEYVAULT_NAME}.vault.azure.net/${KEYVAULT_KEY_NAME} ${TEST_REGISTRY}/all@`${GITHUB_WORKSPACE}/bin/oras manifest fetch ${TEST_REGISTRY}/all:v0 --descriptor | jq .digest | xargs` + e2e-licensechecker-setup: rm -rf .staging/licensechecker mkdir -p .staging/licensechecker @@ -484,7 +514,7 @@ e2e-inlinecert-setup: .staging/notation/notation cert generate-test "alternate-cert" NOTATION_EXPERIMENTAL=1 .staging/notation/notation sign -u ${TEST_REGISTRY_USERNAME} -p ${TEST_REGISTRY_PASSWORD} --key "alternate-cert" ${TEST_REGISTRY}/notation@`${GITHUB_WORKSPACE}/bin/oras manifest fetch ${TEST_REGISTRY}/notation:signed-alternate --descriptor | jq .digest | xargs` -e2e-azure-setup: e2e-create-all-image e2e-notation-setup e2e-notation-leaf-cert-setup e2e-cosign-setup e2e-licensechecker-setup e2e-sbom-setup e2e-schemavalidator-setup +e2e-azure-setup: e2e-create-all-image e2e-notation-setup e2e-notation-leaf-cert-setup e2e-cosign-akv-setup e2e-licensechecker-setup e2e-sbom-setup e2e-schemavalidator-setup e2e-deploy-gatekeeper: e2e-helm-install ./.staging/helm/linux-amd64/helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts @@ -556,7 +586,7 @@ e2e-helm-deploy-ratify: --set-file provider.tls.caKey=${CERT_DIR}/ca.key \ --set provider.tls.cabundle="$(shell cat ${CERT_DIR}/ca.crt | base64 | tr -d '\n')" \ --set notationCerts[0]="$$(cat ~/.config/notation/localkeys/ratify-bats-test.crt)" \ - --set cosign.key="$$(cat .staging/cosign/cosign.pub)" \ + --set cosignKeys[0]="$$(cat .staging/cosign/cosign.pub)" \ --set oras.useHttp=true \ --set-file dockerConfig="mount_config.json" \ --set logger.level=debug @@ -574,7 +604,7 @@ e2e-helm-deploy-ratify-without-tls-certs: --set gatekeeper.version=${GATEKEEPER_VERSION} \ --set featureFlags.RATIFY_CERT_ROTATION=${CERT_ROTATION_ENABLED} \ --set notaryCert="$$(cat ~/.config/notation/localkeys/ratify-bats-test.crt)" \ - --set cosign.key="$$(cat .staging/cosign/cosign.pub)" \ + --set cosignKeys[0]="$$(cat .staging/cosign/cosign.pub)" \ --set oras.useHttp=true \ --set-file dockerConfig="mount_config.json" \ --set logger.level=debug diff --git a/charts/ratify/README.md b/charts/ratify/README.md index eafe93360..60fa92242 100644 --- a/charts/ratify/README.md +++ b/charts/ratify/README.md @@ -48,8 +48,9 @@ Values marked `# DEPRECATED` in the `values.yaml` as well as **DEPRECATED** in t | affinity | Pod affinity for the Ratify deployment | `{}` | | tolerations | Pod tolerations for the Ratify deployment | `[]` | | notationCerts | An array of public certificate/certificate chain used to create inline certstore used by Notation verifier | `` | +| cosignKeys | An aray of public keys used to create inline key management providers used by Cosign verifier | `[]` | | cosign.enabled | Enables/disables cosign tag-based signature lookup in ORAS store. MUST be set to true for cosign verification. | `true` | -| cosign.key | Public certificate used by cosign verifier | `` | +| cosign.scopes | An array of scopes relevant to the single trust policy configured in Cosign verifier. A scope of '*' is a global wildcard character to represent all images apply. | `["*"]` | | vulnerabilityreport.enabled | Enables/disables installation of vulnerability report verifier | `false` | | vulnerabilityreport.passthrough | Enables/disables passthrough. All validation except `maximumAge` are disregarded and report content is added to verifier report | `false` | | vulnerabilityreport.schemaURL | URL for JSON schema to validate report against | `` | @@ -137,3 +138,4 @@ Values marked `# DEPRECATED` in the `values.yaml` as well as **DEPRECATED** in t | akvCertConfig.cert2Version | **DEPRECATED** Please use `azurekeyvault.certificates` instead. Exact version of certificate to use from AKV. | `` | | akvCertConfig.certificates | **DEPRECATED** Please use `azurekeyvault.certificates` instead. An array of certificate objects identified by `name` and `version` stored in AKV | `` | | akvCertConfig.tenantId | **DEPRECATED** Please use `azurekeyvault.certificates` instead. TenantID of the configured AKV resource | `` | +| cosign.key | **DEPRECATED** Please use `cosignKeys` instead. Public key used by cosign verifier | `` | diff --git a/charts/ratify/templates/deployment.yaml b/charts/ratify/templates/deployment.yaml index 912eda48a..7a979ca43 100644 --- a/charts/ratify/templates/deployment.yaml +++ b/charts/ratify/templates/deployment.yaml @@ -88,7 +88,7 @@ spec: name: healthz protocol: TCP volumeMounts: - {{- if .Values.cosign.enabled }} + {{- if and .Values.cosign.enabled .Values.cosign.key }} - mountPath: "/usr/local/ratify-certs/cosign" name: cosign-certs readOnly: true @@ -145,7 +145,7 @@ spec: resources: {{- toYaml .Values.resources | nindent 12 }} volumes: - {{- if .Values.cosign.enabled }} + {{- if and .Values.cosign.enabled .Values.cosign.key }} - name: cosign-certs secret: secretName: {{ include "ratify.fullname" . }}-cosign-certificate diff --git a/charts/ratify/templates/secret.yaml b/charts/ratify/templates/secret.yaml index b74390f49..5e8173bfa 100644 --- a/charts/ratify/templates/secret.yaml +++ b/charts/ratify/templates/secret.yaml @@ -1,4 +1,4 @@ -{{- if .Values.cosign.enabled }} +{{- if and .Values.cosign.enabled .Values.cosign.key}} apiVersion: v1 kind: Secret metadata: diff --git a/charts/ratify/templates/verifier.yaml b/charts/ratify/templates/verifier.yaml index 6f3c78378..a1fd42319 100644 --- a/charts/ratify/templates/verifier.yaml +++ b/charts/ratify/templates/verifier.yaml @@ -1,5 +1,4 @@ {{- $fullname := include "ratify.fullname" . -}} ---- apiVersion: config.ratify.deislabs.io/v1beta1 kind: Verifier metadata: @@ -16,17 +15,16 @@ spec: certs: {{- if or .Values.azurekeyvault.enabled .Values.akvCertConfig.enabled }} - kmprovider-akv - {{- else }} - {{- if .Values.notationCert }} - {{- if .Values.notationCerts }} - {{- fail "Please specify notation certs with .Values.notationCerts, single certificate .Values.notationCert has been deprecated, will soon be removed." }} - {{- end }} + {{- end }} + {{- if .Values.notationCert }} + {{- if .Values.notationCerts }} + {{- fail "Please specify notation certs with .Values.notationCerts, single certificate .Values.notationCert has been deprecated, will soon be removed." }} + {{- end }} - {{$fullname}}-notation-inline-cert - {{- end }} - {{- range $i, $cert := .Values.notationCerts }} + {{- end }} + {{- range $i, $cert := .Values.notationCerts }} - {{$fullname}}-notation-inline-cert-{{$i}} - {{- end }} - {{- end }} + {{- end }} trustPolicyDoc: version: "1.0" trustPolicies: @@ -50,10 +48,25 @@ metadata: helm.sh/hook-weight: "5" spec: name: cosign - version: 1.0.0 artifactTypes: application/vnd.dev.cosign.artifact.sig.v1+json parameters: - key: /usr/local/ratify-certs/cosign/cosign.pub + {{- if .Values.cosign.key }} + key: {{ .Values.cosign.key | quote }} + {{- else }} + trustPolicies: + - name: default + scopes: + {{- range $i, $scope := .Values.cosign.scopes }} + - "{{$scope}}" + {{- end }} + keys: + {{- range $i, $key := .Values.cosignKeys }} + - provider: {{$fullname}}-cosign-inline-key-{{$i}} + {{- end }} + {{- if and .Values.azurekeyvault.enabled (gt (len .Values.azurekeyvault.keys) 0) }} + - provider: kmprovider-akv + {{- end }} + {{- end }} {{- end }} --- {{- if .Values.vulnerabilityreport.enabled }} diff --git a/charts/ratify/values.yaml b/charts/ratify/values.yaml index 315dbf509..f25a58b7e 100644 --- a/charts/ratify/values.yaml +++ b/charts/ratify/values.yaml @@ -14,7 +14,8 @@ cosignKeys: [] cosign: enabled: true - key: "" + scopes: ["*"] # corresponds to a single trust policy + key: "" # DEPRECATED: Use cosignKeys instead vulnerabilityreport: enabled: false passthrough: false diff --git a/config/samples/config_v1beta1_verifier_cosign.yaml b/config/samples/config_v1beta1_verifier_cosign.yaml index 70cb12aa1..713f5713e 100644 --- a/config/samples/config_v1beta1_verifier_cosign.yaml +++ b/config/samples/config_v1beta1_verifier_cosign.yaml @@ -6,4 +6,9 @@ spec: name: cosign artifactTypes: application/vnd.dev.cosign.artifact.sig.v1+json parameters: - key: /usr/local/ratify-certs/cosign/cosign.pub \ No newline at end of file + trustPolicies: + - name: default + scopes: + - "*" + keys: + - provider: ratify-cosign-inline-key-0 \ No newline at end of file diff --git a/config/samples/config_v1beta_verifier_cosign_legacy.yaml b/config/samples/config_v1beta_verifier_cosign_legacy.yaml new file mode 100644 index 000000000..70cb12aa1 --- /dev/null +++ b/config/samples/config_v1beta_verifier_cosign_legacy.yaml @@ -0,0 +1,9 @@ +apiVersion: config.ratify.deislabs.io/v1beta1 +kind: Verifier +metadata: + name: verifier-cosign +spec: + name: cosign + artifactTypes: application/vnd.dev.cosign.artifact.sig.v1+json + parameters: + key: /usr/local/ratify-certs/cosign/cosign.pub \ No newline at end of file diff --git a/go.mod b/go.mod index c5df24c68..62c9924b6 100644 --- a/go.mod +++ b/go.mod @@ -122,6 +122,7 @@ require ( go.step.sm/crypto v0.44.2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect + gotest.tools/v3 v3.1.0 // indirect sigs.k8s.io/release-utils v0.7.7 // indirect ) diff --git a/go.sum b/go.sum index 4c1f69712..7ac1a760c 100644 --- a/go.sum +++ b/go.sum @@ -720,6 +720,7 @@ github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= @@ -998,6 +999,7 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1096,6 +1098,7 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= @@ -1220,8 +1223,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= +gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/controllers/keymanagementprovider_controller.go b/pkg/controllers/keymanagementprovider_controller.go index 787ca1fb4..eaf9af2cc 100644 --- a/pkg/controllers/keymanagementprovider_controller.go +++ b/pkg/controllers/keymanagementprovider_controller.go @@ -104,7 +104,7 @@ func (r *KeyManagementProviderReconciler) Reconcile(ctx context.Context, req ctr return ctrl.Result{}, fmt.Errorf("Error fetching keys in KMProvider %v with %v provider, error: %w", resource, keyManagementProvider.Spec.Type, err) } keymanagementprovider.SetCertificatesInMap(resource, certificates) - keymanagementprovider.SetKeysInMap(resource, keys) + keymanagementprovider.SetKeysInMap(resource, keyManagementProvider.Spec.Type, keys) // merge certificates and keys status into one maps.Copy(keyAttributes, certAttributes) isFetchSuccessful = true diff --git a/pkg/keymanagementprovider/azurekeyvault/provider.go b/pkg/keymanagementprovider/azurekeyvault/provider.go index ebde84a50..c207a8679 100644 --- a/pkg/keymanagementprovider/azurekeyvault/provider.go +++ b/pkg/keymanagementprovider/azurekeyvault/provider.go @@ -43,7 +43,7 @@ import ( ) const ( - providerName string = "azurekeyvault" + ProviderName string = "azurekeyvault" PKCS12ContentType string = "application/x-pkcs12" PEMContentType string = "application/x-pem-file" ) @@ -81,7 +81,7 @@ var initKVClient = initializeKvClient // init calls to register the provider func init() { - factory.Register(providerName, &akvKMProviderFactory{}) + factory.Register(ProviderName, &akvKMProviderFactory{}) } // Create creates a new instance of the provider after marshalling and validating the configuration @@ -99,15 +99,15 @@ func (f *akvKMProviderFactory) Create(_ string, keyManagementProviderConfig conf azureCloudEnv, err := parseAzureEnvironment(conf.CloudName) if err != nil { - return nil, re.ErrorCodeConfigInvalid.NewError(re.KeyManagementProvider, providerName, re.EmptyLink, nil, fmt.Sprintf("cloudName %s is not valid", conf.CloudName), re.HideStackTrace) + return nil, re.ErrorCodeConfigInvalid.NewError(re.KeyManagementProvider, ProviderName, re.EmptyLink, nil, fmt.Sprintf("cloudName %s is not valid", conf.CloudName), re.HideStackTrace) } if len(conf.Certificates) == 0 && len(conf.Keys) == 0 { - return nil, re.ErrorCodeConfigInvalid.NewError(re.KeyManagementProvider, providerName, re.EmptyLink, nil, "no keyvault certificates or keys configured", re.HideStackTrace) + return nil, re.ErrorCodeConfigInvalid.NewError(re.KeyManagementProvider, ProviderName, re.EmptyLink, nil, "no keyvault certificates or keys configured", re.HideStackTrace) } provider := &akvKMProvider{ - provider: providerName, + provider: ProviderName, vaultURI: strings.TrimSpace(conf.VaultURI), tenantID: strings.TrimSpace(conf.TenantID), clientID: strings.TrimSpace(conf.ClientID), @@ -124,7 +124,7 @@ func (f *akvKMProviderFactory) Create(_ string, keyManagementProviderConfig conf kvClient, err := initKVClient(context.Background(), provider.cloudEnv.KeyVaultEndpoint, provider.tenantID, provider.clientID) if err != nil { - return nil, re.ErrorCodePluginInitFailure.NewError(re.KeyManagementProvider, providerName, re.AKVLink, err, "failed to create keyvault client", re.HideStackTrace) + return nil, re.ErrorCodePluginInitFailure.NewError(re.KeyManagementProvider, ProviderName, re.AKVLink, err, "failed to create keyvault client", re.HideStackTrace) } provider.kvClient = kvClient @@ -223,12 +223,12 @@ func initializeKvClient(ctx context.Context, keyVaultEndpoint, tenantID, clientI err := kvClient.AddToUserAgent("ratify") if err != nil { - return nil, re.ErrorCodeConfigInvalid.NewError(re.KeyManagementProvider, providerName, re.AKVLink, err, "failed to add user agent to keyvault client", re.PrintStackTrace) + return nil, re.ErrorCodeConfigInvalid.NewError(re.KeyManagementProvider, ProviderName, re.AKVLink, err, "failed to add user agent to keyvault client", re.PrintStackTrace) } kvClient.Authorizer, err = getAuthorizerForWorkloadIdentity(ctx, tenantID, clientID, kvEndpoint) if err != nil { - return nil, re.ErrorCodeAuthDenied.NewError(re.KeyManagementProvider, providerName, re.AKVLink, err, "failed to get authorizer for keyvault client", re.PrintStackTrace) + return nil, re.ErrorCodeAuthDenied.NewError(re.KeyManagementProvider, ProviderName, re.AKVLink, err, "failed to get authorizer for keyvault client", re.PrintStackTrace) } return &kvClient, nil } @@ -237,7 +237,7 @@ func initializeKvClient(ctx context.Context, keyVaultEndpoint, tenantID, clientI // In a certificate chain scenario, all certificates from root to leaf will be returned func getCertsFromSecretBundle(ctx context.Context, secretBundle kv.SecretBundle, certName string) ([]*x509.Certificate, []map[string]string, error) { if secretBundle.ContentType == nil || secretBundle.Value == nil || secretBundle.ID == nil { - return nil, nil, re.ErrorCodeCertInvalid.NewError(re.KeyManagementProvider, providerName, re.EmptyLink, nil, "found invalid secret bundle for certificate %s, contentType, value, and id must not be nil", re.HideStackTrace) + return nil, nil, re.ErrorCodeCertInvalid.NewError(re.KeyManagementProvider, ProviderName, re.EmptyLink, nil, "found invalid secret bundle for certificate %s, contentType, value, and id must not be nil", re.HideStackTrace) } version := getObjectVersion(*secretBundle.ID) @@ -246,7 +246,7 @@ func getCertsFromSecretBundle(ctx context.Context, secretBundle kv.SecretBundle, // akv plugin supports both PKCS12 and PEM. https://github.com/Azure/notation-azure-kv/blob/558e7345ef8318783530de6a7a0a8420b9214ba8/Notation.Plugin.AzureKeyVault/KeyVault/KeyVaultClient.cs#L192 if *secretBundle.ContentType != PKCS12ContentType && *secretBundle.ContentType != PEMContentType { - return nil, nil, re.ErrorCodeCertInvalid.NewError(re.KeyManagementProvider, providerName, re.EmptyLink, nil, fmt.Sprintf("certificate %s version %s, unsupported secret content type %s, supported type are %s and %s", certName, version, *secretBundle.ContentType, PKCS12ContentType, PEMContentType), re.HideStackTrace) + return nil, nil, re.ErrorCodeCertInvalid.NewError(re.KeyManagementProvider, ProviderName, re.EmptyLink, nil, fmt.Sprintf("certificate %s version %s, unsupported secret content type %s, supported type are %s and %s", certName, version, *secretBundle.ContentType, PKCS12ContentType, PEMContentType), re.HideStackTrace) } results := []*x509.Certificate{} @@ -258,12 +258,12 @@ func getCertsFromSecretBundle(ctx context.Context, secretBundle kv.SecretBundle, if *secretBundle.ContentType == PKCS12ContentType { p12, err := base64.StdEncoding.DecodeString(*secretBundle.Value) if err != nil { - return nil, nil, re.ErrorCodeCertInvalid.NewError(re.KeyManagementProvider, providerName, re.EmptyLink, err, fmt.Sprintf("azure keyvault key management provider: failed to decode PKCS12 Value. Certificate %s, version %s", certName, version), re.HideStackTrace) + return nil, nil, re.ErrorCodeCertInvalid.NewError(re.KeyManagementProvider, ProviderName, re.EmptyLink, err, fmt.Sprintf("azure keyvault key management provider: failed to decode PKCS12 Value. Certificate %s, version %s", certName, version), re.HideStackTrace) } blocks, err := pkcs12.ToPEM(p12, "") if err != nil { - return nil, nil, re.ErrorCodeCertInvalid.NewError(re.KeyManagementProvider, providerName, re.EmptyLink, err, fmt.Sprintf("azure keyvault key management provider: failed to convert PKCS12 Value to PEM. Certificate %s, version %s", certName, version), re.HideStackTrace) + return nil, nil, re.ErrorCodeCertInvalid.NewError(re.KeyManagementProvider, ProviderName, re.EmptyLink, err, fmt.Sprintf("azure keyvault key management provider: failed to convert PKCS12 Value to PEM. Certificate %s, version %s", certName, version), re.HideStackTrace) } var pemData []byte @@ -284,7 +284,7 @@ func getCertsFromSecretBundle(ctx context.Context, secretBundle kv.SecretBundle, pemData = append(pemData, pem.EncodeToMemory(block)...) decodedCerts, err := keymanagementprovider.DecodeCertificates(pemData) if err != nil { - return nil, nil, re.ErrorCodeCertInvalid.NewError(re.KeyManagementProvider, providerName, re.EmptyLink, err, fmt.Sprintf("azure keyvault key management provider: failed to decode Certificate %s, version %s", certName, version), re.HideStackTrace) + return nil, nil, re.ErrorCodeCertInvalid.NewError(re.KeyManagementProvider, ProviderName, re.EmptyLink, err, fmt.Sprintf("azure keyvault key management provider: failed to decode Certificate %s, version %s", certName, version), re.HideStackTrace) } for _, cert := range decodedCerts { results = append(results, cert) @@ -297,7 +297,7 @@ func getCertsFromSecretBundle(ctx context.Context, secretBundle kv.SecretBundle, block, rest = pem.Decode(rest) if block == nil && len(rest) > 0 { - return nil, nil, re.ErrorCodeCertInvalid.NewError(re.KeyManagementProvider, providerName, re.EmptyLink, nil, fmt.Sprintf("certificate '%s', version '%s': azure keyvault key management provider error, block is nil and remaining block to parse > 0", certName, version), re.HideStackTrace) + return nil, nil, re.ErrorCodeCertInvalid.NewError(re.KeyManagementProvider, ProviderName, re.EmptyLink, nil, fmt.Sprintf("certificate '%s', version '%s': azure keyvault key management provider error, block is nil and remaining block to parse > 0", certName, version), re.HideStackTrace) } } logger.GetLogger(ctx, logOpt).Debugf("azurekeyvault certprovider getCertsFromSecretBundle: %v certificates parsed, Certificate '%s', version '%s'", len(results), certName, version) @@ -308,7 +308,7 @@ func getCertsFromSecretBundle(ctx context.Context, secretBundle kv.SecretBundle, func getKeyFromKeyBundle(keyBundle kv.KeyBundle) (crypto.PublicKey, error) { webKey := keyBundle.Key if webKey == nil { - return nil, re.ErrorCodeKeyInvalid.NewError(re.KeyManagementProvider, providerName, re.EmptyLink, nil, "found invalid key bundle, key must not be nil", re.HideStackTrace) + return nil, re.ErrorCodeKeyInvalid.NewError(re.KeyManagementProvider, ProviderName, re.EmptyLink, nil, "found invalid key bundle, key must not be nil", re.HideStackTrace) } keyType := webKey.Kty @@ -321,13 +321,13 @@ func getKeyFromKeyBundle(keyBundle kv.KeyBundle) (crypto.PublicKey, error) { keyBytes, err := json.Marshal(webKey) if err != nil { - return nil, re.ErrorCodeKeyInvalid.NewError(re.KeyManagementProvider, providerName, re.EmptyLink, err, "failed to marshal key", re.HideStackTrace) + return nil, re.ErrorCodeKeyInvalid.NewError(re.KeyManagementProvider, ProviderName, re.EmptyLink, err, "failed to marshal key", re.HideStackTrace) } key := jose.JSONWebKey{} err = key.UnmarshalJSON(keyBytes) if err != nil { - return nil, re.ErrorCodeKeyInvalid.NewError(re.KeyManagementProvider, providerName, re.EmptyLink, err, "failed to unmarshal key into JSON Web Key", re.HideStackTrace) + return nil, re.ErrorCodeKeyInvalid.NewError(re.KeyManagementProvider, ProviderName, re.EmptyLink, err, "failed to unmarshal key into JSON Web Key", re.HideStackTrace) } return key.Key, nil @@ -346,26 +346,26 @@ func getObjectVersion(id string) string { // validate checks vaultURI, tenantID, clientID are set and all certificates/keys have a name func (s *akvKMProvider) validate() error { if s.vaultURI == "" { - return re.ErrorCodeConfigInvalid.NewError(re.KeyManagementProvider, providerName, re.EmptyLink, nil, "vaultURI is not set", re.HideStackTrace) + return re.ErrorCodeConfigInvalid.NewError(re.KeyManagementProvider, ProviderName, re.EmptyLink, nil, "vaultURI is not set", re.HideStackTrace) } if s.tenantID == "" { - return re.ErrorCodeConfigInvalid.NewError(re.KeyManagementProvider, providerName, re.EmptyLink, nil, "tenantID is not set", re.HideStackTrace) + return re.ErrorCodeConfigInvalid.NewError(re.KeyManagementProvider, ProviderName, re.EmptyLink, nil, "tenantID is not set", re.HideStackTrace) } if s.clientID == "" { - return re.ErrorCodeConfigInvalid.NewError(re.KeyManagementProvider, providerName, re.EmptyLink, nil, "clientID is not set", re.HideStackTrace) + return re.ErrorCodeConfigInvalid.NewError(re.KeyManagementProvider, ProviderName, re.EmptyLink, nil, "clientID is not set", re.HideStackTrace) } // all certificates must have a name for i := range s.certificates { if s.certificates[i].Name == "" { - return re.ErrorCodeConfigInvalid.NewError(re.KeyManagementProvider, providerName, re.EmptyLink, nil, fmt.Sprintf("name is not set for the %d th certificate", i+1), re.HideStackTrace) + return re.ErrorCodeConfigInvalid.NewError(re.KeyManagementProvider, ProviderName, re.EmptyLink, nil, fmt.Sprintf("name is not set for the %d th certificate", i+1), re.HideStackTrace) } } // all keys must have a name for i := range s.keys { if s.keys[i].Name == "" { - return re.ErrorCodeConfigInvalid.NewError(re.KeyManagementProvider, providerName, re.EmptyLink, nil, fmt.Sprintf("name is not set for the %d th key", i+1), re.HideStackTrace) + return re.ErrorCodeConfigInvalid.NewError(re.KeyManagementProvider, ProviderName, re.EmptyLink, nil, fmt.Sprintf("name is not set for the %d th key", i+1), re.HideStackTrace) } } diff --git a/pkg/keymanagementprovider/keymanagementprovider.go b/pkg/keymanagementprovider/keymanagementprovider.go index b0731f342..d1267ca0e 100644 --- a/pkg/keymanagementprovider/keymanagementprovider.go +++ b/pkg/keymanagementprovider/keymanagementprovider.go @@ -38,6 +38,11 @@ type KMPMapKey struct { Version string } +type PublicKey struct { + Key crypto.PublicKey + ProviderType string +} + // KeyManagementProvider is an interface that defines methods to be implemented by a each key management provider provider type KeyManagementProvider interface { // Returns an array of certificates and the provider specific cert attributes @@ -57,8 +62,9 @@ var certificatesMap sync.Map // static concurrency-safe map to store keys fetched from key management provider // layout: // -// map["/"] = map[KMPMapKey]PublicKey -// where KMPMapKey is dimensioned by the name and version of the public key. +// map["/"] = map[KMPMapKey]PublicKey +// where KMPMapKey is dimensioned by the name and version of the public key +// where PublicKey is a struct containing the public key and the provider type var keyMap sync.Map // DecodeCertificates decodes PEM-encoded bytes into an x509.Certificate chain. @@ -127,27 +133,22 @@ func FlattenKMPMap(certMap map[KMPMapKey][]*x509.Certificate) []*x509.Certificat return items } -// FlattenKMPMapKeys flattens the map of keys fetched for a single key management provider resource and returns a single array -func FlattenKMPMapKeys(keyMap map[KMPMapKey]crypto.PublicKey) []crypto.PublicKey { - items := []crypto.PublicKey{} - for _, val := range keyMap { - items = append(items, val) - } - return items -} - // SetKeysInMap sets the keys in the map -func SetKeysInMap(resource string, keys map[KMPMapKey]crypto.PublicKey) { - keyMap.Store(resource, keys) +func SetKeysInMap(resource string, providerType string, keys map[KMPMapKey]crypto.PublicKey) { + typedMap := make(map[KMPMapKey]PublicKey) + for key, value := range keys { + typedMap[key] = PublicKey{Key: value, ProviderType: providerType} + } + keyMap.Store(resource, typedMap) } -// GetKeysFromMap gets the keys from the map and returns an empty map of keys if not found -func GetKeysFromMap(resource string) map[KMPMapKey]crypto.PublicKey { +// GetKeysFromMap gets the keys from the map and returns an empty map with false boolean if not found +func GetKeysFromMap(resource string) (map[KMPMapKey]PublicKey, bool) { keys, ok := keyMap.Load(resource) if !ok { - return map[KMPMapKey]crypto.PublicKey{} + return map[KMPMapKey]PublicKey{}, false } - return keys.(map[KMPMapKey]crypto.PublicKey) + return keys.(map[KMPMapKey]PublicKey), true } // DeleteKeysFromMap deletes the keys from the map diff --git a/pkg/keymanagementprovider/keymanagementprovider_test.go b/pkg/keymanagementprovider/keymanagementprovider_test.go index 3607d2d98..266d790b6 100644 --- a/pkg/keymanagementprovider/keymanagementprovider_test.go +++ b/pkg/keymanagementprovider/keymanagementprovider_test.go @@ -178,7 +178,7 @@ func TestFlattenKMPMap(t *testing.T) { // TestSetKeysInMap checks if keys are set in the map func TestSetKeysInMap(t *testing.T) { keyMap.Delete("test") - SetKeysInMap("test", map[KMPMapKey]crypto.PublicKey{{}: &rsa.PublicKey{}}) + SetKeysInMap("test", "", map[KMPMapKey]crypto.PublicKey{{}: &rsa.PublicKey{}}) if _, ok := keyMap.Load("test"); !ok { t.Fatalf("keysMap should have been set for key") } @@ -187,8 +187,8 @@ func TestSetKeysInMap(t *testing.T) { // TestGetKeysFromMap checks if keys are fetched from the map func TestGetKeysFromMap(t *testing.T) { keyMap.Delete("test") - SetKeysInMap("test", map[KMPMapKey]crypto.PublicKey{{}: &rsa.PublicKey{}}) - keys := GetKeysFromMap("test") + SetKeysInMap("test", "", map[KMPMapKey]crypto.PublicKey{{}: &rsa.PublicKey{}}) + keys, _ := GetKeysFromMap("test") if len(keys) != 1 { t.Fatalf("keys should have been fetched from the map") } @@ -197,7 +197,7 @@ func TestGetKeysFromMap(t *testing.T) { // TestGetKeysFromMap_FailedToFetch checks if keys fail to fetch from map func TestGetKeysFromMap_FailedToFetch(t *testing.T) { keyMap.Delete("test") - keys := GetKeysFromMap("test") + keys, _ := GetKeysFromMap("test") if len(keys) != 0 { t.Fatalf("keys should not have been fetched from the map") } @@ -206,21 +206,13 @@ func TestGetKeysFromMap_FailedToFetch(t *testing.T) { // TestDeleteKeysFromMap checks if key map entry is deleted from the map func TestDeleteKeysFromMap(t *testing.T) { keyMap.Delete("test") - SetKeysInMap("test", map[KMPMapKey]crypto.PublicKey{{}: &rsa.PublicKey{}}) + SetKeysInMap("test", "", map[KMPMapKey]crypto.PublicKey{{}: &rsa.PublicKey{}}) DeleteKeysFromMap("test") if _, ok := keyMap.Load("test"); ok { t.Fatalf("keysMap should have been deleted for key") } } -// TestFlattenKMPMapKeys checks if keys in map are flattened to a single array -func TestFlattenKMPMapKeys(t *testing.T) { - keys := FlattenKMPMapKeys(map[KMPMapKey]crypto.PublicKey{{Name: "testkey1"}: &rsa.PublicKey{}, {Name: "testkey2"}: &rsa.PublicKey{}}) - if len(keys) != 2 { - t.Fatalf("keys should have been flattened") - } -} - // TestDecodeKey checks if key is decoded from pem func TestDecodeKey(t *testing.T) { validKey := `-----BEGIN PUBLIC KEY----- diff --git a/pkg/referrerstore/mocks/memory_store.go b/pkg/referrerstore/mocks/memory_store.go index 93bbf1749..e64aa9c4e 100644 --- a/pkg/referrerstore/mocks/memory_store.go +++ b/pkg/referrerstore/mocks/memory_store.go @@ -53,14 +53,14 @@ func (store *MemoryTestStore) GetBlobContent(_ context.Context, _ common.Referen if item, ok := store.Blobs[digest]; ok { return item, nil } - return nil, nil + return nil, fmt.Errorf("blob not found") } func (store *MemoryTestStore) GetReferenceManifest(_ context.Context, _ common.Reference, desc ocispecs.ReferenceDescriptor) (ocispecs.ReferenceManifest, error) { if item, ok := store.Manifests[desc.Digest]; ok { return item, nil } - return ocispecs.ReferenceManifest{}, nil + return ocispecs.ReferenceManifest{}, fmt.Errorf("manifest not found") } func (store *MemoryTestStore) GetConfig() *config.StoreConfig { diff --git a/pkg/verifier/cosign/cosign.go b/pkg/verifier/cosign/cosign.go index 957c2323f..8e77da67c 100644 --- a/pkg/verifier/cosign/cosign.go +++ b/pkg/verifier/cosign/cosign.go @@ -18,15 +18,22 @@ package cosign import ( "context" "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" "crypto/x509" + "encoding/base64" "encoding/json" "fmt" + "math/big" "os" "path/filepath" "strings" "github.com/deislabs/ratify/internal/logger" "github.com/deislabs/ratify/pkg/common" + "github.com/deislabs/ratify/pkg/keymanagementprovider" + "github.com/deislabs/ratify/pkg/keymanagementprovider/azurekeyvault" "github.com/deislabs/ratify/pkg/ocispecs" "github.com/deislabs/ratify/pkg/referrerstore" "github.com/deislabs/ratify/pkg/utils" @@ -34,6 +41,8 @@ import ( "github.com/deislabs/ratify/pkg/verifier/config" "github.com/deislabs/ratify/pkg/verifier/factory" "github.com/deislabs/ratify/pkg/verifier/types" + "golang.org/x/crypto/cryptobyte" + "golang.org/x/crypto/cryptobyte/asn1" re "github.com/deislabs/ratify/errors" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -50,12 +59,13 @@ import ( ) type PluginConfig struct { - Name string `json:"name"` - Type string `json:"type,omitempty"` - ArtifactTypes string `json:"artifactTypes"` - KeyRef string `json:"key,omitempty"` - RekorURL string `json:"rekorURL,omitempty"` - NestedReferences []string `json:"nestedArtifactTypes,omitempty"` + Name string `json:"name"` + Type string `json:"type,omitempty"` + ArtifactTypes string `json:"artifactTypes"` + KeyRef string `json:"key,omitempty"` + RekorURL string `json:"rekorURL,omitempty"` + NestedReferences []string `json:"nestedArtifactTypes,omitempty"` + TrustPolicies []TrustPolicyConfig `json:"trustPolicies,omitempty"` } type Extension struct { @@ -67,6 +77,7 @@ type cosignExtension struct { IsSuccess bool `json:"isSuccess"` BundleVerified bool `json:"bundleVerified"` Err string `json:"error,omitempty"` + KeyInformation PKKey `json:"keyInformation,omitempty"` } type cosignVerifier struct { @@ -75,6 +86,9 @@ type cosignVerifier struct { artifactTypes []string nestedReferences []string config *PluginConfig + isLegacy bool + trustPolicies *TrustPolicies + namespace string } type cosignVerifierFactory struct{} @@ -83,6 +97,9 @@ var logOpt = logger.Option{ ComponentType: logger.Verifier, } +// used for mocking purposes +var getKeysMaps = getKeysMapsDefault + const verifierType string = "cosign" // init() registers the cosign verifier with the factory @@ -103,12 +120,30 @@ func (f *cosignVerifierFactory) Create(_ string, verifierConfig config.VerifierC return nil, re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName) } + // if key or rekorURL is provided, trustPolicies should not be provided + if (config.KeyRef != "" || config.RekorURL != "") && len(config.TrustPolicies) > 0 { + return nil, re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail("'key' and 'rekorURL' are part of cosign legacy configuration and cannot be used with `trustPolicies`") + } + + var trustPolicies *TrustPolicies + legacy := true + if config.KeyRef == "" && config.RekorURL == "" { + trustPolicies, err = CreateTrustPolicies(config.TrustPolicies, verifierName) + if err != nil { + return nil, err + } + legacy = false + } + return &cosignVerifier{ name: verifierName, verifierType: config.Type, artifactTypes: strings.Split(config.ArtifactTypes, ","), nestedReferences: config.NestedReferences, config: config, + isLegacy: legacy, + trustPolicies: trustPolicies, + namespace: namespace, }, nil } @@ -132,8 +167,157 @@ func (v *cosignVerifier) CanVerify(_ context.Context, referenceDescriptor ocispe return false } -// Verify verifies the subject reference using the cosign verifier func (v *cosignVerifier) Verify(ctx context.Context, subjectReference common.Reference, referenceDescriptor ocispecs.ReferenceDescriptor, referrerStore referrerstore.ReferrerStore) (verifier.VerifierResult, error) { + if v.isLegacy { + return v.verifyLegacy(ctx, subjectReference, referenceDescriptor, referrerStore) + } + return v.verifyInternal(ctx, subjectReference, referenceDescriptor, referrerStore) +} + +func (v *cosignVerifier) verifyInternal(ctx context.Context, subjectReference common.Reference, referenceDescriptor ocispecs.ReferenceDescriptor, referrerStore referrerstore.ReferrerStore) (verifier.VerifierResult, error) { + // get the map of keys and relevant cosign options for that reference + keysMap, cosignOpts, err := getKeysMaps(ctx, v.trustPolicies, subjectReference.Original, v.namespace) + if err != nil { + return errorToVerifyResult(v.name, v.verifierType, err), nil + } + + // get the reference manifest (cosign oci image) + referenceManifest, err := referrerStore.GetReferenceManifest(ctx, subjectReference, referenceDescriptor) + if err != nil { + return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("failed to get reference manifest: %w", err)), nil + } + + // manifest must be an OCI Image + if referenceManifest.MediaType != imgspec.MediaTypeImageManifest { + return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("reference manifest is not an image")), nil + } + + // get the subject image descriptor + subjectDesc, err := referrerStore.GetSubjectDescriptor(ctx, subjectReference) + if err != nil { + return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("failed to create subject hash: %w", err)), nil + } + + // create the hash of the subject image descriptor (used as the hashed payload) + subjectDescHash := v1.Hash{ + Algorithm: subjectDesc.Digest.Algorithm().String(), + Hex: subjectDesc.Digest.Hex(), + } + + sigExtensions := make([]cosignExtension, 0) + hasValidSignature := false + // check each signature found + for _, blob := range referenceManifest.Blobs { + // fetch the blob content of the signature from the referrer store + blobBytes, err := referrerStore.GetBlobContent(ctx, subjectReference, blob.Digest) + if err != nil { + return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("failed to get blob content: %w", err)), nil + } + // convert the blob to a static signature + staticOpts, err := staticLayerOpts(blob) + if err != nil { + return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("failed to parse static signature opts: %w", err)), nil + } + sig, err := static.NewSignature(blobBytes, blob.Annotations[static.SignatureAnnotationKey], staticOpts...) + if err != nil { + return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("failed to generate static signature: %w", err)), nil + } + // check each key in the map of keys returned by the trust policy + for mapKey, pubKey := range keysMap { + hashType := crypto.SHA256 + // default hash type is SHA256 but for AKV scenarios, the hash type is determined by the key size + // TODO: investigate if it's possible to extract hash type from sig directly. This is a workaround for now + if pubKey.ProviderType == azurekeyvault.ProviderName { + switch keyType := pubKey.Key.(type) { + case *rsa.PublicKey: + switch keyType.Size() { + case 256: + hashType = crypto.SHA256 + case 384: + hashType = crypto.SHA384 + case 512: + hashType = crypto.SHA512 + default: + return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("RSA key check: unsupported key size: %d", keyType.Size())), nil + } + + // TODO: remove section after fix for bug in cosign azure key vault implementation + // tracking issue: https://github.com/sigstore/sigstore/issues/1384 + // summary: azure keyvault implementation ASN.1 encodes sig after online signing with keyvault + // EC verifiers in cosign have built in ASN.1 decoding, but RSA verifiers do not + base64DecodedBytes, err := base64.StdEncoding.DecodeString(blob.Annotations[static.SignatureAnnotationKey]) + if err != nil { + return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("RSA key check: failed to decode base64 signature: %w", err)), nil + } + // decode ASN.1 signature to raw signature if it is ASN.1 encoded + decodedSigBytes, err := decodeASN1Signature(base64DecodedBytes) + if err != nil { + return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("RSA key check: failed to decode ASN.1 signature: %w", err)), nil + } + encodedBase64SigBytes := base64.StdEncoding.EncodeToString(decodedSigBytes) + sig, err = static.NewSignature(blobBytes, encodedBase64SigBytes, staticOpts...) + if err != nil { + return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("RSA key check: failed to generate static signature: %w", err)), nil + } + case *ecdsa.PublicKey: + switch keyType.Curve { + case elliptic.P256(): + hashType = crypto.SHA256 + case elliptic.P384(): + hashType = crypto.SHA384 + case elliptic.P521(): + hashType = crypto.SHA512 + default: + return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("ECDSA key check: unsupported key curve: %s", keyType.Params().Name)), nil + } + default: + return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("unsupported public key type: %T", pubKey)), nil + } + } + + // return the correct verifier based on public key type and bytes + verifier, err := signature.LoadVerifier(pubKey.Key, hashType) + if err != nil { + return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("failed to load public key from provider [%s] name [%s] version [%s]: %w", mapKey.Provider, mapKey.Name, mapKey.Version, err)), nil + } + cosignOpts.SigVerifier = verifier + // verify signature with cosign options + perform bundle verification + bundleVerified, err := cosign.VerifyImageSignature(ctx, sig, subjectDescHash, &cosignOpts) + extension := cosignExtension{ + SignatureDigest: blob.Digest, + IsSuccess: true, + BundleVerified: bundleVerified, + KeyInformation: mapKey, + } + if err != nil { + extension.IsSuccess = false + extension.Err = err.Error() + } else { + hasValidSignature = true + } + sigExtensions = append(sigExtensions, extension) + } + + // TODO: perform keyless verification instead if no keys are found + } + + if hasValidSignature { + return verifier.VerifierResult{ + Name: v.name, + Type: v.verifierType, + IsSuccess: true, + Message: "cosign verification success. valid signatures found. please refer to extensions field for verifications performed.", + Extensions: Extension{SignatureExtension: sigExtensions}, + }, nil + } + + errorResult := errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("no valid signatures found")) + errorResult.Extensions = Extension{SignatureExtension: sigExtensions} + return errorResult, nil +} + +// **LEGACY** This implementation will be removed in Ratify v2.0.0. Verify verifies the subject reference using the cosign verifier. +func (v *cosignVerifier) verifyLegacy(ctx context.Context, subjectReference common.Reference, referenceDescriptor ocispecs.ReferenceDescriptor, referrerStore referrerstore.ReferrerStore) (verifier.VerifierResult, error) { cosignOpts := &cosign.CheckOpts{ ClaimVerifier: cosign.SimpleClaimVerifier, } @@ -322,3 +506,57 @@ func errorToVerifyResult(name string, verifierType string, err error) verifier.V Message: errors.Wrap(err, "cosign verification failed").Error(), } } + +// decodeASN1Signature decodes the ASN.1 signature to raw signature bytes +func decodeASN1Signature(sig []byte) ([]byte, error) { + // Convert the ASN.1 Sequence to a concatenated r||s byte string + // This logic is based from https://cs.opensource.google/go/go/+/refs/tags/go1.17.3:src/crypto/ecdsa/ecdsa.go;l=339 + var ( + r, s = &big.Int{}, &big.Int{} + inner cryptobyte.String + ) + + rawSigBytes := sig + input := cryptobyte.String(sig) + if input.ReadASN1(&inner, asn1.SEQUENCE) { + // if ASN.1 sequence is found, parse r and s + if !inner.ReadASN1Integer(r) { + return nil, fmt.Errorf("failed parsing r") + } + if !inner.ReadASN1Integer(s) { + return nil, fmt.Errorf("failed parsing s") + } + if !inner.Empty() { + return nil, fmt.Errorf("failed parsing signature") + } + rawSigBytes = []byte{} + rawSigBytes = append(rawSigBytes, r.Bytes()...) + rawSigBytes = append(rawSigBytes, s.Bytes()...) + } + + return rawSigBytes, nil +} + +// getKeysMapsDefault returns the map of keys and cosign options for the reference +func getKeysMapsDefault(ctx context.Context, trustPolicies *TrustPolicies, reference string, namespace string) (map[PKKey]keymanagementprovider.PublicKey, cosign.CheckOpts, error) { + // get the trust policy for the reference + trustPolicy, err := trustPolicies.GetScopedPolicy(reference) + if err != nil { + return nil, cosign.CheckOpts{}, err + } + logger.GetLogger(ctx, logOpt).Debugf("selected trust policy %s for reference %s", trustPolicy.GetName(), reference) + + // get the map of keys for that reference + keysMap, err := trustPolicy.GetKeys(namespace) + if err != nil { + return nil, cosign.CheckOpts{}, err + } + + // get the cosign options for that trust policy + cosignOpts, err := trustPolicy.GetCosignOpts(ctx) + if err != nil { + return nil, cosign.CheckOpts{}, err + } + + return keysMap, cosignOpts, nil +} diff --git a/pkg/verifier/cosign/cosign_test.go b/pkg/verifier/cosign/cosign_test.go index 5cb6903a2..13a0261be 100644 --- a/pkg/verifier/cosign/cosign_test.go +++ b/pkg/verifier/cosign/cosign_test.go @@ -17,15 +17,31 @@ package cosign import ( "context" + "crypto/ecdh" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" "fmt" + "slices" + "strings" "testing" + "github.com/deislabs/ratify/pkg/common" + "github.com/deislabs/ratify/pkg/keymanagementprovider" + "github.com/deislabs/ratify/pkg/keymanagementprovider/azurekeyvault" "github.com/deislabs/ratify/pkg/ocispecs" + "github.com/deislabs/ratify/pkg/referrerstore/mocks" "github.com/deislabs/ratify/pkg/verifier/config" + "github.com/opencontainers/go-digest" imgspec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/oci/static" + "github.com/sigstore/sigstore/pkg/cryptoutils" ) +const ratifySampleImageRef string = "ghcr.io/deislabs/ratify:v1" + // TestCreate tests the Create function of the cosign verifier func TestCreate(t *testing.T) { tests := []struct { @@ -38,6 +54,22 @@ func TestCreate(t *testing.T) { config: config.VerifierConfig{ "name": "test", "artifactTypes": "testtype", + "trustPolicies": []TrustPolicyConfig{ + { + Name: "test", + Keyless: KeylessConfig{RekorURL: DefaultRekorURL}, + Scopes: []string{"*"}, + }, + }, + }, + wantErr: false, + }, + { + name: "valid legacy config", + config: config.VerifierConfig{ + "name": "test", + "artifactTypes": "testtype", + "key": "testkey_path", }, wantErr: false, }, @@ -55,6 +87,31 @@ func TestCreate(t *testing.T) { }, wantErr: true, }, + { + name: "invalid trust policies config", + config: config.VerifierConfig{ + "name": "test", + "artifactTypes": "testtype", + "trustPolicies": []TrustPolicyConfig{}, + }, + wantErr: true, + }, + { + name: "invalid config with legacy and trust policies", + config: config.VerifierConfig{ + "name": "test", + "artifactTypes": "testtype", + "trustPolicies": []TrustPolicyConfig{ + { + Name: "test", + Keyless: KeylessConfig{RekorURL: DefaultRekorURL}, + Scopes: []string{"*"}, + }, + }, + "key": "testkey_path", + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -75,6 +132,13 @@ func TestName(t *testing.T) { validConfig := config.VerifierConfig{ "name": name, "artifactTypes": "testtype", + "trustPolicies": []TrustPolicyConfig{ + { + Name: "test", + Keyless: KeylessConfig{RekorURL: DefaultRekorURL}, + Scopes: []string{"*"}, + }, + }, } cosignVerifier, err := verifierFactory.Create("", validConfig, "", "test-namespace") if err != nil { @@ -92,6 +156,13 @@ func TestType(t *testing.T) { validConfig := config.VerifierConfig{ "name": "test", "artifactTypes": "testtype", + "trustPolicies": []TrustPolicyConfig{ + { + Name: "test", + Keyless: KeylessConfig{RekorURL: DefaultRekorURL}, + Scopes: []string{"*"}, + }, + }, } cosignVerifier, err := verifierFactory.Create("", validConfig, "", "test-namespace") if err != nil { @@ -138,6 +209,13 @@ func TestCanVerify(t *testing.T) { validConfig := config.VerifierConfig{ "name": "test", "artifactTypes": tt.artifactTypes, + "trustPolicies": []TrustPolicyConfig{ + { + Name: "test", + Keyless: KeylessConfig{RekorURL: DefaultRekorURL}, + Scopes: []string{"*"}, + }, + }, } cosignVerifier, err := verifierFactory.Create("", validConfig, "", "test-namespace") if err != nil { @@ -155,8 +233,15 @@ func TestCanVerify(t *testing.T) { func TestGetNestedReferences(t *testing.T) { verifierFactory := cosignVerifierFactory{} validConfig := config.VerifierConfig{ - "name": "test", - "artifactTypes": "testtype", + "name": "test", + "artifactTypes": "testtype", + "trustPolicies": []TrustPolicyConfig{ + { + Name: "test", + Keyless: KeylessConfig{RekorURL: DefaultRekorURL}, + Scopes: []string{"*"}, + }, + }, "nestedArtifactTypes": []string{"nested-artifact-type"}, } cosignVerifier, err := verifierFactory.Create("", validConfig, "", "test-namespace") @@ -308,3 +393,546 @@ func TestErrorToVerifyResult(t *testing.T) { t.Errorf("errorToVerifyResult() = %v, want %v", verifierResult.Message, "cosign verification failed: test error") } } + +// TestDecodeASN1Signature tests the decodeASN1Signature function +func TestDecodeASN1Signature(t *testing.T) { + tc := []struct { + name string + sigBytes []byte + expectedSigBytes []byte + wantErr bool + }{ + { + name: "invalid not asn1", + sigBytes: []byte("test"), + expectedSigBytes: []byte("test"), + wantErr: false, + }, + { + name: "valid asn1", + sigBytes: []byte("0E\x02!\x00\xb4\xd7R\xf0\xee\x11ձ\x9f\rtsog\x99\xa1\x86L=\x04\x92\u07b8\xb7\xa1\x94Mj\xfe\xd2\xda\x02\x02 \x027|~q\xcb2\xaf\xd1\xddx;\x04\xed\r\x9a\x9a\x03\xa9\x84\x8cu\xba\x1a\x9eFb\xc2h\x7fk\xc3"), + expectedSigBytes: []byte("\xb4\xd7R\xf0\xee\x11ձ\x9f\rtsog\x99\xa1\x86L=\x04\x92\u07b8\xb7\xa1\x94Mj\xfe\xd2\xda\x02\x027|~q\xcb2\xaf\xd1\xddx;\x04\xed\r\x9a\x9a\x03\xa9\x84\x8cu\xba\x1a\x9eFb\xc2h\x7fk\xc3"), + wantErr: false, + }, + { + name: "invalid r", + sigBytes: []byte("0E\x03!\x00\xb4\xd7R\xf0\xee\x11ձ\x9f\rtsog\x99\xa1\x86L=\x04\x92\u07b8\xb7\xa1\x94Mj\xfe\xd2\xda\x02\x02 \x027|~q\xcb2\xaf\xd1\xddx;\x04\xed\r\x9a\x9a\x03\xa9\x84\x8cu\xba\x1a\x9eFb\xc2h\x7fk\xc3"), + expectedSigBytes: nil, + wantErr: true, + }, + { + name: "invalid s", + sigBytes: []byte("0E\x02!\x00\xb4\xd7R\xf0\xee\x11ձ\x9f\rtsog\x99\xa1\x86L=\x04\x92\u07b8\xb7\xa1\x94Mj\xfe\xd2\xda\x02\x03 \x027|~q\xcb2\xaf\xd1\xddx;\x04\xed\r\x9a\x9a\x03\xa9\x84\x8cu\xba\x1a\x9eFb\xc2h\x7fk\xc3"), + expectedSigBytes: nil, + wantErr: true, + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + sigBytes, err := decodeASN1Signature(tt.sigBytes) + if (err != nil) != tt.wantErr { + t.Errorf("decodeASN1Signature() error = %v, wantErr %v", err, tt.wantErr) + } + if sigBytes != nil && !slices.Equal(tt.expectedSigBytes, sigBytes) { + t.Errorf("decodeASN1Signature() = %v, want %v", sigBytes, tt.expectedSigBytes) + } + }) + } +} + +func TestGetKeysMaps_Success(t *testing.T) { + trustPolciesConfig := []TrustPolicyConfig{ + { + Name: "test-policy", + Keyless: KeylessConfig{RekorURL: DefaultRekorURL}, + Scopes: []string{"ghcr.io/*"}, + }, + } + + trustPolicies, err := CreateTrustPolicies(trustPolciesConfig, "test") + if err != nil { + t.Fatalf("CreateTrustPolicies() error = %v", err) + } + _, _, err = getKeysMapsDefault(context.Background(), trustPolicies, ratifySampleImageRef, "gatekeeper-system") + if err != nil { + t.Errorf("getKeysMaps() error = %v, wantErr %v", err, false) + } +} + +func TestGetKeysMaps_FailingTrustPolicies(t *testing.T) { + trustPolciesConfig := []TrustPolicyConfig{ + { + Name: "test-policy", + Keyless: KeylessConfig{RekorURL: DefaultRekorURL}, + Scopes: []string{"myregistry.io/*"}, + }, + } + + trustPolicies, err := CreateTrustPolicies(trustPolciesConfig, "test") + if err != nil { + t.Fatalf("CreateTrustPolicies() error = %v", err) + } + _, _, err = getKeysMapsDefault(context.Background(), trustPolicies, ratifySampleImageRef, "gatekeeper-system") + if err == nil { + t.Errorf("getKeysMaps() error = %v, wantErr %v", err, true) + } +} + +func TestGetKeysMaps_FailingGetKeys(t *testing.T) { + trustPolciesConfig := []TrustPolicyConfig{ + { + Name: "test-policy", + Keys: []KeyConfig{ + { + Provider: "non-existent", + }, + }, + Scopes: []string{"*"}, + }, + } + + trustPolicies, err := CreateTrustPolicies(trustPolciesConfig, "test") + if err != nil { + t.Fatalf("CreateTrustPolicies() error = %v", err) + } + _, _, err = getKeysMapsDefault(context.Background(), trustPolicies, ratifySampleImageRef, "gatekeeper-system") + if err == nil { + t.Errorf("getKeysMaps() error = %v, wantErr %v", err, true) + } +} + +// TestVerifyInternal tests the verifyInternal function of the cosign verifier +func TestVerifyInternal(t *testing.T) { + cosignMediaType := "application/vnd.dev.cosign.simplesigning.v1+json" + validSignatureBlob := []byte("test") + subjectDigest := digest.Digest("sha256:5678") + testRefDigest := digest.Digest("sha256:1234") + blobDigest := digest.Digest("valid blob") + //nolint:gosec // this is a test key + invalidRsaPrivKey, _ := rsa.GenerateKey(rand.Reader, 1024) + invalidRsaPubKey := invalidRsaPrivKey.Public() + invalidECPrivKey, _ := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) + invalidECPubKey := invalidECPrivKey.Public() + + validRSA256PubKey, err := cryptoutils.UnmarshalPEMToPublicKey([]byte(`-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtHGXFzi1W93vQ88EwzmI +IhTXMYpcffQmmHYLgjkeLWL4SQ7DJEyq4j+Yz994lq0B4nCT9EaLkXYSMZhfYuHg +y+2kMkh+1eNUtjGVJBkHc5iz7YR9OaIDlY36TnKlk0HfyBrjNrlwyodD6no/2LCf +6FmGT6mVIaE/fyrxN3ZCHzfcw5LaGgHRt+91NJa5PnQCxjXUfyabHbHehgNLjjpn +kwCW3GGs56cOMQowHsLrlwQnXAq5wvAueRz3Ommz+DPnVXUSV+vfYDt56oggX386 +LOe8VCiwi4T9IIuWlKIi+AuIm8aQ+11o9LjpvDqFD1rJU/KMFhczA4Kj0fRM7Ulg +ewIDAQAB +-----END PUBLIC KEY-----`)) + if err != nil { + t.Fatalf("error creating RSA public key: %v", err) + } + + validRSA384PubKey, err := cryptoutils.UnmarshalPEMToPublicKey([]byte(`-----BEGIN PUBLIC KEY----- +MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEArccnwmvTOS0iqaxiCRsD +4vixdUy2az39vL3iABjLr6Ht2NLA8dyO0NeEBb6+lfoqOl8RX29rJ0LnCaQL/wV8 +BQ3idfzdeX/rzdhRoegDiZ7MDgd01ZDeocGSfOAKJ3E1Kr0+etB4UuOF2T7dcVNn +lAtZxEH6wNtW2HFoLg6bnlUuSj+9RVaP2Z0D55Bk4Un1jinB6Et81SCIuvDcMbKt +aW3Xu17mdHiscLQBOnmX86mKRP8R4Ij9TtNyEW/9WLNXHV1iJhm9TVONZkX2hRjy +o3+pPYvsZAAjyIk4AHF4BROCMA+WmyqkjnyVdEcJBi6f8NptjnS8A5jtTXIrndEq +OE1VTu44z8hcQqrIypdyF86rMsJm91F8x68clvSYyvYn15lzv720LOglFF2NrD8S +0SmxbyPB4bnRhEiyxh9ocAbGVXu+FyjrLPjTCTTnIpnTzgm/XtSqjA6104Zz3Seu +TvvdnkTLbUxqHzoFYXSksJHvOiH2U7JAay8S4CZ4KrGvAgMBAAE= +-----END PUBLIC KEY-----`)) + if err != nil { + t.Fatalf("error creating RSA public key: %v", err) + } + + validECDSAP256PubKey, err := cryptoutils.UnmarshalPEMToPublicKey([]byte(`-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1ljPT4AO3Ny57S2B1a5P2LSrru5l +ewt8iyi46g8SRrasghTR8xliLiZJl3GTM3UOdUAZCiruwPC7hihLD5JcqQ== +-----END PUBLIC KEY-----`)) + if err != nil { + t.Fatalf("error creating RSA public key: %v", err) + } + + validECDSAP384PubKey, err := cryptoutils.UnmarshalPEMToPublicKey([]byte(`-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEFbJSMiBAtIiydUeqhMGpZBDRkZhYFu5r +zg5rpyR7WJVgDPH8++2IY9Zg1HYKB0aGqyuLK5i8bG3C8avDLXg2+Dmf35wV2Mgh +mmBwUAwwW0Uc+Nt3bDOCiB1nUsICv1ry +-----END PUBLIC KEY----- + `)) + if err != nil { + t.Fatalf("error creating RSA public key: %v", err) + } + + subjectRef := common.Reference{ + Digest: subjectDigest, + Original: ratifySampleImageRef, + Tag: "v1", + } + refDescriptor := ocispecs.ReferenceDescriptor{ + ArtifactType: "testtype", + Descriptor: imgspec.Descriptor{ + Digest: testRefDigest, + MediaType: imgspec.MediaTypeImageManifest, + }, + } + tc := []struct { + name string + keys map[PKKey]keymanagementprovider.PublicKey + getKeysError bool + cosignOpts cosign.CheckOpts + store *mocks.MemoryTestStore + expectedResultMessagePrefix string + }{ + { + name: "get keys error", + keys: map[PKKey]keymanagementprovider.PublicKey{}, + getKeysError: true, + store: &mocks.MemoryTestStore{}, + expectedResultMessagePrefix: "cosign verification failed: error", + }, + { + name: "manifest fetch error", + keys: map[PKKey]keymanagementprovider.PublicKey{}, + getKeysError: false, + store: &mocks.MemoryTestStore{}, + expectedResultMessagePrefix: "cosign verification failed: failed to get reference manifest", + }, + { + name: "incorrect reference manifest media type error", + keys: map[PKKey]keymanagementprovider.PublicKey{}, + getKeysError: false, + store: &mocks.MemoryTestStore{ + Manifests: map[digest.Digest]ocispecs.ReferenceManifest{ + testRefDigest: { + MediaType: "invalid", + }, + }, + }, + expectedResultMessagePrefix: "cosign verification failed: reference manifest is not an image", + }, + { + name: "failed subject descriptor fetch", + keys: map[PKKey]keymanagementprovider.PublicKey{}, + getKeysError: false, + store: &mocks.MemoryTestStore{ + Manifests: map[digest.Digest]ocispecs.ReferenceManifest{ + testRefDigest: { + MediaType: refDescriptor.MediaType, + }, + }, + }, + expectedResultMessagePrefix: "cosign verification failed: failed to create subject hash", + }, + { + name: "failed to fetch blob", + keys: map[PKKey]keymanagementprovider.PublicKey{}, + getKeysError: false, + store: &mocks.MemoryTestStore{ + Manifests: map[digest.Digest]ocispecs.ReferenceManifest{ + testRefDigest: { + MediaType: refDescriptor.MediaType, + Blobs: []imgspec.Descriptor{ + { + Digest: digest.Digest("nonexistent blob hash"), + }, + }, + }, + }, + Subjects: map[digest.Digest]*ocispecs.SubjectDescriptor{ + subjectDigest: { + Descriptor: imgspec.Descriptor{ + Digest: subjectDigest, + MediaType: imgspec.MediaTypeImageManifest, + }, + }, + }, + }, + expectedResultMessagePrefix: "cosign verification failed: failed to get blob content", + }, + { + name: "invalid key type for AKV", + keys: map[PKKey]keymanagementprovider.PublicKey{ + {Provider: "test"}: {Key: &ecdh.PublicKey{}, ProviderType: azurekeyvault.ProviderName}, + }, + getKeysError: false, + store: &mocks.MemoryTestStore{ + Manifests: map[digest.Digest]ocispecs.ReferenceManifest{ + testRefDigest: { + MediaType: refDescriptor.MediaType, + Blobs: []imgspec.Descriptor{ + { + Digest: blobDigest, + }, + }, + }, + }, + Subjects: map[digest.Digest]*ocispecs.SubjectDescriptor{ + subjectDigest: { + Descriptor: imgspec.Descriptor{ + Digest: subjectDigest, + MediaType: imgspec.MediaTypeImageManifest, + }, + }, + }, + Blobs: map[digest.Digest][]byte{ + blobDigest: validSignatureBlob, + }, + }, + expectedResultMessagePrefix: "cosign verification failed: unsupported public key type", + }, + { + name: "invalid RSA key size for AKV", + keys: map[PKKey]keymanagementprovider.PublicKey{ + {Provider: "test"}: {Key: invalidRsaPubKey, ProviderType: azurekeyvault.ProviderName}, + }, + getKeysError: false, + store: &mocks.MemoryTestStore{ + Manifests: map[digest.Digest]ocispecs.ReferenceManifest{ + testRefDigest: { + MediaType: refDescriptor.MediaType, + Blobs: []imgspec.Descriptor{ + { + Digest: blobDigest, + }, + }, + }, + }, + Subjects: map[digest.Digest]*ocispecs.SubjectDescriptor{ + subjectDigest: { + Descriptor: imgspec.Descriptor{ + Digest: subjectDigest, + MediaType: imgspec.MediaTypeImageManifest, + }, + }, + }, + Blobs: map[digest.Digest][]byte{ + blobDigest: validSignatureBlob, + }, + }, + expectedResultMessagePrefix: "cosign verification failed: RSA key check: unsupported key size", + }, + { + name: "invalid ECDSA curve type for AKV", + keys: map[PKKey]keymanagementprovider.PublicKey{ + {Provider: "test"}: {Key: invalidECPubKey, ProviderType: azurekeyvault.ProviderName}, + }, + getKeysError: false, + store: &mocks.MemoryTestStore{ + Manifests: map[digest.Digest]ocispecs.ReferenceManifest{ + testRefDigest: { + MediaType: refDescriptor.MediaType, + Blobs: []imgspec.Descriptor{ + { + Digest: blobDigest, + }, + }, + }, + }, + Subjects: map[digest.Digest]*ocispecs.SubjectDescriptor{ + subjectDigest: { + Descriptor: imgspec.Descriptor{ + Digest: subjectDigest, + MediaType: imgspec.MediaTypeImageManifest, + }, + }, + }, + Blobs: map[digest.Digest][]byte{ + blobDigest: validSignatureBlob, + }, + }, + expectedResultMessagePrefix: "cosign verification failed: ECDSA key check: unsupported key curve", + }, + { + name: "valid hash: 256 keysize: 2048 RSA key AKV", + keys: map[PKKey]keymanagementprovider.PublicKey{ + {Provider: "test"}: {Key: validRSA256PubKey, ProviderType: azurekeyvault.ProviderName}, + }, + cosignOpts: cosign.CheckOpts{ + IgnoreSCT: true, + IgnoreTlog: true, + }, + getKeysError: false, + store: &mocks.MemoryTestStore{ + Manifests: map[digest.Digest]ocispecs.ReferenceManifest{ + testRefDigest: { + MediaType: refDescriptor.MediaType, + Blobs: []imgspec.Descriptor{ + { + Size: 267, + Digest: "sha256:6e1ffef2ba058cda5d1aa7ed792cb1e63b4207d8195a469bee1b5fc662cd9b70", + MediaType: cosignMediaType, + Annotations: map[string]string{ + static.SignatureAnnotationKey: "j6VNQ+Z3BqLeM75WM8WKnJqtwR7Kv21BwURHLmK6S05gCV/JntSbVthNVKoNY3906NMqmfZDlP/QuUOQt7Fxq2ivixw1xKa1KlE+ydW951GyMysaZx36U08Wmfyqt6dbgXMU6/nQE8oxG855rfywvE+MAmIJ+u1ktPbU+HoXEPP8yNUyK4gY/JAopQVEcktGAqFAbT49LzlE3FTJQNE6WryCQy5GiaM/1qdKzQi9GQb2g20Vxg6+e4AuxogAs+bzexoA4J5bUkDAkE/PDIXNz2EgjB0o7zK1NQEDiLNRq7fafTY5G56vXtltuMWOzCGnLMXbk4f3K9wKXF++7h4I3w==", + }, + }, + }, + }, + }, + Subjects: map[digest.Digest]*ocispecs.SubjectDescriptor{ + subjectDigest: { + Descriptor: imgspec.Descriptor{ + Digest: subjectDigest, + MediaType: imgspec.MediaTypeImageManifest, + }, + }, + }, + Blobs: map[digest.Digest][]byte{ + "sha256:6e1ffef2ba058cda5d1aa7ed792cb1e63b4207d8195a469bee1b5fc662cd9b70": []byte(`{"critical":{"identity":{"docker-reference":"artifactstest.azurecr.io/4-15-24/cosign/hello-world"},"image":{"docker-manifest-digest":"sha256:d37ada95d47ad12224c205a938129df7a3e52345828b4fa27b03a98825d1e2e7"},"type":"cosign container image signature"},"optional":null}`), + }, + }, + expectedResultMessagePrefix: "cosign verification success", + }, + { + name: "valid hash: 256 keysize: 3072 RSA key", + keys: map[PKKey]keymanagementprovider.PublicKey{ + {Provider: "test"}: {Key: validRSA384PubKey}, + }, + cosignOpts: cosign.CheckOpts{ + IgnoreSCT: true, + IgnoreTlog: true, + }, + getKeysError: false, + store: &mocks.MemoryTestStore{ + Manifests: map[digest.Digest]ocispecs.ReferenceManifest{ + testRefDigest: { + MediaType: refDescriptor.MediaType, + Blobs: []imgspec.Descriptor{ + { + Size: 267, + Digest: "sha256:6e1ffef2ba058cda5d1aa7ed792cb1e63b4207d8195a469bee1b5fc662cd9b70", + MediaType: cosignMediaType, + Annotations: map[string]string{ + static.SignatureAnnotationKey: "fP5+FQcc59WjqDAcvcgfHBZbu/FfQYh+ZjgwuEwLj/y0ku2S+rFbk8XE2gPZ4mcgT9Bceu+UMY/pYLqNI7ngkXMamYg1gzsTPrAG5DpEbApGMDiQyOlCcEFqgJbxqFOmg+HD9eSOMmibFbUh8XMt4LuyZIjmcCqJ22i8B49y8LFo6QiE64/jjhNLlRK4LvDTSUGDJ4VXW+c9y/PxbpZxtHIVyIYK82qL8P2/BuRxQ9ZVKJE1eFdz3Suz0ZIQmhkimLqQdOOxoGFcO4syjHYzfneBNvySWNxVXJCjw86DJqsDl5se+mY2Zww13DihfQX0cKSGGVfRoMgvIQOeaMNyFaCad2BQFfraqVUU5p7v0FqO6r0FU9z0ixRj81xVKJA3GPUZdF1ImcwOE4cOuQYARE6aiw78t2vrW5PRGtRPWpu+JY1+2v5m61w60G9HAozpnucWG3u9agdhwwD6VLJzPduVdnZr8t1WN8BpZs5NA3n4wkrlmRpnYtw7MqupaJQ2", + }, + }, + }, + }, + }, + Subjects: map[digest.Digest]*ocispecs.SubjectDescriptor{ + subjectDigest: { + Descriptor: imgspec.Descriptor{ + Digest: subjectDigest, + MediaType: imgspec.MediaTypeImageManifest, + }, + }, + }, + Blobs: map[digest.Digest][]byte{ + "sha256:6e1ffef2ba058cda5d1aa7ed792cb1e63b4207d8195a469bee1b5fc662cd9b70": []byte(`{"critical":{"identity":{"docker-reference":"artifactstest.azurecr.io/4-15-24/cosign/hello-world"},"image":{"docker-manifest-digest":"sha256:d37ada95d47ad12224c205a938129df7a3e52345828b4fa27b03a98825d1e2e7"},"type":"cosign container image signature"},"optional":null}`), + }, + }, + expectedResultMessagePrefix: "cosign verification success", + }, + { + name: "valid hash: 256 curve: P256 ECDSA key AKV", + keys: map[PKKey]keymanagementprovider.PublicKey{ + {Provider: "test"}: {Key: validECDSAP256PubKey, ProviderType: azurekeyvault.ProviderName}, + }, + cosignOpts: cosign.CheckOpts{ + IgnoreSCT: true, + IgnoreTlog: true, + }, + getKeysError: false, + store: &mocks.MemoryTestStore{ + Manifests: map[digest.Digest]ocispecs.ReferenceManifest{ + testRefDigest: { + MediaType: refDescriptor.MediaType, + Blobs: []imgspec.Descriptor{ + { + Size: 267, + Digest: "sha256:6e1ffef2ba058cda5d1aa7ed792cb1e63b4207d8195a469bee1b5fc662cd9b70", + MediaType: cosignMediaType, + Annotations: map[string]string{ + static.SignatureAnnotationKey: "MEYCIQDCMOtZXzsgZknsOhcv1VC7cN72xuBr16GU98bT0tXWdQIhAJp9X9jh4sIG1xhmoaYwGGkl1/8EQW7zqFUpMkEoi3s1", + }, + }, + }, + }, + }, + Subjects: map[digest.Digest]*ocispecs.SubjectDescriptor{ + subjectDigest: { + Descriptor: imgspec.Descriptor{ + Digest: subjectDigest, + MediaType: imgspec.MediaTypeImageManifest, + }, + }, + }, + Blobs: map[digest.Digest][]byte{ + "sha256:6e1ffef2ba058cda5d1aa7ed792cb1e63b4207d8195a469bee1b5fc662cd9b70": []byte(`{"critical":{"identity":{"docker-reference":"artifactstest.azurecr.io/4-15-24/cosign/hello-world"},"image":{"docker-manifest-digest":"sha256:d37ada95d47ad12224c205a938129df7a3e52345828b4fa27b03a98825d1e2e7"},"type":"cosign container image signature"},"optional":null}`), + }, + }, + expectedResultMessagePrefix: "cosign verification success", + }, + { + name: "valid hash: 256 curve: P384 ECDSA key", + keys: map[PKKey]keymanagementprovider.PublicKey{ + {Provider: "test"}: {Key: validECDSAP384PubKey}, + }, + cosignOpts: cosign.CheckOpts{ + IgnoreSCT: true, + IgnoreTlog: true, + }, + getKeysError: false, + store: &mocks.MemoryTestStore{ + Manifests: map[digest.Digest]ocispecs.ReferenceManifest{ + testRefDigest: { + MediaType: refDescriptor.MediaType, + Blobs: []imgspec.Descriptor{ + { + Size: 267, + Digest: "sha256:6e1ffef2ba058cda5d1aa7ed792cb1e63b4207d8195a469bee1b5fc662cd9b70", + MediaType: cosignMediaType, + Annotations: map[string]string{ + static.SignatureAnnotationKey: "MGUCMQC6Z7RgD2uxG5IiqKoOmrjTRVqBn+XqSjHU5oSI/RNAl9FBrM5HuzZm6cMmlp40jIoCMHKeH42xtJBTOPzbkG/z9aWaNagjn8jEFKWB28w4hjufN6NG1QReF2ai7befjTnRmg==", + }, + }, + }, + }, + }, + Subjects: map[digest.Digest]*ocispecs.SubjectDescriptor{ + subjectDigest: { + Descriptor: imgspec.Descriptor{ + Digest: subjectDigest, + MediaType: imgspec.MediaTypeImageManifest, + }, + }, + }, + Blobs: map[digest.Digest][]byte{ + "sha256:6e1ffef2ba058cda5d1aa7ed792cb1e63b4207d8195a469bee1b5fc662cd9b70": []byte(`{"critical":{"identity":{"docker-reference":"artifactstest.azurecr.io/4-15-24/cosign/hello-world"},"image":{"docker-manifest-digest":"sha256:d37ada95d47ad12224c205a938129df7a3e52345828b4fa27b03a98825d1e2e7"},"type":"cosign container image signature"},"optional":null}`), + }, + }, + expectedResultMessagePrefix: "cosign verification success", + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + getKeysMaps = func(_ context.Context, _ *TrustPolicies, _ string, _ string) (map[PKKey]keymanagementprovider.PublicKey, cosign.CheckOpts, error) { + if tt.getKeysError { + return nil, cosign.CheckOpts{}, fmt.Errorf("error") + } + + return tt.keys, tt.cosignOpts, nil + } + verifierFactory := cosignVerifierFactory{} + trustPoliciesConfig := []TrustPolicyConfig{ + { + Name: "test-policy", + Keyless: KeylessConfig{RekorURL: DefaultRekorURL}, + Scopes: []string{"*"}, + }, + } + validConfig := config.VerifierConfig{ + "name": "test", + "artifactTypes": "testtype", + "type": "cosign", + "trustPolicies": trustPoliciesConfig, + } + cosignVerifier, err := verifierFactory.Create("", validConfig, "", "test-namespace") + if err != nil { + t.Fatalf("Create() error = %v", err) + } + result, _ := cosignVerifier.Verify(context.Background(), subjectRef, refDescriptor, tt.store) + if !strings.HasPrefix(result.Message, tt.expectedResultMessagePrefix) { + t.Errorf("Verify() = %v, want %v", result.Message, tt.expectedResultMessagePrefix) + } + }) + } +} diff --git a/pkg/verifier/cosign/trustpolicies.go b/pkg/verifier/cosign/trustpolicies.go new file mode 100644 index 000000000..a83bc585b --- /dev/null +++ b/pkg/verifier/cosign/trustpolicies.go @@ -0,0 +1,162 @@ +/* +Copyright The Ratify Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cosign + +import ( + "fmt" + "regexp" + "slices" + "strings" + + re "github.com/deislabs/ratify/errors" +) + +type TrustPolicies struct { + policies []TrustPolicy +} + +const GlobalWildcardCharacter = '*' + +var validScopeRegex = regexp.MustCompile(`^[a-z0-9\.\-:@\/]*\*?$`) + +// CreateTrustPolicies creates a set of trust policies from the given configuration +func CreateTrustPolicies(configs []TrustPolicyConfig, verifierName string) (*TrustPolicies, error) { + if configs == nil { + return nil, re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail("failed to create trust policies: no policies found") + } + + if len(configs) == 0 { + return nil, re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail("failed to create trust policies: no policies defined") + } + + policies := make([]TrustPolicy, 0, len(configs)) + names := make(map[string]struct{}) + for _, policyConfig := range configs { + if _, ok := names[policyConfig.Name]; ok { + return nil, re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail(fmt.Sprintf("failed to create trust policies: duplicate policy name %s", policyConfig.Name)) + } + names[policyConfig.Name] = struct{}{} + policy, err := CreateTrustPolicy(policyConfig, verifierName) + if err != nil { + return nil, err + } + policies = append(policies, policy) + } + + if len(policies) == 0 { + return nil, re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail("failed to create trust policies: no trust policies defined") + } + + err := validateScopes(policies) + if err != nil { + return nil, err + } + + return &TrustPolicies{ + policies: policies, + }, nil +} + +// GetScopedPolicy returns the policy that applies to the given reference +func (tps *TrustPolicies) GetScopedPolicy(reference string) (TrustPolicy, error) { + var globalPolicy TrustPolicy + for _, policy := range tps.policies { + scopes := policy.GetScopes() + for _, scope := range scopes { + if scope == string(GlobalWildcardCharacter) { + // if global wildcard character is used, save the policy and continue + globalPolicy = policy + continue + } + if strings.HasSuffix(scope, string(GlobalWildcardCharacter)) { + if strings.HasPrefix(reference, strings.TrimSuffix(scope, string(GlobalWildcardCharacter))) { + return policy, nil + } + } else if reference == scope { + return policy, nil + } + } + } + // if no scoped policy is found, return the global policy if it exists + if globalPolicy != nil { + return globalPolicy, nil + } + return nil, re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithDetail(fmt.Sprintf("failed to get trust policy: no policy found for reference %s", reference)) +} + +// validateScopes validates the scopes in the trust policies +func validateScopes(policies []TrustPolicy) error { + scopesMap := make(map[string]TrustPolicy) + hasGlobalWildcard := false + for _, policy := range policies { + policyName := policy.GetName() + scopes := policy.GetScopes() + if len(scopes) == 0 { + return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithDetail(fmt.Sprintf("failed to create trust policies: no scopes defined for trust policy %s", policyName)) + } + // check for global wildcard character along with other scopes in the same policy + if len(scopes) > 1 && slices.Contains(scopes, string(GlobalWildcardCharacter)) { + return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithDetail(fmt.Sprintf("failed to create trust policies: global wildcard character %c cannot be used with other scopes within the same trust policy %s", GlobalWildcardCharacter, policyName)) + } + // check for duplicate global wildcard characters across policies + if slices.Contains(scopes, string(GlobalWildcardCharacter)) { + if hasGlobalWildcard { + return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithDetail(fmt.Sprintf("failed to create trust policies: global wildcard character %c can only be used once", GlobalWildcardCharacter)) + } + hasGlobalWildcard = true + continue + } + for _, scope := range scopes { + // check for empty scope + if scope == "" { + return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithDetail(fmt.Sprintf("failed to create trust policies: scope defined is empty for trust policy %s", policyName)) + } + // check scope is formatted correctly + if !validScopeRegex.MatchString(scope) { + return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithDetail(fmt.Sprintf("failed to create trust policies: invalid scope %s for trust policy %s", scope, policyName)) + } + // check for duplicate scopes + if _, ok := scopesMap[scope]; ok { + return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithDetail(fmt.Sprintf("failed to create trust policies: duplicate scope %s for trust policy %s", scope, policyName)) + } + // check wildcard overlaps + for existingScope := range scopesMap { + isConflict := false + trimmedScope := strings.TrimSuffix(scope, string(GlobalWildcardCharacter)) + trimmedExistingScope := strings.TrimSuffix(existingScope, string(GlobalWildcardCharacter)) + if existingScope[len(existingScope)-1] == GlobalWildcardCharacter && scope[len(scope)-1] == GlobalWildcardCharacter { + // if both scopes have wildcard characters, check if they overlap + if len(scope) < len(existingScope) { + isConflict = strings.HasPrefix(trimmedExistingScope, trimmedScope) + } else { + isConflict = strings.HasPrefix(trimmedScope, trimmedExistingScope) + } + } else if existingScope[len(existingScope)-1] == GlobalWildcardCharacter { + // if existing scope has wildcard character, check if it overlaps with the new absolute scope + isConflict = strings.HasPrefix(scope, trimmedExistingScope) + } else if scope[len(scope)-1] == GlobalWildcardCharacter { + // if new scope has wildcard character, check if it overlaps with the existing absolute scope + isConflict = strings.HasPrefix(existingScope, trimmedScope) + } + if isConflict { + return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithDetail(fmt.Sprintf("failed to create trust policies: overlapping scopes %s and %s for trust policy %s", scope, existingScope, policyName)) + } + } + scopesMap[scope] = policy + } + } + return nil +} diff --git a/pkg/verifier/cosign/trustpolicies_test.go b/pkg/verifier/cosign/trustpolicies_test.go new file mode 100644 index 000000000..cdea65b8a --- /dev/null +++ b/pkg/verifier/cosign/trustpolicies_test.go @@ -0,0 +1,449 @@ +/* +Copyright The Ratify Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cosign + +import ( + "testing" +) + +// TestCreateTrustPolicies tests the CreateTrustPolicies function +func TestCreateTrustPolicies(t *testing.T) { + tc := []struct { + name string + policyConfigs []TrustPolicyConfig + wantErr bool + }{ + { + name: "valid policy", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/ratify:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: false, + }, + { + name: "nil policy", + policyConfigs: nil, + wantErr: true, + }, + { + name: "empty policy", + policyConfigs: []TrustPolicyConfig{}, + wantErr: true, + }, + { + name: "valid multiple policies", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/ratify:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + { + Name: "test-2", + Scopes: []string{"ghcr.io/deislabs/ratify:v2"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: false, + }, + { + name: "invalid policy scopes", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/ratify:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + { + Name: "test-2", + Scopes: []string{"ghcr.io/deislabs/ratify:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: true, + }, + { + name: "invalid policy duplicate names", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/ratify:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/ratify:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: true, + }, + { + name: "invalid policy invalid trust policy config", + policyConfigs: []TrustPolicyConfig{ + { + Scopes: []string{"ghcr.io/deislabs/ratify:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/ratify:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: true, + }, + } + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + _, err := CreateTrustPolicies(tt.policyConfigs, "test-verifier") + if (err != nil) != tt.wantErr { + t.Errorf("CreateTrustPolicies() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +// TestGetScopedPolicy tests the GetScopedPolicy functions +func TestGetScopedPolicy(t *testing.T) { + tc := []struct { + name string + policyConfigs []TrustPolicyConfig + reference string + wantErr bool + wantPolicyName string + }{ + { + name: "valid policy", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/ratify:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + { + Name: "test-2", + Scopes: []string{"ghcr.io/deislabs/ratify:v2"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + reference: "ghcr.io/deislabs/ratify:v1", + wantErr: false, + wantPolicyName: "test", + }, + { + name: "valid policy wildcards", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/ratify:*"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + { + Name: "test-2", + Scopes: []string{"ghcr.io/deislabs/ratify2:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + reference: "ghcr.io/deislabs/ratify:v1", + wantErr: false, + wantPolicyName: "test", + }, + { + name: "no matching policy", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/ratify:*"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + { + Name: "test-2", + Scopes: []string{"ghcr.io/deislabs/ratify2:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + reference: "ghcr.io/deislabs/ratify3:v1", + wantErr: true, + wantPolicyName: "", + }, + { + name: "default to global wildcard policy if exists", + policyConfigs: []TrustPolicyConfig{ + { + Name: "global", + Scopes: []string{"*"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + { + Name: "test-2", + Scopes: []string{"ghcr.io/deislabs/ratify2:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + reference: "ghcr.io/deislabs/ratify3:v1", + wantErr: false, + wantPolicyName: "global", + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + policies, err := CreateTrustPolicies(tt.policyConfigs, "test-verifier") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + policy, err := policies.GetScopedPolicy(tt.reference) + if (err != nil) != tt.wantErr { + t.Errorf("GetScopedPolicy() error = %v, wantErr %v", err, tt.wantErr) + } + if err == nil && policy.GetName() != tt.wantPolicyName { + t.Errorf("GetScopedPolicy() policy name = %v, want %v", policy.GetName(), tt.wantPolicyName) + } + }) + } +} + +// TestValidateScopes tests the validateScopes function +func TestValidateScopes(t *testing.T) { + tc := []struct { + name string + policyConfigs []TrustPolicyConfig + wantErr bool + }{ + { + name: "valid absolute scope", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/ratify:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: false, + }, + { + name: "multiple valid absolute scopes", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/ratify:v1", "ghcr.io/deislabs/ratify:v2"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: false, + }, + { + name: "valid wild card scope", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/ratify:*"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: false, + }, + { + name: "multiple valid wild card scopes", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/ratify:*"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: false, + }, + { + name: "valid global wild card scope", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"*"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: false, + }, + { + name: "invalid global wild card scope with other scopes", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"*", "somescope"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: true, + }, + { + name: "invalid absolute scope duplicate", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/ratify:v1", "ghcr.io/deislabs/ratify:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: true, + }, + { + name: "invalid absolute scope duplicate across policies", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/ratify:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + { + Name: "test-2", + Scopes: []string{"ghcr.io/deislabs/ratify:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: true, + }, + { + name: "invalid global wildcard scope with duplicate wild card scope", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"*"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + { + Name: "test-2", + Scopes: []string{"*"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: true, + }, + { + name: "invalid wild card scope duplicate", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/ratify:*", "ghcr.io/deislabs/ratify:*"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: true, + }, + { + name: "invalid wild card scope prefix", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"*.azurecr.io"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: true, + }, + { + name: "invalid wild card character middle of scope", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/*/ratify:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: true, + }, + { + name: "invalid wildcard overlap scopes", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/*", "ghcr.io/deislabs/*"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: true, + }, + { + name: "valid wildcard no overlap wildcard scopes", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/*"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + { + Name: "test-2", + Scopes: []string{"ghcr.io/test/*"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: false, + }, + { + name: "invalid wildcard and absolute overlap", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/*"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + { + Name: "test-2", + Scopes: []string{"ghcr.io/deislabs/ratify:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: true, + }, + { + name: "invalid wildcard and absolute overlap reverse order", + policyConfigs: []TrustPolicyConfig{ + { + Name: "test", + Scopes: []string{"ghcr.io/deislabs/ratify:v1"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + { + Name: "test-2", + Scopes: []string{"ghcr.io/deislabs/ratify:*"}, + Keyless: KeylessConfig{RekorURL: "https://rekor.sigstore.dev"}, + }, + }, + wantErr: true, + }, + } + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + policies := make([]TrustPolicy, 0, len(tt.policyConfigs)) + for _, policyConfig := range tt.policyConfigs { + policy, err := CreateTrustPolicy(policyConfig, "test-verifier") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + policies = append(policies, policy) + } + err := validateScopes(policies) + if (err != nil) != tt.wantErr { + t.Errorf("validateScopes() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/verifier/cosign/trustpolicy.go b/pkg/verifier/cosign/trustpolicy.go new file mode 100644 index 000000000..b990d20a1 --- /dev/null +++ b/pkg/verifier/cosign/trustpolicy.go @@ -0,0 +1,273 @@ +/* +Copyright The Ratify Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cosign + +import ( + "context" + "crypto" + "fmt" + "os" + + re "github.com/deislabs/ratify/errors" + "github.com/deislabs/ratify/internal/constants" + "github.com/deislabs/ratify/pkg/keymanagementprovider" + vu "github.com/deislabs/ratify/pkg/verifier/utils" + "github.com/deislabs/ratify/utils" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/fulcio" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor" + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +type KeyConfig struct { + Provider string `json:"provider,omitempty"` + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` + File string `json:"file,omitempty"` +} + +type KeylessConfig struct { + RekorURL string `json:"rekorURL,omitempty"` + CTLogVerify *bool `json:"ctLogVerify,omitempty"` +} + +type TrustPolicyConfig struct { + Name string `json:"name"` + Scopes []string `json:"scopes"` + Keys []KeyConfig `json:"keys,omitempty"` + Keyless KeylessConfig `json:"keyless,omitempty"` + TLogVerify *bool `json:"tLogVerify,omitempty"` +} + +type PKKey struct { + Provider string `json:"provider"` + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` +} + +type trustPolicy struct { + scopes []string + localKeys map[PKKey]keymanagementprovider.PublicKey + config TrustPolicyConfig + verifierName string + isKeyless bool +} + +type TrustPolicy interface { + GetName() string + GetKeys(string) (map[PKKey]keymanagementprovider.PublicKey, error) + GetScopes() []string + GetCosignOpts(context.Context) (cosign.CheckOpts, error) +} + +const ( + fileProviderName string = "file" + DefaultRekorURL string = "https://rekor.sigstore.dev" + DefaultTLogVerify bool = true + DefaultCTLogVerify bool = true +) + +// CreateTrustPolicy creates a trust policy from the given configuration +// returns an error if the configuration is invalid +// reads the public keys from the file path +func CreateTrustPolicy(config TrustPolicyConfig, verifierName string) (TrustPolicy, error) { + if err := validate(config, verifierName); err != nil { + return nil, err + } + + keyMap := make(map[PKKey]keymanagementprovider.PublicKey) + for _, keyConfig := range config.Keys { + // check if the key is defined by file path or by key management provider + if keyConfig.File != "" { + pubKey, err := loadKeyFromPath(keyConfig.File) + if err != nil { + return nil, re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail(fmt.Sprintf("trust policy %s failed: failed to load key from file %s", config.Name, keyConfig.File)).WithError(err) + } + keyMap[PKKey{Provider: fileProviderName, Name: keyConfig.File}] = keymanagementprovider.PublicKey{Key: pubKey, ProviderType: fileProviderName} + } + } + + if config.Keyless.RekorURL == "" { + config.Keyless.RekorURL = DefaultRekorURL + } + + if config.TLogVerify == nil { + config.TLogVerify = utils.MakePtr(DefaultTLogVerify) + } + + if config.Keyless != (KeylessConfig{}) && config.Keyless.CTLogVerify == nil { + config.Keyless.CTLogVerify = utils.MakePtr(DefaultCTLogVerify) + } + + return &trustPolicy{ + scopes: config.Scopes, + localKeys: keyMap, + config: config, + verifierName: verifierName, + isKeyless: config.Keyless != KeylessConfig{}, + }, nil +} + +// GetName returns the name of the trust policy +func (tp *trustPolicy) GetName() string { + return tp.config.Name +} + +// GetKeys returns the public keys defined in the trust policy +func (tp *trustPolicy) GetKeys(namespace string) (map[PKKey]keymanagementprovider.PublicKey, error) { + keyMap := make(map[PKKey]keymanagementprovider.PublicKey) + // preload the local keys into the map of keys to be returned + for key, pubKey := range tp.localKeys { + keyMap[key] = pubKey + } + + for _, keyConfig := range tp.config.Keys { + // if the key is defined by file path, it has already been loaded into the key map + if keyConfig.File != "" { + continue + } + // must prepend namespace to key management provider name if not provided since namespace is prepended during key management provider intialization + namespacedKMP := prependNamespaceToKMPName(keyConfig.Provider, namespace) + // get the key management provider resource which contains a map of keys + kmpResource, ok := keymanagementprovider.GetKeysFromMap(namespacedKMP) + if !ok { + return nil, re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(tp.verifierName).WithDetail(fmt.Sprintf("trust policy %s failed: key management provider %s not found", tp.config.Name, namespacedKMP)) + } + // get a specific key from the key management provider resource + if keyConfig.Name != "" { + pubKey, exists := kmpResource[keymanagementprovider.KMPMapKey{Name: keyConfig.Name, Version: keyConfig.Version}] + if !exists { + return nil, re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(tp.verifierName).WithDetail(fmt.Sprintf("trust policy %s failed: key %s with version %s not found in key management provider %s", tp.config.Name, keyConfig.Name, keyConfig.Version, namespacedKMP)) + } + keyMap[PKKey{Provider: namespacedKMP, Name: keyConfig.Name, Version: keyConfig.Version}] = pubKey + } else { + // get all public keys from the key management provider + for key, pubKey := range kmpResource { + keyMap[PKKey{Provider: namespacedKMP, Name: key.Name, Version: key.Version}] = pubKey + } + } + } + return keyMap, nil +} + +// GetScopes returns the scopes defined in the trust policy +func (tp *trustPolicy) GetScopes() []string { + return tp.scopes +} + +func (tp *trustPolicy) GetCosignOpts(ctx context.Context) (cosign.CheckOpts, error) { + cosignOpts := cosign.CheckOpts{} + if tp.isKeyless { + roots, err := fulcio.GetRoots() + if err != nil || roots == nil { + return cosignOpts, fmt.Errorf("failed to get fulcio roots: %w", err) + } + cosignOpts.RootCerts = roots + cosignOpts.RekorClient, err = rekor.NewClient(tp.config.Keyless.RekorURL) + if err != nil { + return cosignOpts, fmt.Errorf("failed to create Rekor client from URL %s: %w", tp.config.Keyless.RekorURL, err) + } + if tp.config.Keyless.CTLogVerify != nil && *tp.config.Keyless.CTLogVerify { + cosignOpts.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) + if err != nil { + return cosignOpts, fmt.Errorf("failed to fetch certificate transparency log public keys: %w", err) + } + } else { + cosignOpts.IgnoreSCT = true + } + // Fetches the Rekor public keys from the Rekor server + cosignOpts.RekorPubKeys, err = cosign.GetRekorPubs(ctx) + if err != nil { + return cosignOpts, fmt.Errorf("failed to fetch Rekor public keys: %w", err) + } + cosignOpts.IntermediateCerts, err = fulcio.GetIntermediates() + if err != nil { + return cosignOpts, fmt.Errorf("failed to get fulcio intermediate certificates: %w", err) + } + } + if tp.config.TLogVerify != nil && *tp.config.TLogVerify { + cosignOpts.IgnoreTlog = true + } + return cosignOpts, nil +} + +// validate checks if the trust policy configuration is valid +// returns an error if the configuration is invalid +func validate(config TrustPolicyConfig, verifierName string) error { + if config.Name == "" { + return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail("missing trust policy name") + } + + if len(config.Scopes) == 0 { + return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail(fmt.Sprintf("trust policy %s failed: no scopes defined", config.Name)) + } + + // keys or keyless must be defined + if len(config.Keys) == 0 && config.Keyless == (KeylessConfig{}) { + return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail(fmt.Sprintf("trust policy %s failed: no keys defined and keyless section not configured", config.Name)) + } + + // only one of keys or keyless can be defined + if len(config.Keys) > 0 && config.Keyless != (KeylessConfig{}) { + return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail(fmt.Sprintf("trust policy %s failed: both keys and keyless sections are defined", config.Name)) + } + + for _, keyConfig := range config.Keys { + // check if the key is defined by file path or by key management provider + if keyConfig.File == "" && keyConfig.Provider == "" { + return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail(fmt.Sprintf("trust policy %s failed: key management provider name is required when not using file path", config.Name)) + } + // both file path and key management provider cannot be defined together + if keyConfig.File != "" && keyConfig.Provider != "" { + return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail(fmt.Sprintf("trust policy %s failed: 'name' and 'file' cannot be configured together", config.Name)) + } + // key management provider is required when specific keys are configured + if keyConfig.Name != "" && keyConfig.Provider == "" { + return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail(fmt.Sprintf("trust policy %s failed: key management provider name is required when key name is defined", config.Name)) + } + // key name is required when key version is defined + if keyConfig.Version != "" && keyConfig.Name == "" { + return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail(fmt.Sprintf("trust policy %s failed: key name is required when key version is defined", config.Name)) + } + } + + return nil +} + +// loadKeyFromPath loads a public key from a file path and returns it +// TODO: look into supporting cosign's blob.LoadFileOrURL to support URL + env variables +func loadKeyFromPath(filePath string) (crypto.PublicKey, error) { + contents, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + if len(contents) == 0 { + return nil, fmt.Errorf("key file %s is empty", filePath) + } + + return cryptoutils.UnmarshalPEMToPublicKey(contents) +} + +// prependNamespaceToKMPName prepends the namespace to the key management provider name if not already present +// if the namespace is empty, the key management provider name is returned as is +func prependNamespaceToKMPName(kmpName string, namespace string) string { + // namespace will be empty for CLI scenarios. use the KMP name as is + if vu.IsNamespacedNamed(kmpName) || namespace == "" { + return kmpName + } + return fmt.Sprintf("%s%s%s", namespace, constants.NamespaceSeperator, kmpName) +} diff --git a/pkg/verifier/cosign/trustpolicy_test.go b/pkg/verifier/cosign/trustpolicy_test.go new file mode 100644 index 000000000..5d658dd15 --- /dev/null +++ b/pkg/verifier/cosign/trustpolicy_test.go @@ -0,0 +1,361 @@ +/* +Copyright The Ratify Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cosign + +import ( + "crypto" + "crypto/ecdsa" + "testing" + + "github.com/deislabs/ratify/pkg/keymanagementprovider" +) + +func TestCreateTrustPolicy(t *testing.T) { + tc := []struct { + name string + cfg TrustPolicyConfig + wantErr bool + }{ + { + name: "invalid config", + cfg: TrustPolicyConfig{}, + wantErr: true, + }, + { + name: "invalid local key path", + cfg: TrustPolicyConfig{ + Name: "test", + Scopes: []string{"*"}, + Keys: []KeyConfig{ + { + File: "invalid", + }, + }, + }, + wantErr: true, + }, + { + name: "valid local key path", + cfg: TrustPolicyConfig{ + Name: "test", + Scopes: []string{"*"}, + Keys: []KeyConfig{ + { + File: "../../../test/testdata/cosign.pub", + }, + }, + }, + wantErr: false, + }, + { + name: "valid keyless config with rekor specified", + cfg: TrustPolicyConfig{ + Name: "test", + Scopes: []string{"*"}, + Keyless: KeylessConfig{ + RekorURL: DefaultRekorURL, + }, + }, + wantErr: false, + }, + } + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + _, err := CreateTrustPolicy(tt.cfg, "test-verifier") + if (err != nil) != tt.wantErr { + t.Fatalf("expected %v, got %v", tt.wantErr, err) + } + }) + } +} + +// TestGetName tests the GetName function for Trust Policy +func TestGetName(t *testing.T) { + trustPolicyConfig := TrustPolicyConfig{ + Name: "test", + Scopes: []string{"*"}, + Keyless: KeylessConfig{RekorURL: DefaultRekorURL}, + } + trustPolicy, err := CreateTrustPolicy(trustPolicyConfig, "test-verifier") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if trustPolicy.GetName() != trustPolicyConfig.Name { + t.Fatalf("expected %s, got %s", trustPolicyConfig.Name, trustPolicy.GetName()) + } +} + +// TestGetScopes tests the GetScopes function for Trust Policy +func TestGetScopes(t *testing.T) { + trustPolicyConfig := TrustPolicyConfig{ + Name: "test", + Scopes: []string{"*"}, + Keyless: KeylessConfig{RekorURL: DefaultRekorURL}, + } + trustPolicy, err := CreateTrustPolicy(trustPolicyConfig, "test-verifier") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(trustPolicy.GetScopes()) != len(trustPolicyConfig.Scopes) { + t.Fatalf("expected %v, got %v", trustPolicyConfig.Scopes, trustPolicy.GetScopes()) + } + if trustPolicy.GetScopes()[0] != trustPolicyConfig.Scopes[0] { + t.Fatalf("expected %s, got %s", trustPolicyConfig.Scopes[0], trustPolicy.GetScopes()[0]) + } +} + +// TestGetKeys tests the GetKeys function for Trust Policy +func TestGetKeys(t *testing.T) { + inputMap := map[keymanagementprovider.KMPMapKey]crypto.PublicKey{ + {Name: "key1"}: &ecdsa.PublicKey{}, + } + keymanagementprovider.SetKeysInMap("ns/kmp", "", inputMap) + tc := []struct { + name string + cfg TrustPolicyConfig + wantErr bool + }{ + { + name: "only local keys", + cfg: TrustPolicyConfig{ + Name: "test", + Scopes: []string{"*"}, + Keys: []KeyConfig{ + { + File: "../../../test/testdata/cosign.pub", + }, + }, + }, + wantErr: false, + }, + { + name: "nonexistent KMP", + cfg: TrustPolicyConfig{ + Name: "test", + Scopes: []string{"*"}, + Keys: []KeyConfig{ + { + Provider: "nonexistent", + }, + }, + }, + wantErr: true, + }, + { + name: "valid KMP", + cfg: TrustPolicyConfig{ + Name: "test", + Scopes: []string{"*"}, + Keys: []KeyConfig{ + { + Provider: "kmp", + Name: "key1", + }, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + trustPolicy, err := CreateTrustPolicy(tt.cfg, "test-verifier") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + keys, err := trustPolicy.GetKeys("ns") + if (err != nil) != tt.wantErr { + t.Fatalf("expected %v, got %v", tt.wantErr, err) + } + if err == nil && len(keys) != len(tt.cfg.Keys) { + t.Fatalf("expected %v, got %v", tt.cfg.Keys, keys) + } + }) + } +} + +// TestValidate tests the validate function +func TestValidate(t *testing.T) { + tc := []struct { + name string + policyConfig TrustPolicyConfig + wantErr bool + }{ + { + name: "no name", + policyConfig: TrustPolicyConfig{}, + wantErr: true, + }, + { + name: "no scopes", + policyConfig: TrustPolicyConfig{ + Name: "test", + }, + wantErr: true, + }, + { + name: "no keys or keyless defined", + policyConfig: TrustPolicyConfig{ + Name: "test", + Scopes: []string{"*"}, + }, + wantErr: true, + }, + { + name: "keys and keyless defined", + policyConfig: TrustPolicyConfig{ + Name: "test", + Scopes: []string{"*"}, + Keys: []KeyConfig{ + { + Provider: "kmp", + }, + }, + Keyless: KeylessConfig{RekorURL: DefaultRekorURL}, + }, + wantErr: true, + }, + { + name: "key provider and key path not defined", + policyConfig: TrustPolicyConfig{ + Name: "test", + Scopes: []string{"*"}, + Keys: []KeyConfig{{}}, + }, + wantErr: true, + }, + { + name: "key provider and key path both defined", + policyConfig: TrustPolicyConfig{ + Name: "test", + Scopes: []string{"*"}, + Keys: []KeyConfig{ + { + Provider: "kmp", + File: "path", + }, + }, + }, + wantErr: true, + }, + { + name: "key provider not defined but name defined", + policyConfig: TrustPolicyConfig{ + Name: "test", + Scopes: []string{"*"}, + Keys: []KeyConfig{ + { + Name: "key name", + }, + }, + }, + wantErr: true, + }, + { + name: "key provider name not defined but version defined", + policyConfig: TrustPolicyConfig{ + Name: "test", + Scopes: []string{"*"}, + Keys: []KeyConfig{ + { + Provider: "kmp", + Version: "key version", + }, + }, + }, + wantErr: true, + }, + { + name: "valid", + policyConfig: TrustPolicyConfig{ + Name: "test", + Scopes: []string{"*"}, + Keys: []KeyConfig{ + { + Provider: "kmp", + Name: "key name", + Version: "key version", + }, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + actual := validate(tt.policyConfig, "test-verifier") + if (actual != nil) != tt.wantErr { + t.Fatalf("expected %v, got %v", tt.wantErr, actual) + } + }) + } +} + +// TestLoadKeyFromPath tests the loadKeyFromPath function +func TestLoadKeyFromPath(t *testing.T) { + cosignValidPath := "../../../test/testdata/cosign.pub" + key, err := loadKeyFromPath(cosignValidPath) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if key == nil { + t.Fatalf("expected key, got nil") + } + switch keyType := key.(type) { + case *ecdsa.PublicKey: + default: + t.Fatalf("expected ecdsa.PublicKey, got %v", keyType) + } +} + +// TestPrependNamespaceToKMPName tests the prependNamespaceToKMPName function +func TestPrependNamespaceToKMPName(t *testing.T) { + tc := []struct { + name string + kmpName string + ns string + expected string + }{ + { + name: "empty namespace", + kmpName: "kmp", + ns: "", + expected: "kmp", + }, + { + name: "non-empty namespace", + kmpName: "kmp", + ns: "ns", + expected: "ns/kmp", + }, + { + name: "namespaced kmp", + kmpName: "ns/kmp", + ns: "ns", + expected: "ns/kmp", + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + actual := prependNamespaceToKMPName(tt.kmpName, tt.ns) + if actual != tt.expected { + t.Fatalf("expected %s, got %s", tt.expected, actual) + } + }) + } +} diff --git a/pkg/verifier/notation/notation.go b/pkg/verifier/notation/notation.go index 678f35f56..10997c848 100644 --- a/pkg/verifier/notation/notation.go +++ b/pkg/verifier/notation/notation.go @@ -37,6 +37,7 @@ import ( "github.com/deislabs/ratify/pkg/verifier/types" "github.com/notaryproject/notation-go/log" + vu "github.com/deislabs/ratify/pkg/verifier/utils" _ "github.com/notaryproject/notation-core-go/signature/cose" // register COSE signature _ "github.com/notaryproject/notation-core-go/signature/jws" // register JWS signature "github.com/notaryproject/notation-go" @@ -228,15 +229,10 @@ func prependNamespaceToCertStore(verificationCertStore map[string][]string, name for _, certStores := range verificationCertStore { for i, certstore := range certStores { - if !isNamespacedNamed(certstore) { + if !vu.IsNamespacedNamed(certstore) { certStores[i] = namespace + constants.NamespaceSeperator + certstore } } } return verificationCertStore, nil } - -// return true if string looks like a K8s namespaced resource. e.g. namespace/name -func isNamespacedNamed(name string) bool { - return strings.Contains(name, constants.NamespaceSeperator) -} diff --git a/pkg/verifier/utils/utils.go b/pkg/verifier/utils/utils.go new file mode 100644 index 000000000..07e9068e5 --- /dev/null +++ b/pkg/verifier/utils/utils.go @@ -0,0 +1,27 @@ +/* +Copyright The Ratify Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "strings" + + "github.com/deislabs/ratify/internal/constants" +) + +// return true if string looks like a K8s namespaced resource. e.g. namespace/name +func IsNamespacedNamed(name string) bool { + return strings.Contains(name, constants.NamespaceSeperator) +} diff --git a/scripts/azure-ci-test.sh b/scripts/azure-ci-test.sh index 347be097d..e97175a8c 100755 --- a/scripts/azure-ci-test.sh +++ b/scripts/azure-ci-test.sh @@ -34,6 +34,7 @@ export RATIFY_NAMESPACE=${4:-gatekeeper-system} CERT_DIR=${5:-"~/ratify/certs"} export NOTATION_PEM_NAME="notation" export NOTATION_CHAIN_PEM_NAME="notationchain" +export KEYVAULT_KEY_NAME="test-key" TAG="test${SUFFIX}" REGISTRY="${ACR_NAME}.azurecr.io" @@ -71,7 +72,7 @@ deploy_ratify() { --set azurekeyvault.tenantId=${TENANT_ID} \ --set oras.authProviders.azureWorkloadIdentityEnabled=true \ --set azureWorkloadIdentity.clientId=${IDENTITY_CLIENT_ID} \ - --set-file cosign.key=".staging/cosign/cosign.pub" \ + --set azurekeyvault.keys[0].name=${KEYVAULT_KEY_NAME} \ --set featureFlags.RATIFY_CERT_ROTATION=true \ --set logger.level=debug @@ -105,6 +106,14 @@ upload_cert_to_akv() { -p @./test/bats/tests/config/akvpolicy.json } +create_key_akv() { + az keyvault key create \ + --vault-name ${KEYVAULT_NAME} \ + -n ${KEYVAULT_KEY_NAME} \ + --kty RSA \ + --size 2048 +} + save_logs() { echo "Saving logs" local LOG_SUFFIX="${KUBERNETES_VERSION}-${GATEKEEPER_VERSION}" @@ -125,10 +134,11 @@ trap cleanup EXIT main() { ./scripts/create-azure-resources.sh - + create_key_akv + local ACR_USER_NAME="00000000-0000-0000-0000-000000000000" local ACR_PASSWORD=$(az acr login --name ${ACR_NAME} --expose-token --output tsv --query accessToken) - make e2e-azure-setup TEST_REGISTRY=$REGISTRY TEST_REGISTRY_USERNAME=${ACR_USER_NAME} TEST_REGISTRY_PASSWORD=${ACR_PASSWORD} + make e2e-azure-setup TEST_REGISTRY=$REGISTRY TEST_REGISTRY_USERNAME=${ACR_USER_NAME} TEST_REGISTRY_PASSWORD=${ACR_PASSWORD} KEYVAULT_KEY_NAME=${KEYVAULT_KEY_NAME} KEYVAULT_NAME=${KEYVAULT_NAME} build_push_to_acr upload_cert_to_akv diff --git a/scripts/create-azure-resources.sh b/scripts/create-azure-resources.sh index b72f044ae..217ec5bf1 100755 --- a/scripts/create-azure-resources.sh +++ b/scripts/create-azure-resources.sh @@ -96,7 +96,7 @@ create_akv() { echo "AKV '${KEYVAULT_NAME}' is created" # Grant permissions to access the certificate. - az keyvault set-policy --name ${KEYVAULT_NAME} --secret-permissions get --object-id ${USER_ASSIGNED_IDENTITY_OBJECT_ID} + az keyvault set-policy --name ${KEYVAULT_NAME} --secret-permissions get --key-permissions get --object-id ${USER_ASSIGNED_IDENTITY_OBJECT_ID} } main() { diff --git a/test/bats/azure-test.bats b/test/bats/azure-test.bats index b638b3716..94a2d98a4 100644 --- a/test/bats/azure-test.bats +++ b/test/bats/azure-test.bats @@ -220,8 +220,6 @@ SLEEP_TIME=1 assert_success sleep 5 - run kubectl apply -f ./config/samples/config_v1beta1_verifier_cosign.yaml - sleep 5 run kubectl apply -f ./config/samples/config_v1beta1_verifier_sbom.yaml sleep 5 run kubectl apply -f ./config/samples/config_v1beta1_verifier_complete_licensechecker.yaml diff --git a/test/bats/base-test.bats b/test/bats/base-test.bats index 53c79f5dd..d4a799c23 100644 --- a/test/bats/base-test.bats +++ b/test/bats/base-test.bats @@ -140,6 +140,69 @@ RATIFY_NAMESPACE=gatekeeper-system assert_failure } +@test "cosign test" { + teardown() { + echo "cleaning up" + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod cosign-demo-key --namespace default --force --ignore-not-found=true' + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod cosign-demo-unsigned --namespace default --force --ignore-not-found=true' + } + run kubectl apply -f ./library/default/template.yaml + assert_success + sleep 5 + run kubectl apply -f ./library/default/samples/constraint.yaml + assert_success + sleep 5 + + run kubectl run cosign-demo-key --namespace default --image=registry:5000/cosign:signed-key + assert_success + + run kubectl run cosign-demo-unsigned --namespace default --image=registry:5000/cosign:unsigned + assert_failure +} + +@test "cosign legacy test" { + teardown() { + echo "cleaning up" + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod cosign-demo-key --namespace default --force --ignore-not-found=true' + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod cosign-demo-unsigned --namespace default --force --ignore-not-found=true' + } + + # use imperative command to guarantee verifier config is updated + run kubectl replace -f ./config/samples/config_v1beta1_verifier_cosign_legacy.yaml + sleep 5 + + run kubectl apply -f ./library/default/template.yaml + assert_success + sleep 5 + run kubectl apply -f ./library/default/samples/constraint.yaml + assert_success + sleep 5 + + run kubectl run cosign-demo-key --namespace default --image=registry:5000/cosign:signed-key + assert_success + + run kubectl run cosign-demo-unsigned --namespace default --image=registry:5000/cosign:unsigned + assert_failure +} + +@test "cosign keyless test" { + teardown() { + echo "cleaning up" + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod cosign-demo-keyless --namespace default --force --ignore-not-found=true' + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl replace -f ./config/samples/config_v1beta1_verifier_cosign.yaml' + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl replace -f ./config/samples/config_v1beta1_store_oras_http.yaml' + } + + # use imperative command to guarantee useHttp is updated + run kubectl replace -f ./config/samples/config_v1beta1_verifier_cosign_keyless.yaml + sleep 5 + + run kubectl replace -f ./config/samples/config_v1beta1_store_oras.yaml + sleep 5 + + wait_for_process 20 10 'kubectl run cosign-demo-keyless --namespace default --image=wabbitnetworks.azurecr.io/test/cosign-image:signed-keyless' +} + @test "validate crd add, replace and delete" { teardown() { echo "cleaning up" diff --git a/test/bats/plugin-test.bats b/test/bats/plugin-test.bats index c2c744939..b395325c0 100644 --- a/test/bats/plugin-test.bats +++ b/test/bats/plugin-test.bats @@ -59,44 +59,6 @@ SLEEP_TIME=1 assert_success } -@test "cosign test" { - teardown() { - echo "cleaning up" - wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod cosign-demo-key --namespace default --force --ignore-not-found=true' - wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod cosign-demo-unsigned --namespace default --force --ignore-not-found=true' - } - run kubectl apply -f ./library/default/template.yaml - assert_success - sleep 5 - run kubectl apply -f ./library/default/samples/constraint.yaml - assert_success - sleep 5 - - run kubectl run cosign-demo-key --namespace default --image=registry:5000/cosign:signed-key - assert_success - - run kubectl run cosign-demo-unsigned --namespace default --image=registry:5000/cosign:unsigned - assert_failure -} - -@test "cosign keyless test" { - teardown() { - echo "cleaning up" - wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod cosign-demo-keyless --namespace default --force --ignore-not-found=true' - wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl replace -f ./config/samples/config_v1beta1_verifier_cosign.yaml' - wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl replace -f ./config/samples/config_v1beta1_store_oras_http.yaml' - } - - # use imperative command to guarantee useHttp is updated - run kubectl replace -f ./config/samples/config_v1beta1_verifier_cosign_keyless.yaml - sleep 5 - - run kubectl replace -f ./config/samples/config_v1beta1_store_oras.yaml - sleep 5 - - wait_for_process 20 10 'kubectl run cosign-demo-keyless --namespace default --image=wabbitnetworks.azurecr.io/test/cosign-image:signed-keyless' -} - @test "licensechecker test" { teardown() { echo "cleaning up" diff --git a/utils/utils.go b/utils/utils.go index df05b618a..be9b8c78c 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -29,3 +29,8 @@ func SanitizeString(input string) string { func SanitizeURL(input url.URL) string { return SanitizeString(input.String()) } + +func MakePtr[T any](value T) *T { + b := value + return &b +} From 31db6c74fa19bbc4d00bcc4c28a78e17c7f6aa6b Mon Sep 17 00:00:00 2001 From: akashsinghal Date: Sat, 20 Apr 2024 00:19:10 +0000 Subject: [PATCH 2/7] update extension config for verifier report --- pkg/verifier/cosign/cosign.go | 43 +++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/pkg/verifier/cosign/cosign.go b/pkg/verifier/cosign/cosign.go index 8e77da67c..c3fe2681d 100644 --- a/pkg/verifier/cosign/cosign.go +++ b/pkg/verifier/cosign/cosign.go @@ -68,12 +68,31 @@ type PluginConfig struct { TrustPolicies []TrustPolicyConfig `json:"trustPolicies,omitempty"` } -type Extension struct { +// LegacyExtension is the structure for the verifier result extensions +// used for backwards compatibility with the legacy cosign verifier +type LegacyExtension struct { SignatureExtension []cosignExtension `json:"signatures,omitempty"` } +// Extension is the structure for the verifier result extensions +// contains a list of signature verification results +// where each entry corresponds to a single signature verified +type Extension struct { + SignatureExtension []cosignExtensionList `json:"signatures,omitempty"` +} + +// cosignExtensionList is the structure verifications performed +// per signature found in the image manifest +type cosignExtensionList struct { + Signature string `json:"signature"` + Verifications []cosignExtension `json:"verifications"` +} + +// cosignExtension is the structure for the verification result +// of a single signature found in the image manifest for a +// single public key type cosignExtension struct { - SignatureDigest digest.Digest `json:"signatureDigest"` + SignatureDigest digest.Digest `json:"signatureDigest,omitempty"` IsSuccess bool `json:"isSuccess"` BundleVerified bool `json:"bundleVerified"` Err string `json:"error,omitempty"` @@ -204,10 +223,14 @@ func (v *cosignVerifier) verifyInternal(ctx context.Context, subjectReference co Hex: subjectDesc.Digest.Hex(), } - sigExtensions := make([]cosignExtension, 0) + sigExtensions := make([]cosignExtensionList, 0) hasValidSignature := false // check each signature found for _, blob := range referenceManifest.Blobs { + extensionListEntry := cosignExtensionList{ + Signature: blob.Annotations[static.SignatureAnnotationKey], + Verifications: make([]cosignExtension, 0), + } // fetch the blob content of the signature from the referrer store blobBytes, err := referrerStore.GetBlobContent(ctx, subjectReference, blob.Digest) if err != nil { @@ -284,10 +307,9 @@ func (v *cosignVerifier) verifyInternal(ctx context.Context, subjectReference co // verify signature with cosign options + perform bundle verification bundleVerified, err := cosign.VerifyImageSignature(ctx, sig, subjectDescHash, &cosignOpts) extension := cosignExtension{ - SignatureDigest: blob.Digest, - IsSuccess: true, - BundleVerified: bundleVerified, - KeyInformation: mapKey, + IsSuccess: true, + BundleVerified: bundleVerified, + KeyInformation: mapKey, } if err != nil { extension.IsSuccess = false @@ -295,10 +317,11 @@ func (v *cosignVerifier) verifyInternal(ctx context.Context, subjectReference co } else { hasValidSignature = true } - sigExtensions = append(sigExtensions, extension) + extensionListEntry.Verifications = append(extensionListEntry.Verifications, extension) } // TODO: perform keyless verification instead if no keys are found + sigExtensions = append(sigExtensions, extensionListEntry) } if hasValidSignature { @@ -418,12 +441,12 @@ func (v *cosignVerifier) verifyLegacy(ctx context.Context, subjectReference comm Type: v.verifierType, IsSuccess: true, Message: "cosign verification success. valid signatures found", - Extensions: Extension{SignatureExtension: sigExtensions}, + Extensions: LegacyExtension{SignatureExtension: sigExtensions}, }, nil } errorResult := errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("no valid signatures found")) - errorResult.Extensions = Extension{SignatureExtension: sigExtensions} + errorResult.Extensions = LegacyExtension{SignatureExtension: sigExtensions} return errorResult, nil } From ac7a0fc5f9f64b23685589d61cf2ff45f59ecb68 Mon Sep 17 00:00:00 2001 From: akashsinghal Date: Mon, 22 Apr 2024 19:30:49 +0000 Subject: [PATCH 3/7] address comments --- charts/ratify/README.md | 2 +- charts/ratify/templates/verifier.yaml | 4 ++-- pkg/verifier/cosign/trustpolicies.go | 17 ++++------------- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/charts/ratify/README.md b/charts/ratify/README.md index 60fa92242..4ec94eea8 100644 --- a/charts/ratify/README.md +++ b/charts/ratify/README.md @@ -48,7 +48,7 @@ Values marked `# DEPRECATED` in the `values.yaml` as well as **DEPRECATED** in t | affinity | Pod affinity for the Ratify deployment | `{}` | | tolerations | Pod tolerations for the Ratify deployment | `[]` | | notationCerts | An array of public certificate/certificate chain used to create inline certstore used by Notation verifier | `` | -| cosignKeys | An aray of public keys used to create inline key management providers used by Cosign verifier | `[]` | +| cosignKeys | An array of public keys used to create inline key management providers used by Cosign verifier | `[]` | | cosign.enabled | Enables/disables cosign tag-based signature lookup in ORAS store. MUST be set to true for cosign verification. | `true` | | cosign.scopes | An array of scopes relevant to the single trust policy configured in Cosign verifier. A scope of '*' is a global wildcard character to represent all images apply. | `["*"]` | | vulnerabilityreport.enabled | Enables/disables installation of vulnerability report verifier | `false` | diff --git a/charts/ratify/templates/verifier.yaml b/charts/ratify/templates/verifier.yaml index a1fd42319..c2684b555 100644 --- a/charts/ratify/templates/verifier.yaml +++ b/charts/ratify/templates/verifier.yaml @@ -61,10 +61,10 @@ spec: {{- end }} keys: {{- range $i, $key := .Values.cosignKeys }} - - provider: {{$fullname}}-cosign-inline-key-{{$i}} + - provider: {{ .Release.Namespace }}/{{$fullname}}-cosign-inline-key-{{$i}} {{- end }} {{- if and .Values.azurekeyvault.enabled (gt (len .Values.azurekeyvault.keys) 0) }} - - provider: kmprovider-akv + - provider: {{ .Release.Namespace }}/kmprovider-akv {{- end }} {{- end }} {{- end }} diff --git a/pkg/verifier/cosign/trustpolicies.go b/pkg/verifier/cosign/trustpolicies.go index a83bc585b..da0bcb087 100644 --- a/pkg/verifier/cosign/trustpolicies.go +++ b/pkg/verifier/cosign/trustpolicies.go @@ -34,12 +34,8 @@ var validScopeRegex = regexp.MustCompile(`^[a-z0-9\.\-:@\/]*\*?$`) // CreateTrustPolicies creates a set of trust policies from the given configuration func CreateTrustPolicies(configs []TrustPolicyConfig, verifierName string) (*TrustPolicies, error) { - if configs == nil { - return nil, re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail("failed to create trust policies: no policies found") - } - if len(configs) == 0 { - return nil, re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail("failed to create trust policies: no policies defined") + return nil, re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail("failed to create trust policies: no policies found") } policies := make([]TrustPolicy, 0, len(configs)) @@ -56,12 +52,7 @@ func CreateTrustPolicies(configs []TrustPolicyConfig, verifierName string) (*Tru policies = append(policies, policy) } - if len(policies) == 0 { - return nil, re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail("failed to create trust policies: no trust policies defined") - } - - err := validateScopes(policies) - if err != nil { + if err := validateScopes(policies); err != nil { return nil, err } @@ -99,7 +90,7 @@ func (tps *TrustPolicies) GetScopedPolicy(reference string) (TrustPolicy, error) // validateScopes validates the scopes in the trust policies func validateScopes(policies []TrustPolicy) error { - scopesMap := make(map[string]TrustPolicy) + scopesMap := make(map[string]struct{}) hasGlobalWildcard := false for _, policy := range policies { policyName := policy.GetName() @@ -155,7 +146,7 @@ func validateScopes(policies []TrustPolicy) error { return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithDetail(fmt.Sprintf("failed to create trust policies: overlapping scopes %s and %s for trust policy %s", scope, existingScope, policyName)) } } - scopesMap[scope] = policy + scopesMap[scope] = struct{}{} } } return nil From 812e0a60e7c1b85b71fe3a34238fc5f6a7775fff Mon Sep 17 00:00:00 2001 From: akashsinghal Date: Mon, 22 Apr 2024 19:48:21 +0000 Subject: [PATCH 4/7] fix --- charts/ratify/templates/verifier.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/ratify/templates/verifier.yaml b/charts/ratify/templates/verifier.yaml index c2684b555..7cc5c851a 100644 --- a/charts/ratify/templates/verifier.yaml +++ b/charts/ratify/templates/verifier.yaml @@ -61,10 +61,10 @@ spec: {{- end }} keys: {{- range $i, $key := .Values.cosignKeys }} - - provider: {{ .Release.Namespace }}/{{$fullname}}-cosign-inline-key-{{$i}} + - provider: {{ $.Release.Namespace }}/{{$fullname}}-cosign-inline-key-{{$i}} {{- end }} {{- if and .Values.azurekeyvault.enabled (gt (len .Values.azurekeyvault.keys) 0) }} - - provider: {{ .Release.Namespace }}/kmprovider-akv + - provider: {{ $.Release.Namespace }}/kmprovider-akv {{- end }} {{- end }} {{- end }} From f8c365a9ad1d23b139e698dbe28a65822bdac9da Mon Sep 17 00:00:00 2001 From: akashsinghal Date: Mon, 22 Apr 2024 20:26:07 +0000 Subject: [PATCH 5/7] add version --- charts/ratify/templates/verifier.yaml | 1 + pkg/verifier/cosign/trustpolicy.go | 24 ++++++++++++++++++++---- pkg/verifier/cosign/trustpolicy_test.go | 17 +++++++++++++++-- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/charts/ratify/templates/verifier.yaml b/charts/ratify/templates/verifier.yaml index 7cc5c851a..ed562acec 100644 --- a/charts/ratify/templates/verifier.yaml +++ b/charts/ratify/templates/verifier.yaml @@ -55,6 +55,7 @@ spec: {{- else }} trustPolicies: - name: default + version: 1.0.0 scopes: {{- range $i, $scope := .Values.cosign.scopes }} - "{{$scope}}" diff --git a/pkg/verifier/cosign/trustpolicy.go b/pkg/verifier/cosign/trustpolicy.go index b990d20a1..9975af171 100644 --- a/pkg/verifier/cosign/trustpolicy.go +++ b/pkg/verifier/cosign/trustpolicy.go @@ -20,6 +20,7 @@ import ( "crypto" "fmt" "os" + "slices" re "github.com/deislabs/ratify/errors" "github.com/deislabs/ratify/internal/constants" @@ -45,6 +46,7 @@ type KeylessConfig struct { } type TrustPolicyConfig struct { + Version string `json:"version"` Name string `json:"name"` Scopes []string `json:"scopes"` Keys []KeyConfig `json:"keys,omitempty"` @@ -74,16 +76,25 @@ type TrustPolicy interface { } const ( - fileProviderName string = "file" - DefaultRekorURL string = "https://rekor.sigstore.dev" - DefaultTLogVerify bool = true - DefaultCTLogVerify bool = true + fileProviderName string = "file" + DefaultRekorURL string = "https://rekor.sigstore.dev" + DefaultTLogVerify bool = true + DefaultCTLogVerify bool = true + DefaultTrustPolicyConfigVersion string = "1.0.0" ) +var SupportedTrustPolicyConfigVersions = []string{DefaultTrustPolicyConfigVersion} + // CreateTrustPolicy creates a trust policy from the given configuration // returns an error if the configuration is invalid // reads the public keys from the file path func CreateTrustPolicy(config TrustPolicyConfig, verifierName string) (TrustPolicy, error) { + // set the default trust policy version if not provided + // currently only one version is supported + if config.Version == "" { + config.Version = DefaultTrustPolicyConfigVersion + } + if err := validate(config, verifierName); err != nil { return nil, err } @@ -207,6 +218,11 @@ func (tp *trustPolicy) GetCosignOpts(ctx context.Context) (cosign.CheckOpts, err // validate checks if the trust policy configuration is valid // returns an error if the configuration is invalid func validate(config TrustPolicyConfig, verifierName string) error { + // check if the trust policy version is supported + if !slices.Contains(SupportedTrustPolicyConfigVersions, config.Version) { + return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail(fmt.Sprintf("trust policy %s failed: unsupported version %s", config.Name, config.Version)) + } + if config.Name == "" { return re.ErrorCodeConfigInvalid.WithComponentType(re.Verifier).WithPluginName(verifierName).WithDetail("missing trust policy name") } diff --git a/pkg/verifier/cosign/trustpolicy_test.go b/pkg/verifier/cosign/trustpolicy_test.go index 5d658dd15..84155dae6 100644 --- a/pkg/verifier/cosign/trustpolicy_test.go +++ b/pkg/verifier/cosign/trustpolicy_test.go @@ -71,6 +71,18 @@ func TestCreateTrustPolicy(t *testing.T) { }, wantErr: false, }, + { + name: "invalid config version", + cfg: TrustPolicyConfig{ + Version: "0.0.0", + Name: "test", + Scopes: []string{"*"}, + Keyless: KeylessConfig{ + RekorURL: DefaultRekorURL, + }, + }, + wantErr: true, + }, } for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { @@ -281,8 +293,9 @@ func TestValidate(t *testing.T) { { name: "valid", policyConfig: TrustPolicyConfig{ - Name: "test", - Scopes: []string{"*"}, + Version: "1.0.0", + Name: "test", + Scopes: []string{"*"}, Keys: []KeyConfig{ { Provider: "kmp", From fe941942815c4397bee7215316f5a0e375445ee0 Mon Sep 17 00:00:00 2001 From: akashsinghal Date: Mon, 22 Apr 2024 22:09:47 +0000 Subject: [PATCH 6/7] create akv helper --- pkg/verifier/cosign/cosign.go | 98 ++++++++++++++++-------------- pkg/verifier/cosign/cosign_test.go | 7 ++- 2 files changed, 58 insertions(+), 47 deletions(-) diff --git a/pkg/verifier/cosign/cosign.go b/pkg/verifier/cosign/cosign.go index c3fe2681d..819dd0509 100644 --- a/pkg/verifier/cosign/cosign.go +++ b/pkg/verifier/cosign/cosign.go @@ -251,50 +251,9 @@ func (v *cosignVerifier) verifyInternal(ctx context.Context, subjectReference co // default hash type is SHA256 but for AKV scenarios, the hash type is determined by the key size // TODO: investigate if it's possible to extract hash type from sig directly. This is a workaround for now if pubKey.ProviderType == azurekeyvault.ProviderName { - switch keyType := pubKey.Key.(type) { - case *rsa.PublicKey: - switch keyType.Size() { - case 256: - hashType = crypto.SHA256 - case 384: - hashType = crypto.SHA384 - case 512: - hashType = crypto.SHA512 - default: - return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("RSA key check: unsupported key size: %d", keyType.Size())), nil - } - - // TODO: remove section after fix for bug in cosign azure key vault implementation - // tracking issue: https://github.com/sigstore/sigstore/issues/1384 - // summary: azure keyvault implementation ASN.1 encodes sig after online signing with keyvault - // EC verifiers in cosign have built in ASN.1 decoding, but RSA verifiers do not - base64DecodedBytes, err := base64.StdEncoding.DecodeString(blob.Annotations[static.SignatureAnnotationKey]) - if err != nil { - return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("RSA key check: failed to decode base64 signature: %w", err)), nil - } - // decode ASN.1 signature to raw signature if it is ASN.1 encoded - decodedSigBytes, err := decodeASN1Signature(base64DecodedBytes) - if err != nil { - return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("RSA key check: failed to decode ASN.1 signature: %w", err)), nil - } - encodedBase64SigBytes := base64.StdEncoding.EncodeToString(decodedSigBytes) - sig, err = static.NewSignature(blobBytes, encodedBase64SigBytes, staticOpts...) - if err != nil { - return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("RSA key check: failed to generate static signature: %w", err)), nil - } - case *ecdsa.PublicKey: - switch keyType.Curve { - case elliptic.P256(): - hashType = crypto.SHA256 - case elliptic.P384(): - hashType = crypto.SHA384 - case elliptic.P521(): - hashType = crypto.SHA512 - default: - return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("ECDSA key check: unsupported key curve: %s", keyType.Params().Name)), nil - } - default: - return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("unsupported public key type: %T", pubKey)), nil + hashType, sig, err = processAKVSignature(blob.Annotations[static.SignatureAnnotationKey], sig, pubKey.Key, blobBytes, staticOpts) + if err != nil { + return errorToVerifyResult(v.name, v.verifierType, fmt.Errorf("failed to process AKV signature: %w", err)), nil } } @@ -583,3 +542,54 @@ func getKeysMapsDefault(ctx context.Context, trustPolicies *TrustPolicies, refer return keysMap, cosignOpts, nil } + +// processAKVSignature processes the AKV signature and returns the hash type, signature and error +func processAKVSignature(sigEncoded string, staticSig oci.Signature, publicKey crypto.PublicKey, payloadBytes []byte, staticOpts []static.Option) (crypto.Hash, oci.Signature, error) { + var hashType crypto.Hash + switch keyType := publicKey.(type) { + case *rsa.PublicKey: + switch keyType.Size() { + case 256: + hashType = crypto.SHA256 + case 384: + hashType = crypto.SHA384 + case 512: + hashType = crypto.SHA512 + default: + return crypto.SHA256, nil, fmt.Errorf("RSA key check: unsupported key size: %d", keyType.Size()) + } + + // TODO: remove section after fix for bug in cosign azure key vault implementation + // tracking issue: https://github.com/sigstore/sigstore/issues/1384 + // summary: azure keyvault implementation ASN.1 encodes sig after online signing with keyvault + // EC verifiers in cosign have built in ASN.1 decoding, but RSA verifiers do not + base64DecodedBytes, err := base64.StdEncoding.DecodeString(sigEncoded) + if err != nil { + return crypto.SHA256, nil, fmt.Errorf("RSA key check: failed to decode base64 signature: %w", err) + } + // decode ASN.1 signature to raw signature if it is ASN.1 encoded + decodedSigBytes, err := decodeASN1Signature(base64DecodedBytes) + if err != nil { + return crypto.SHA256, nil, fmt.Errorf("RSA key check: failed to decode ASN.1 signature: %w", err) + } + encodedBase64SigBytes := base64.StdEncoding.EncodeToString(decodedSigBytes) + staticSig, err = static.NewSignature(payloadBytes, encodedBase64SigBytes, staticOpts...) + if err != nil { + return crypto.SHA256, nil, fmt.Errorf("RSA key check: failed to generate static signature: %w", err) + } + case *ecdsa.PublicKey: + switch keyType.Curve { + case elliptic.P256(): + hashType = crypto.SHA256 + case elliptic.P384(): + hashType = crypto.SHA384 + case elliptic.P521(): + hashType = crypto.SHA512 + default: + return crypto.SHA256, nil, fmt.Errorf("ECDSA key check: unsupported key curve: %s", keyType.Params().Name) + } + default: + return crypto.SHA256, nil, fmt.Errorf("unsupported public key type: %T", publicKey) + } + return hashType, staticSig, nil +} diff --git a/pkg/verifier/cosign/cosign_test.go b/pkg/verifier/cosign/cosign_test.go index 13a0261be..6770936d5 100644 --- a/pkg/verifier/cosign/cosign_test.go +++ b/pkg/verifier/cosign/cosign_test.go @@ -503,6 +503,7 @@ func TestGetKeysMaps_FailingGetKeys(t *testing.T) { } // TestVerifyInternal tests the verifyInternal function of the cosign verifier +// it also tests the processAKVSignature function implicitly func TestVerifyInternal(t *testing.T) { cosignMediaType := "application/vnd.dev.cosign.simplesigning.v1+json" validSignatureBlob := []byte("test") @@ -676,7 +677,7 @@ mmBwUAwwW0Uc+Nt3bDOCiB1nUsICv1ry blobDigest: validSignatureBlob, }, }, - expectedResultMessagePrefix: "cosign verification failed: unsupported public key type", + expectedResultMessagePrefix: "cosign verification failed: failed to process AKV signature: unsupported public key type", }, { name: "invalid RSA key size for AKV", @@ -707,7 +708,7 @@ mmBwUAwwW0Uc+Nt3bDOCiB1nUsICv1ry blobDigest: validSignatureBlob, }, }, - expectedResultMessagePrefix: "cosign verification failed: RSA key check: unsupported key size", + expectedResultMessagePrefix: "cosign verification failed: failed to process AKV signature: RSA key check: unsupported key size", }, { name: "invalid ECDSA curve type for AKV", @@ -738,7 +739,7 @@ mmBwUAwwW0Uc+Nt3bDOCiB1nUsICv1ry blobDigest: validSignatureBlob, }, }, - expectedResultMessagePrefix: "cosign verification failed: ECDSA key check: unsupported key curve", + expectedResultMessagePrefix: "cosign verification failed: failed to process AKV signature: ECDSA key check: unsupported key curve", }, { name: "valid hash: 256 keysize: 2048 RSA key AKV", From dcef905788331923e569558fb7ac0acbc4f78375 Mon Sep 17 00:00:00 2001 From: akashsinghal Date: Tue, 23 Apr 2024 17:07:51 +0000 Subject: [PATCH 7/7] comments --- pkg/verifier/cosign/trustpolicies.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/verifier/cosign/trustpolicies.go b/pkg/verifier/cosign/trustpolicies.go index da0bcb087..b4082d21d 100644 --- a/pkg/verifier/cosign/trustpolicies.go +++ b/pkg/verifier/cosign/trustpolicies.go @@ -62,6 +62,7 @@ func CreateTrustPolicies(configs []TrustPolicyConfig, verifierName string) (*Tru } // GetScopedPolicy returns the policy that applies to the given reference +// TODO: add link to scopes docs when published func (tps *TrustPolicies) GetScopedPolicy(reference string) (TrustPolicy, error) { var globalPolicy TrustPolicy for _, policy := range tps.policies {