From 0e1c5001a627d5fd26fd80d3fd63c738f326df8f Mon Sep 17 00:00:00 2001 From: akashsinghal Date: Tue, 12 Mar 2024 18:13:39 +0000 Subject: [PATCH 01/10] feat: add key management provider resource --- PROJECT | 13 + .../keymanagementprovider_types.go | 73 ++++ api/unversioned/zz_generated.deepcopy.go | 79 ++++ api/v1beta1/keymanagementproviders_types.go | 86 +++++ api/v1beta1/zz_generated.deepcopy.go | 95 +++++ charts/ratify/README.md | 2 +- ...mentprovider-customresourcedefinition.yaml | 87 +++++ .../templates/akv-certificate-provider.yaml | 34 -- .../akv-key-management-provider.yaml | 30 ++ ...ml => inline-key-management-provider.yaml} | 10 +- .../ratify-manager-role-clusterrole.yaml | 26 ++ charts/ratify/templates/verifier.yaml | 2 +- ...fy.deislabs.io_keymanagementproviders.yaml | 87 +++++ config/crd/kustomization.yaml | 3 + ...cainjection_in_keymanagementproviders.yaml | 7 + .../webhook_in_keymanagementproviders.yaml | 16 + .../keymanagementprovider_editor_role.yaml | 31 ++ .../keymanagementprovider_viewer_role.yaml | 27 ++ config/rbac/role.yaml | 26 ++ ...fig_v1beta1_keymanagementprovider_akv.yaml | 13 + ..._v1beta1_keymanagementprovider_inline.yaml | 29 ++ ...v1beta1_verifier_notation_kmprovider.yaml} | 8 +- ...rifier_notation_specificnskmprovider.yaml} | 0 errors/types.go | 17 +- .../certificatestore_controller.go | 21 +- pkg/controllers/constants.go | 16 + .../keymanagementprovider_controller.go | 194 ++++++++++ .../keymanagementprovider_controller_test.go | 267 ++++++++++++++ .../azurekeyvault/auth.go | 116 ++++++ .../azurekeyvault/auth_test.go | 67 ++++ .../azurekeyvault/provider.go | 323 +++++++++++++++++ .../azurekeyvault/provider_test.go | 337 ++++++++++++++++++ .../azurekeyvault/types/types.go | 36 ++ pkg/keymanagementprovider/config/config.go | 25 ++ pkg/keymanagementprovider/factory/factory.go | 65 ++++ .../factory/factory_test.go | 120 +++++++ pkg/keymanagementprovider/inline/provider.go | 95 +++++ .../inline/provider_test.go | 118 ++++++ .../keymanagementprovider.go | 101 ++++++ .../keymanagementprovider_test.go | 174 +++++++++ pkg/keymanagementprovider/mocks/types.go | 33 ++ pkg/keymanagementprovider/types/types.go | 23 ++ pkg/manager/manager.go | 7 + pkg/verifier/notation/truststore.go | 10 +- scripts/azure-ci-test.sh | 4 +- test/bats/azure-test.bats | 16 +- test/bats/base-test.bats | 99 +++-- test/bats/high-availability.bats | 4 +- ..._v1beta1_keymanagementprovider_inline.yaml | 9 + .../config_v1beta1_verifier_notation_akv.yaml | 2 +- ..._v1beta1_verifier_notation_kmprovider.yaml | 23 ++ 51 files changed, 3009 insertions(+), 97 deletions(-) create mode 100644 api/unversioned/keymanagementprovider_types.go create mode 100644 api/v1beta1/keymanagementproviders_types.go create mode 100644 charts/ratify/crds/keymanagementprovider-customresourcedefinition.yaml delete mode 100644 charts/ratify/templates/akv-certificate-provider.yaml create mode 100644 charts/ratify/templates/akv-key-management-provider.yaml rename charts/ratify/templates/{inline-certificate-provider.yaml => inline-key-management-provider.yaml} (81%) create mode 100644 config/crd/bases/config.ratify.deislabs.io_keymanagementproviders.yaml create mode 100644 config/crd/patches/cainjection_in_keymanagementproviders.yaml create mode 100644 config/crd/patches/webhook_in_keymanagementproviders.yaml create mode 100644 config/rbac/keymanagementprovider_editor_role.yaml create mode 100644 config/rbac/keymanagementprovider_viewer_role.yaml create mode 100644 config/samples/config_v1beta1_keymanagementprovider_akv.yaml create mode 100644 config/samples/config_v1beta1_keymanagementprovider_inline.yaml rename config/samples/{config_v1beta1_verifier_notation_certstore.yaml => config_v1beta1_verifier_notation_kmprovider.yaml} (83%) rename config/samples/{config_v1beta1_verifier_notation_specificnscertstore.yaml => config_v1beta1_verifier_notation_specificnskmprovider.yaml} (100%) create mode 100644 pkg/controllers/constants.go create mode 100644 pkg/controllers/keymanagementprovider_controller.go create mode 100644 pkg/controllers/keymanagementprovider_controller_test.go create mode 100644 pkg/keymanagementprovider/azurekeyvault/auth.go create mode 100644 pkg/keymanagementprovider/azurekeyvault/auth_test.go create mode 100644 pkg/keymanagementprovider/azurekeyvault/provider.go create mode 100644 pkg/keymanagementprovider/azurekeyvault/provider_test.go create mode 100644 pkg/keymanagementprovider/azurekeyvault/types/types.go create mode 100644 pkg/keymanagementprovider/config/config.go create mode 100644 pkg/keymanagementprovider/factory/factory.go create mode 100644 pkg/keymanagementprovider/factory/factory_test.go create mode 100644 pkg/keymanagementprovider/inline/provider.go create mode 100644 pkg/keymanagementprovider/inline/provider_test.go create mode 100644 pkg/keymanagementprovider/keymanagementprovider.go create mode 100644 pkg/keymanagementprovider/keymanagementprovider_test.go create mode 100644 pkg/keymanagementprovider/mocks/types.go create mode 100644 pkg/keymanagementprovider/types/types.go create mode 100644 test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml create mode 100644 test/bats/tests/config/config_v1beta1_verifier_notation_kmprovider.yaml diff --git a/PROJECT b/PROJECT index f1fa9b5ca..75078fb96 100644 --- a/PROJECT +++ b/PROJECT @@ -1,3 +1,7 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html domain: ratify.deislabs.io layout: - go.kubebuilder.io/v3 @@ -67,4 +71,13 @@ resources: kind: CertificateStore path: github.com/deislabs/ratify/api/v1beta1 version: v1beta1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: ratify.deislabs.io + group: config + kind: KeyManagementProvider + path: github.com/deislabs/ratify/api/v1beta1 + version: v1beta1 version: "3" diff --git a/api/unversioned/keymanagementprovider_types.go b/api/unversioned/keymanagementprovider_types.go new file mode 100644 index 000000000..9b3a77db9 --- /dev/null +++ b/api/unversioned/keymanagementprovider_types.go @@ -0,0 +1,73 @@ +/* +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. +*/ + +// +kubebuilder:skip +package unversioned + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// KeyManagementProviderSpec defines the desired state of KeyManagementProvider +type KeyManagementProviderSpec struct { + // Important: Run "make" to regenerate code after modifying this file + + // Name of the key management provider + Type string `json:"type,omitempty"` + + // Parameters of the key management provider + Parameters runtime.RawExtension `json:"parameters,omitempty"` +} + +// KeyManagementProviderStatus defines the observed state of KeyManagementProvider +type KeyManagementProviderStatus struct { + // Important: Run "make manifests" to regenerate code after modifying this file + + // Is successful in loading certificate/key files + IsSuccess bool `json:"issuccess"` + // Error message if operation was unsuccessful + // +optional + Error string `json:"error,omitempty"` + // Truncated error message if the message is too long + // +optional + BriefError string `json:"brieferror,omitempty"` + // The time stamp of last successful certificate/key fetch operation. If operation failed, last fetched time shows the time of error + // +optional + LastFetchedTime *metav1.Time `json:"lastfetchedtime,omitempty"` + // provider specific properties of the each individual certificate/key + // +optional + Properties runtime.RawExtension `json:"properties,omitempty"` +} + +// KeyManagementProvider is the Schema for the keymanagementproviders API +type KeyManagementProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec KeyManagementProviderSpec `json:"spec,omitempty"` + Status KeyManagementProviderStatus `json:"status,omitempty"` +} + +// KeyManagementProviderList contains a list of KeyManagementProvider +type KeyManagementProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []KeyManagementProvider `json:"items"` +} diff --git a/api/unversioned/zz_generated.deepcopy.go b/api/unversioned/zz_generated.deepcopy.go index 6ac10b4f5..433353d89 100644 --- a/api/unversioned/zz_generated.deepcopy.go +++ b/api/unversioned/zz_generated.deepcopy.go @@ -102,6 +102,85 @@ func (in *CertificateStoreStatus) DeepCopy() *CertificateStoreStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeyManagementProvider) DeepCopyInto(out *KeyManagementProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyManagementProvider. +func (in *KeyManagementProvider) DeepCopy() *KeyManagementProvider { + if in == nil { + return nil + } + out := new(KeyManagementProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeyManagementProviderList) DeepCopyInto(out *KeyManagementProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]KeyManagementProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyManagementProviderList. +func (in *KeyManagementProviderList) DeepCopy() *KeyManagementProviderList { + if in == nil { + return nil + } + out := new(KeyManagementProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeyManagementProviderSpec) DeepCopyInto(out *KeyManagementProviderSpec) { + *out = *in + in.Parameters.DeepCopyInto(&out.Parameters) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyManagementProviderSpec. +func (in *KeyManagementProviderSpec) DeepCopy() *KeyManagementProviderSpec { + if in == nil { + return nil + } + out := new(KeyManagementProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeyManagementProviderStatus) DeepCopyInto(out *KeyManagementProviderStatus) { + *out = *in + if in.LastFetchedTime != nil { + in, out := &in.LastFetchedTime, &out.LastFetchedTime + *out = (*in).DeepCopy() + } + in.Properties.DeepCopyInto(&out.Properties) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyManagementProviderStatus. +func (in *KeyManagementProviderStatus) DeepCopy() *KeyManagementProviderStatus { + if in == nil { + return nil + } + out := new(KeyManagementProviderStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PluginSource) DeepCopyInto(out *PluginSource) { *out = *in diff --git a/api/v1beta1/keymanagementproviders_types.go b/api/v1beta1/keymanagementproviders_types.go new file mode 100644 index 000000000..d8f10d53c --- /dev/null +++ b/api/v1beta1/keymanagementproviders_types.go @@ -0,0 +1,86 @@ +/* +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 v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// KeyManagementProviderSpec defines the desired state of KeyManagementProvider +type KeyManagementProviderSpec struct { + // Important: Run "make" to regenerate code after modifying this file + + // Name of the key management provider + Type string `json:"type,omitempty"` + + // +kubebuilder:pruning:PreserveUnknownFields + // Parameters of the key management provider + Parameters runtime.RawExtension `json:"parameters,omitempty"` +} + +// KeyManagementProviderStatus defines the observed state of KeyManagementProvider +type KeyManagementProviderStatus struct { + // Important: Run "make manifests" to regenerate code after modifying this file + + // Is successful in loading certificate/key files + IsSuccess bool `json:"issuccess"` + // Error message if operation was unsuccessful + // +optional + Error string `json:"error,omitempty"` + // Truncated error message if the message is too long + // +optional + BriefError string `json:"brieferror,omitempty"` + // The time stamp of last successful certificate/key fetch operation. If operation failed, last fetched time shows the time of error + // +optional + LastFetchedTime *metav1.Time `json:"lastfetchedtime,omitempty"` + // +kubebuilder:pruning:PreserveUnknownFields + // provider specific properties of the each individual certificate/key + // +optional + Properties runtime.RawExtension `json:"properties,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:storageversion +// +kubebuilder:printcolumn:name="IsSuccess",type=boolean,JSONPath=`.status.issuccess` +// +kubebuilder:printcolumn:name="Error",type=string,JSONPath=`.status.brieferror` +// +kubebuilder:printcolumn:name="LastFetchedTime",type=date,JSONPath=`.status.lastfetchedtime` +// KeyManagementProvider is the Schema for the keymanagementproviders API +type KeyManagementProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec KeyManagementProviderSpec `json:"spec,omitempty"` + Status KeyManagementProviderStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:storageversion +// KeyManagementProviderList contains a list of KeyManagementProvider +type KeyManagementProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []KeyManagementProvider `json:"items"` +} + +func init() { + SchemeBuilder.Register(&KeyManagementProvider{}, &KeyManagementProviderList{}) +} diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index cb22292ee..8e390e3c8 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -120,6 +120,101 @@ func (in *CertificateStoreStatus) DeepCopy() *CertificateStoreStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeyManagementProvider) DeepCopyInto(out *KeyManagementProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyManagementProvider. +func (in *KeyManagementProvider) DeepCopy() *KeyManagementProvider { + if in == nil { + return nil + } + out := new(KeyManagementProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *KeyManagementProvider) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeyManagementProviderList) DeepCopyInto(out *KeyManagementProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]KeyManagementProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyManagementProviderList. +func (in *KeyManagementProviderList) DeepCopy() *KeyManagementProviderList { + if in == nil { + return nil + } + out := new(KeyManagementProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *KeyManagementProviderList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeyManagementProviderSpec) DeepCopyInto(out *KeyManagementProviderSpec) { + *out = *in + in.Parameters.DeepCopyInto(&out.Parameters) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyManagementProviderSpec. +func (in *KeyManagementProviderSpec) DeepCopy() *KeyManagementProviderSpec { + if in == nil { + return nil + } + out := new(KeyManagementProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeyManagementProviderStatus) DeepCopyInto(out *KeyManagementProviderStatus) { + *out = *in + if in.LastFetchedTime != nil { + in, out := &in.LastFetchedTime, &out.LastFetchedTime + *out = (*in).DeepCopy() + } + in.Properties.DeepCopyInto(&out.Properties) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyManagementProviderStatus. +func (in *KeyManagementProviderStatus) DeepCopy() *KeyManagementProviderStatus { + if in == nil { + return nil + } + out := new(KeyManagementProviderStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PluginSource) DeepCopyInto(out *PluginSource) { *out = *in diff --git a/charts/ratify/README.md b/charts/ratify/README.md index edc8a5106..f253d1124 100644 --- a/charts/ratify/README.md +++ b/charts/ratify/README.md @@ -126,5 +126,5 @@ $ helm upgrade -n gatekeeper-system [RELEASE_NAME] ratify/ratify | akvCertConfig.cert1Version | Exact version of certificate to use from AKV. This value has been ***deprecated*** , and will be removed in future releases of Ratify. Please switch to ```akvCertConfig.certificates``` to specify an array of verification certificates | `` | | akvCertConfig.cert2Name | Exact name of the certificate stored in AKV. This value has been ***deprecated*** , and will be removed in future releases of Ratify. Please switch to ```akvCertConfig.certificates``` to specify an array of verification certificates | `` | | akvCertConfig.cert2Version | Exact version of certificate to use from AKV. This value has been ***deprecated*** , and will be removed in future releases of Ratify. Please switch to ```akvCertConfig.certificates``` to specify an array of verification certificates | `` | -| akvCertConfig.certificates | An array of certificate objects identified by certificateName and certificateVersion stored in AKV | `` | +| akvCertConfig.certificates | An array of certificate objects identified by `name` and `version` stored in AKV | `` | | akvCertConfig.tenantId | TenantID of the configured AKV resource | `` | diff --git a/charts/ratify/crds/keymanagementprovider-customresourcedefinition.yaml b/charts/ratify/crds/keymanagementprovider-customresourcedefinition.yaml new file mode 100644 index 000000000..29ddb906d --- /dev/null +++ b/charts/ratify/crds/keymanagementprovider-customresourcedefinition.yaml @@ -0,0 +1,87 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: keymanagementproviders.config.ratify.deislabs.io +spec: + group: config.ratify.deislabs.io + names: + kind: KeyManagementProvider + listKind: KeyManagementProviderList + plural: keymanagementproviders + singular: keymanagementprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.issuccess + name: IsSuccess + type: boolean + - jsonPath: .status.brieferror + name: Error + type: string + - jsonPath: .status.lastfetchedtime + name: LastFetchedTime + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: KeyManagementProvider is the Schema for the keymanagementproviders + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: KeyManagementProviderSpec defines the desired state of KeyManagementProvider + properties: + parameters: + description: Parameters of the key management provider + type: object + x-kubernetes-preserve-unknown-fields: true + type: + description: Name of the key management provider + type: string + type: object + status: + description: KeyManagementProviderStatus defines the observed state of + KeyManagementProvider + properties: + brieferror: + description: Truncated error message if the message is too long + type: string + error: + description: Error message if operation was unsuccessful + type: string + issuccess: + description: Is successful in loading certificate/key files + type: boolean + lastfetchedtime: + description: The time stamp of last successful certificate/key fetch + operation. If operation failed, last fetched time shows the time + of error + format: date-time + type: string + properties: + description: provider specific properties of the each individual certificate/key + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - issuccess + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/ratify/templates/akv-certificate-provider.yaml b/charts/ratify/templates/akv-certificate-provider.yaml deleted file mode 100644 index 662e75426..000000000 --- a/charts/ratify/templates/akv-certificate-provider.yaml +++ /dev/null @@ -1,34 +0,0 @@ -{{- if .Values.akvCertConfig.enabled }} -apiVersion: config.ratify.deislabs.io/v1beta1 -kind: CertificateStore -metadata: - name: certstore-akv - annotations: - helm.sh/hook: pre-install,pre-upgrade - helm.sh/hook-weight: "5" -spec: - provider: azurekeyvault - parameters: - vaultURI: {{ required "vaultURI must be provided when AKV cert config is enabled" .Values.akvCertConfig.vaultURI }} - certificates: | - array: - {{- if .Values.akvCertConfig.cert1Name }} - - | - certificateName: {{ .Values.akvCertConfig.cert1Name }} - certificateVersion: {{ .Values.akvCertConfig.cert1Version }} - {{ end }} - {{- if .Values.akvCertConfig.cert2Name }} - - | - certificateName: {{ .Values.akvCertConfig.cert2Name }} - certificateVersion: {{ .Values.akvCertConfig.cert2Version }} - {{ end }} - {{- range .Values.akvCertConfig.certificates }} - {{- if .certificateName }} - - | - certificateName: {{ .certificateName }} - certificateVersion: {{ .certificateVersion }} - {{- end }} - {{- end }} - tenantID: {{ required "tenantID must be provided when AKV cert config is enabled" .Values.akvCertConfig.tenantId }} - clientID: {{ required "clientID must be provided when use workload identity in akv" .Values.azureWorkloadIdentity.clientId }} -{{ end }} \ No newline at end of file diff --git a/charts/ratify/templates/akv-key-management-provider.yaml b/charts/ratify/templates/akv-key-management-provider.yaml new file mode 100644 index 000000000..548080600 --- /dev/null +++ b/charts/ratify/templates/akv-key-management-provider.yaml @@ -0,0 +1,30 @@ +{{- if .Values.akvCertConfig.enabled }} +apiVersion: config.ratify.deislabs.io/v1beta1 +kind: KeyManagementProvider +metadata: + name: kmprovider-akv + annotations: + helm.sh/hook: pre-install,pre-upgrade + helm.sh/hook-weight: "5" +spec: + type: azurekeyvault + parameters: + vaultURI: {{ required "vaultURI must be provided when AKV cert config is enabled" .Values.akvCertConfig.vaultURI }} + certificates: + {{- if .Values.akvCertConfig.cert1Name }} + - name: {{ .Values.akvCertConfig.cert1Name }} + version: {{ .Values.akvCertConfig.cert1Version }} + {{ end }} + {{- if .Values.akvCertConfig.cert2Name }} + - name: {{ .Values.akvCertConfig.cert2Name }} + version: {{ .Values.akvCertConfig.cert2Version }} + {{ end }} + {{- range .Values.akvCertConfig.certificates }} + {{- if .name }} + - name: {{ .name }} + version: {{ .version }} + {{- end }} + {{- end }} + tenantID: {{ required "tenantID must be provided when AKV cert config is enabled" .Values.akvCertConfig.tenantId }} + clientID: {{ required "clientID must be provided when use workload identity in akv" .Values.azureWorkloadIdentity.clientId }} +{{ end }} \ No newline at end of file diff --git a/charts/ratify/templates/inline-certificate-provider.yaml b/charts/ratify/templates/inline-key-management-provider.yaml similarity index 81% rename from charts/ratify/templates/inline-certificate-provider.yaml rename to charts/ratify/templates/inline-key-management-provider.yaml index 29fba488c..aafbad449 100644 --- a/charts/ratify/templates/inline-certificate-provider.yaml +++ b/charts/ratify/templates/inline-key-management-provider.yaml @@ -2,28 +2,30 @@ --- {{- if .Values.notationCert }} apiVersion: config.ratify.deislabs.io/v1beta1 -kind: CertificateStore +kind: KeyManagementProvider metadata: name: {{$fullname}}-notation-inline-cert annotations: helm.sh/hook: pre-install,pre-upgrade helm.sh/hook-weight: "5" spec: - provider: inline + type: inline parameters: + contentType: certificate value: {{ .Values.notationCert | quote }} {{- end }} --- {{- range $i, $cert := .Values.notationCerts }} apiVersion: config.ratify.deislabs.io/v1beta1 -kind: CertificateStore +kind: KeyManagementProvider metadata: name: {{$fullname}}-notation-inline-cert-{{$i}} annotations: helm.sh/hook: pre-install,pre-upgrade helm.sh/hook-weight: "5" spec: - provider: inline + type: inline parameters: + contentType: certificate value: {{ $cert | quote }} {{- end }} \ No newline at end of file diff --git a/charts/ratify/templates/ratify-manager-role-clusterrole.yaml b/charts/ratify/templates/ratify-manager-role-clusterrole.yaml index b5a8f5dc8..854cf542b 100644 --- a/charts/ratify/templates/ratify-manager-role-clusterrole.yaml +++ b/charts/ratify/templates/ratify-manager-role-clusterrole.yaml @@ -83,6 +83,32 @@ rules: - get - patch - update +- apiGroups: + - config.ratify.deislabs.io + resources: + - keymanagementproviders + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - config.ratify.deislabs.io + resources: + - keymanagementproviders/finalizers + verbs: + - update +- apiGroups: + - config.ratify.deislabs.io + resources: + - keymanagementproviders/status + verbs: + - get + - patch + - update - apiGroups: - config.ratify.deislabs.io resources: diff --git a/charts/ratify/templates/verifier.yaml b/charts/ratify/templates/verifier.yaml index 93b9c5c81..eea0c33c0 100644 --- a/charts/ratify/templates/verifier.yaml +++ b/charts/ratify/templates/verifier.yaml @@ -15,7 +15,7 @@ spec: verificationCertStores: certs: {{- if .Values.akvCertConfig.enabled }} - - certstore-akv + - kmprovider-akv {{- else }} {{- if .Values.notationCert }} {{- if .Values.notationCerts }} diff --git a/config/crd/bases/config.ratify.deislabs.io_keymanagementproviders.yaml b/config/crd/bases/config.ratify.deislabs.io_keymanagementproviders.yaml new file mode 100644 index 000000000..29ddb906d --- /dev/null +++ b/config/crd/bases/config.ratify.deislabs.io_keymanagementproviders.yaml @@ -0,0 +1,87 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: keymanagementproviders.config.ratify.deislabs.io +spec: + group: config.ratify.deislabs.io + names: + kind: KeyManagementProvider + listKind: KeyManagementProviderList + plural: keymanagementproviders + singular: keymanagementprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.issuccess + name: IsSuccess + type: boolean + - jsonPath: .status.brieferror + name: Error + type: string + - jsonPath: .status.lastfetchedtime + name: LastFetchedTime + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: KeyManagementProvider is the Schema for the keymanagementproviders + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: KeyManagementProviderSpec defines the desired state of KeyManagementProvider + properties: + parameters: + description: Parameters of the key management provider + type: object + x-kubernetes-preserve-unknown-fields: true + type: + description: Name of the key management provider + type: string + type: object + status: + description: KeyManagementProviderStatus defines the observed state of + KeyManagementProvider + properties: + brieferror: + description: Truncated error message if the message is too long + type: string + error: + description: Error message if operation was unsuccessful + type: string + issuccess: + description: Is successful in loading certificate/key files + type: boolean + lastfetchedtime: + description: The time stamp of last successful certificate/key fetch + operation. If operation failed, last fetched time shows the time + of error + format: date-time + type: string + properties: + description: provider specific properties of the each individual certificate/key + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - issuccess + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index df29c21ed..26aa14b25 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -6,6 +6,7 @@ resources: - bases/config.ratify.deislabs.io_stores.yaml - bases/config.ratify.deislabs.io_certificatestores.yaml - bases/config.ratify.deislabs.io_policies.yaml + - bases/config.ratify.deislabs.io_keymanagementproviders.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -15,6 +16,7 @@ patchesStrategicMerge: #- patches/webhook_in_stores.yaml #- patches/webhook_in_certificatestores.yaml #- patches/webhook_in_policies.yaml + #- patches/webhook_in_keymanagementproviders.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -23,6 +25,7 @@ patchesStrategicMerge: #- patches/cainjection_in_stores.yaml #- patches/cainjection_in_certificatestores.yaml #- patches/cainjection_in_policies.yaml + #- patches/cainjection_in_keymanagementproviders.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_keymanagementproviders.yaml b/config/crd/patches/cainjection_in_keymanagementproviders.yaml new file mode 100644 index 000000000..8be958b17 --- /dev/null +++ b/config/crd/patches/cainjection_in_keymanagementproviders.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: keymanagementproviders.config.ratify.deislabs.io diff --git a/config/crd/patches/webhook_in_keymanagementproviders.yaml b/config/crd/patches/webhook_in_keymanagementproviders.yaml new file mode 100644 index 000000000..348fc9144 --- /dev/null +++ b/config/crd/patches/webhook_in_keymanagementproviders.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: keymanagementproviders.config.ratify.deislabs.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/keymanagementprovider_editor_role.yaml b/config/rbac/keymanagementprovider_editor_role.yaml new file mode 100644 index 000000000..cacb1a1d9 --- /dev/null +++ b/config/rbac/keymanagementprovider_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit keymanagementproviders. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: keymanagementprovider-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: ratify + app.kubernetes.io/part-of: ratify + app.kubernetes.io/managed-by: kustomize + name: keymanagementprovider-editor-role +rules: +- apiGroups: + - config.ratify.deislabs.io + resources: + - keymanagementproviders + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - config.ratify.deislabs.io + resources: + - keymanagementproviders/status + verbs: + - get diff --git a/config/rbac/keymanagementprovider_viewer_role.yaml b/config/rbac/keymanagementprovider_viewer_role.yaml new file mode 100644 index 000000000..890bdd731 --- /dev/null +++ b/config/rbac/keymanagementprovider_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view keymanagementproviders. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: keymanagementprovider-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: ratify + app.kubernetes.io/part-of: ratify + app.kubernetes.io/managed-by: kustomize + name: keymanagementprovider-viewer-role +rules: +- apiGroups: + - config.ratify.deislabs.io + resources: + - keymanagementproviders + verbs: + - get + - list + - watch +- apiGroups: + - config.ratify.deislabs.io + resources: + - keymanagementproviders/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 5a2210579..36974c9aa 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -31,6 +31,32 @@ rules: - get - patch - update +- apiGroups: + - config.ratify.deislabs.io + resources: + - keymanagementproviders + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - config.ratify.deislabs.io + resources: + - keymanagementproviders/finalizers + verbs: + - update +- apiGroups: + - config.ratify.deislabs.io + resources: + - keymanagementproviders/status + verbs: + - get + - patch + - update - apiGroups: - config.ratify.deislabs.io resources: diff --git a/config/samples/config_v1beta1_keymanagementprovider_akv.yaml b/config/samples/config_v1beta1_keymanagementprovider_akv.yaml new file mode 100644 index 000000000..ae25f7f6a --- /dev/null +++ b/config/samples/config_v1beta1_keymanagementprovider_akv.yaml @@ -0,0 +1,13 @@ +apiVersion: config.ratify.deislabs.io/v1beta1 +kind: KeyManagementProvider +metadata: + name: keymanagementprovider-inline +spec: + type: azurekeyvault + parameters: + vaultURI: https://yourkeyvault.vault.azure.net/ + certificates: + - name: yourCertName + version: yourCertVersion # Optional, fetch latest version if empty + tenantID: + clientID: diff --git a/config/samples/config_v1beta1_keymanagementprovider_inline.yaml b/config/samples/config_v1beta1_keymanagementprovider_inline.yaml new file mode 100644 index 000000000..8c71a965a --- /dev/null +++ b/config/samples/config_v1beta1_keymanagementprovider_inline.yaml @@ -0,0 +1,29 @@ +apiVersion: config.ratify.deislabs.io/v1beta1 +kind: KeyManagementProvider +metadata: + name: keymanagementprovider-inline +spec: + type: inline + parameters: + contentType: certificate + value: | + -----BEGIN CERTIFICATE----- + MIIDWDCCAkCgAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzEL + MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEb + MBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMCAXDTIyMTIwMjA4MDg0NFoYDzIx + MjIxMjAzMDgwODQ0WjBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNV + BAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5l + dHdvcmtzLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnoskJWB0 + ZsYcfbTvCYQMLqWaB/yN3Jf7Ryxvndrij83fWEQPBQJi8Mk8SpNqm2x9uP3gsQDc + L/73a0p6/D+hza2jQQVhebe/oB0LJtUoD5LXlJ83UQdZETLMYAzeBNcBR4kMecrY + CnE6yjHeiEWdAH+U7Mt39zJh+9lGIcbk0aUE5UOp8o3t5RWFDcl9hQ7QOXROwmpO + thLUIiY/bcPpsg/2nH1nzFjqiBef3sgopFCTgtJ7qF8B83Xy/+hJ5vD29xsbSwuB + 3iLE7qLxu2NxdIa4oL0Y2QKMh/getjI0xnvwAmPkFiFbzC7LFdDfd6+gA5GpUXxL + u6UmwucAgiljGQIDAQABoycwJTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYI + KwYBBQUHAwMwDQYJKoZIhvcNAQELBQADggEBAFvRW/mGjnnMNFKJc/e3o/+yiJor + dcrq/1UzyD7eNmOaASXz8rrrFT/6/TBXExPuB2OIf9OgRJFfPGLxmzCwVgaWQbK0 + VfTN4MQzRrSwPmNYsBAAwLxXbarYlMbm4DEmdJGyVikq08T2dZI51GC/YXEwzlnv + ldN0dBflb/FKkY5rAp0JgpHLGKeStxFvB62noBjWfrm7ShCf9gkn1CjmgvP/sYK0 + pJgA1FHPd6EeB6yRBpLV4EJgQYUJoOpbHz+us62jKj5fAXsX052LPmk9ArmP0uJ1 + CJLNdj+aShCs4paSWOObDmIyXHwCx3MxCvYsFk/Wsnwura6jGC+cNsjzSx4= + -----END CERTIFICATE----- diff --git a/config/samples/config_v1beta1_verifier_notation_certstore.yaml b/config/samples/config_v1beta1_verifier_notation_kmprovider.yaml similarity index 83% rename from config/samples/config_v1beta1_verifier_notation_certstore.yaml rename to config/samples/config_v1beta1_verifier_notation_kmprovider.yaml index ec79999f8..407f08f02 100644 --- a/config/samples/config_v1beta1_verifier_notation_certstore.yaml +++ b/config/samples/config_v1beta1_verifier_notation_kmprovider.yaml @@ -8,11 +8,11 @@ spec: parameters: verificationCertStores: certs: - - certstore-akv - - certstore-akv1 + - kmprovider-akv + - kmprovider-akv1 certs1: - - certstore-akv2 - - certstore-akv3 + - kmprovider-akv2 + - kmprovider-akv3 trustPolicyDoc: version: "1.0" trustPolicies: diff --git a/config/samples/config_v1beta1_verifier_notation_specificnscertstore.yaml b/config/samples/config_v1beta1_verifier_notation_specificnskmprovider.yaml similarity index 100% rename from config/samples/config_v1beta1_verifier_notation_specificnscertstore.yaml rename to config/samples/config_v1beta1_verifier_notation_specificnskmprovider.yaml diff --git a/errors/types.go b/errors/types.go index 09ea9a31c..8b4529e83 100644 --- a/errors/types.go +++ b/errors/types.go @@ -43,14 +43,15 @@ var ( type ComponentType string const ( - Verifier ComponentType = "verifier" - ReferrerStore ComponentType = "referrerStore" - Policy ComponentType = "policy" - Executor ComponentType = "executor" - Cache ComponentType = "cache" - AuthProvider ComponentType = "authProvider" - PolicyProvider ComponentType = "policyProvider" - CertProvider ComponentType = "certProvider" + Verifier ComponentType = "verifier" + ReferrerStore ComponentType = "referrerStore" + Policy ComponentType = "policy" + Executor ComponentType = "executor" + Cache ComponentType = "cache" + AuthProvider ComponentType = "authProvider" + PolicyProvider ComponentType = "policyProvider" + CertProvider ComponentType = "certProvider" + KeyManagementProvider ComponentType = "keyManagementProvider" ) // ErrorCode represents the error type. The errors are serialized via strings diff --git a/pkg/controllers/certificatestore_controller.go b/pkg/controllers/certificatestore_controller.go index 9f6d90b2a..69fef7b6d 100644 --- a/pkg/controllers/certificatestore_controller.go +++ b/pkg/controllers/certificatestore_controller.go @@ -45,8 +45,6 @@ var ( certificatesMap = map[string][]*x509.Certificate{} ) -const maxBriefErrLength = 30 - //+kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=certificatestores,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=certificatestores/status,verbs=get;update;patch //+kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=certificatestores/finalizers,verbs=update @@ -78,11 +76,26 @@ func (r *CertificateStoreReconciler) Reconcile(ctx context.Context, req ctrl.Req return ctrl.Result{}, client.IgnoreNotFound(err) } - // get cert provider attributes - attributes, err := getCertStoreConfig(certStore.Spec) lastFetchedTime := metav1.Now() isFetchSuccessful := false + // ensure that certificate store and key management provider are not configured together + var keyManagementProviderList configv1beta1.KeyManagementProviderList + if err := r.List(ctx, &keyManagementProviderList); err != nil { + logger.Error(err, "unable to list key management providers") + return ctrl.Result{}, err + } + + if len(keyManagementProviderList.Items) > 0 { + err := fmt.Errorf("key management provider already exists: key management provider and certificate store cannot be configured together") + logger.Error(err) + writeCertStoreStatus(ctx, r, certStore, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) + // Note: for backwards compatibility in upgrade scenarios, we are not returning an error here + } + + // get cert provider attributes + attributes, err := getCertStoreConfig(certStore.Spec) + if err != nil { writeCertStoreStatus(ctx, r, certStore, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) return ctrl.Result{}, err diff --git a/pkg/controllers/constants.go b/pkg/controllers/constants.go new file mode 100644 index 000000000..922ab6ef0 --- /dev/null +++ b/pkg/controllers/constants.go @@ -0,0 +1,16 @@ +// 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 controllers + +const maxBriefErrLength = 30 diff --git a/pkg/controllers/keymanagementprovider_controller.go b/pkg/controllers/keymanagementprovider_controller.go new file mode 100644 index 000000000..424f21364 --- /dev/null +++ b/pkg/controllers/keymanagementprovider_controller.go @@ -0,0 +1,194 @@ +/* +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 controllers + +import ( + "context" + "encoding/json" + "fmt" + + _ "github.com/deislabs/ratify/pkg/keymanagementprovider/azurekeyvault" // register azure key vault key management provider + _ "github.com/deislabs/ratify/pkg/keymanagementprovider/inline" // register inline key management provider + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + configv1beta1 "github.com/deislabs/ratify/api/v1beta1" + c "github.com/deislabs/ratify/config" + "github.com/deislabs/ratify/pkg/keymanagementprovider" + "github.com/deislabs/ratify/pkg/keymanagementprovider/config" + "github.com/deislabs/ratify/pkg/keymanagementprovider/factory" + "github.com/deislabs/ratify/pkg/keymanagementprovider/types" + "github.com/sirupsen/logrus" +) + +// KeyManagementProviderReconciler reconciles a KeyManagementProvider object +type KeyManagementProviderReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=keymanagementproviders,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=keymanagementproviders/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=keymanagementproviders/finalizers,verbs=update +func (r *KeyManagementProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := logrus.WithContext(ctx) + + var resource = req.NamespacedName.String() + var keyManagementProvider configv1beta1.KeyManagementProvider + + logger.Infof("reconciling key management provider '%v'", resource) + + if err := r.Get(ctx, req.NamespacedName, &keyManagementProvider); err != nil { + if apierrors.IsNotFound(err) { + logger.Infof("deletion detected, removing key management provider %v", resource) + keymanagementprovider.DeleteCertificatesFromMap(resource) + } else { + logger.Error(err, "unable to fetch key management provider") + } + + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + lastFetchedTime := metav1.Now() + isFetchSuccessful := false + + // get certificate store list to check if certificate store is configured + var certificateStoreList configv1beta1.CertificateStoreList + if err := r.List(ctx, &certificateStoreList); err != nil { + logger.Error(err, "unable to list certificate stores") + return ctrl.Result{}, err + } + // if certificate store is configured, return error. Only one of certificate store and key management provider can be configured + if len(certificateStoreList.Items) > 0 { + err := fmt.Errorf("certificate store already exists: key management provider and certificate store cannot be configured together") + logger.Error(err) + writeKMProviderStatus(ctx, r, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) + // Note: for backwards compatibility in upgrade scenarios, we are not returning an error here + } + + provider, err := specToKeyManagementProviderProvider(keyManagementProvider.Spec) + if err != nil { + writeKMProviderStatus(ctx, r, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) + return ctrl.Result{}, err + } + + certificates, certAttributes, err := provider.GetCertificates(ctx) + if err != nil { + writeKMProviderStatus(ctx, r, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) + return ctrl.Result{}, fmt.Errorf("Error fetching certificates in KMProvider %v with %v provider, error: %w", resource, keyManagementProvider.Spec.Type, err) + } + keymanagementprovider.SetCertificatesInMap(resource, certificates) + isFetchSuccessful = true + emptyErrorString := "" + writeKMProviderStatus(ctx, r, &keyManagementProvider, logger, isFetchSuccessful, emptyErrorString, lastFetchedTime, certAttributes) + + logger.Infof("%v certificates fetched for key management provider %v", len(certificates), resource) + + // returning empty result and no error to indicate we’ve successfully reconciled this object + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *KeyManagementProviderReconciler) SetupWithManager(mgr ctrl.Manager) error { + pred := predicate.GenerationChangedPredicate{} + + // status updates will trigger a reconcile event + // if there are no changes to spec of CRD, this event should be filtered out by using the predicate + // see more discussions at https://github.com/kubernetes-sigs/kubebuilder/issues/618 + return ctrl.NewControllerManagedBy(mgr). + For(&configv1beta1.KeyManagementProvider{}).WithEventFilter(pred). + Complete(r) +} + +// specToKeyManagementProviderProvider creates KeyManagementProviderProvider from KeyManagementProviderSpec config +func specToKeyManagementProviderProvider(spec configv1beta1.KeyManagementProviderSpec) (keymanagementprovider.KeyManagementProvider, error) { + kmProviderConfig, err := rawToKeyManagementProviderConfig(spec.Parameters.Raw, spec.Type) + if err != nil { + return nil, fmt.Errorf("failed to parse key management provider config: %w", err) + } + + // TODO: add Version and Address to KeyManagementProviderSpec + keyManagementProviderProvider, err := factory.CreateKeyManagementProviderFromConfig(kmProviderConfig, "0.1.0", c.GetDefaultPluginPath()) + if err != nil { + return nil, fmt.Errorf("failed to create key management provider provider: %w", err) + } + + return keyManagementProviderProvider, nil +} + +// rawToKeyManagementProviderConfig converts raw json to KeyManagementProviderConfig +func rawToKeyManagementProviderConfig(raw []byte, keyManagamentSystemName string) (config.KeyManagementProviderConfig, error) { + pluginConfig := config.KeyManagementProviderConfig{} + + if string(raw) == "" { + return config.KeyManagementProviderConfig{}, fmt.Errorf("no key management provider parameters provided") + } + if err := json.Unmarshal(raw, &pluginConfig); err != nil { + return config.KeyManagementProviderConfig{}, fmt.Errorf("unable to decode key management provider parameters.Raw: %s, err: %w", raw, err) + } + + pluginConfig[types.Type] = keyManagamentSystemName + + return pluginConfig, nil +} + +// writeKMProviderStatus updates the status of the key management provider resource +func writeKMProviderStatus(ctx context.Context, r client.StatusClient, keyManagementProvider *configv1beta1.KeyManagementProvider, logger *logrus.Entry, isSuccess bool, errorString string, operationTime metav1.Time, kmProviderStatus keymanagementprovider.KeyManagementProviderStatus) { + if isSuccess { + updateKMProviderSuccessStatus(keyManagementProvider, &operationTime, kmProviderStatus) + } else { + updateKMProviderErrorStatus(keyManagementProvider, errorString, &operationTime) + } + if statusErr := r.Status().Update(ctx, keyManagementProvider); statusErr != nil { + logger.Error(statusErr, ",unable to update key management provider error status") + } +} + +// updateKMProviderErrorStatus updates the key management provider status with error, brief error and last fetched time +func updateKMProviderErrorStatus(keyManagementProvider *configv1beta1.KeyManagementProvider, errorString string, operationTime *metav1.Time) { + // truncate brief error string to maxBriefErrLength + briefErr := errorString + if len(errorString) > maxBriefErrLength { + briefErr = fmt.Sprintf("%s...", errorString[:maxBriefErrLength]) + } + keyManagementProvider.Status.IsSuccess = false + keyManagementProvider.Status.Error = errorString + keyManagementProvider.Status.BriefError = briefErr + keyManagementProvider.Status.LastFetchedTime = operationTime +} + +// updateKMProviderSuccessStatus updates the key management provider status if status argument is non nil +// Success status includes last fetched time and other provider-specific properties +func updateKMProviderSuccessStatus(keyManagementProvider *configv1beta1.KeyManagementProvider, lastOperationTime *metav1.Time, kmProviderStatus keymanagementprovider.KeyManagementProviderStatus) { + keyManagementProvider.Status.IsSuccess = true + keyManagementProvider.Status.Error = "" + keyManagementProvider.Status.BriefError = "" + keyManagementProvider.Status.LastFetchedTime = lastOperationTime + + if kmProviderStatus != nil { + jsonString, _ := json.Marshal(kmProviderStatus) + + raw := runtime.RawExtension{ + Raw: jsonString, + } + keyManagementProvider.Status.Properties = raw + } +} diff --git a/pkg/controllers/keymanagementprovider_controller_test.go b/pkg/controllers/keymanagementprovider_controller_test.go new file mode 100644 index 000000000..039d825fb --- /dev/null +++ b/pkg/controllers/keymanagementprovider_controller_test.go @@ -0,0 +1,267 @@ +/* +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 controllers + +import ( + "context" + "fmt" + "reflect" + "testing" + + configv1beta1 "github.com/deislabs/ratify/api/v1beta1" + "github.com/deislabs/ratify/pkg/keymanagementprovider" + "github.com/deislabs/ratify/pkg/keymanagementprovider/config" + "github.com/sirupsen/logrus" + "sigs.k8s.io/controller-runtime/pkg/client" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// TestUpdateErrorStatus tests the updateErrorStatus method +func TestKMProviderUpdateErrorStatus(t *testing.T) { + var parametersString = "{\"certs\":{\"name\":\"certName\"}}" + var kmProviderStatus = []byte(parametersString) + + status := configv1beta1.KeyManagementProviderStatus{ + IsSuccess: true, + Properties: runtime.RawExtension{ + Raw: kmProviderStatus, + }, + } + keyManagementProvider := configv1beta1.KeyManagementProvider{ + Status: status, + } + expectedErr := "it's a long error from unit test" + lastFetchedTime := metav1.Now() + updateKMProviderErrorStatus(&keyManagementProvider, expectedErr, &lastFetchedTime) + + if keyManagementProvider.Status.IsSuccess != false { + t.Fatalf("Unexpected error, expected isSuccess to be false , actual %+v", keyManagementProvider.Status.IsSuccess) + } + + if keyManagementProvider.Status.Error != expectedErr { + t.Fatalf("Unexpected error string, expected %+v, got %+v", expectedErr, keyManagementProvider.Status.Error) + } + expectedBriedErr := fmt.Sprintf("%s...", expectedErr[:30]) + if keyManagementProvider.Status.BriefError != expectedBriedErr { + t.Fatalf("Unexpected error string, expected %+v, got %+v", expectedBriedErr, keyManagementProvider.Status.Error) + } + + //make sure properties of last cached cert was not overridden + if len(keyManagementProvider.Status.Properties.Raw) == 0 { + t.Fatalf("Unexpected properties, expected %+v, got %+v", parametersString, string(keyManagementProvider.Status.Properties.Raw)) + } +} + +// TestKMProviderUpdateSuccessStatus tests the updateSuccessStatus method +func TestKMProviderUpdateSuccessStatus(t *testing.T) { + kmProviderStatus := keymanagementprovider.KeyManagementProviderStatus{} + properties := map[string]string{} + properties["CertName"] = "wabbit" + properties["Version"] = "ABC" + + kmProviderStatus["Certificates"] = properties + + lastFetchedTime := metav1.Now() + + status := configv1beta1.KeyManagementProviderStatus{ + IsSuccess: false, + Error: "error from last operation", + } + keyManagementProvider := configv1beta1.KeyManagementProvider{ + Status: status, + } + + updateKMProviderSuccessStatus(&keyManagementProvider, &lastFetchedTime, kmProviderStatus) + + if keyManagementProvider.Status.IsSuccess != true { + t.Fatalf("Expected isSuccess to be true , actual %+v", keyManagementProvider.Status.IsSuccess) + } + + if keyManagementProvider.Status.Error != "" { + t.Fatalf("Unexpected error string, actual %+v", keyManagementProvider.Status.Error) + } + + //make sure properties of last cached cert was updated + if len(keyManagementProvider.Status.Properties.Raw) == 0 { + t.Fatalf("Properties should not be empty") + } +} + +// TestKMProviderUpdateSuccessStatus tests the updateSuccessStatus method with empty properties +func TestKMProviderUpdateSuccessStatus_emptyProperties(t *testing.T) { + lastFetchedTime := metav1.Now() + status := configv1beta1.KeyManagementProviderStatus{ + IsSuccess: false, + Error: "error from last operation", + } + keyManagementProvider := configv1beta1.KeyManagementProvider{ + Status: status, + } + + updateKMProviderSuccessStatus(&keyManagementProvider, &lastFetchedTime, nil) + + if keyManagementProvider.Status.IsSuccess != true { + t.Fatalf("Expected isSuccess to be true , actual %+v", keyManagementProvider.Status.IsSuccess) + } + + if keyManagementProvider.Status.Error != "" { + t.Fatalf("Unexpected error string, actual %+v", keyManagementProvider.Status.Error) + } + + //make sure properties of last cached cert was updated + if len(keyManagementProvider.Status.Properties.Raw) != 0 { + t.Fatalf("Properties should be empty") + } +} + +// TestRawToKeyManagementProviderConfig tests the rawToKeyManagementProviderConfig method +func TestRawToKeyManagementProviderConfig(t *testing.T) { + testCases := []struct { + name string + raw []byte + expectErr bool + expectConfig config.KeyManagementProviderConfig + }{ + { + name: "empty Raw", + raw: []byte{}, + expectErr: true, + expectConfig: config.KeyManagementProviderConfig{}, + }, + { + name: "unmarshal failure", + raw: []byte("invalid"), + expectErr: true, + expectConfig: config.KeyManagementProviderConfig{}, + }, + { + name: "valid Raw", + raw: []byte("{\"type\": \"inline\"}"), + expectErr: false, + expectConfig: config.KeyManagementProviderConfig{ + "type": "inline", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + config, err := rawToKeyManagementProviderConfig(tc.raw, "inline") + + if tc.expectErr != (err != nil) { + t.Fatalf("Expected error to be %t, got %t", tc.expectErr, err != nil) + } + if !reflect.DeepEqual(config, tc.expectConfig) { + t.Fatalf("Expected config to be %v, got %v", tc.expectConfig, config) + } + }) + } +} + +// TestSpecToKeyManagementProviderProvider tests the specToKeyManagementProviderProvider method +func TestSpecToKeyManagementProviderProvider(t *testing.T) { + testCases := []struct { + name string + spec configv1beta1.KeyManagementProviderSpec + expectErr bool + }{ + { + name: "empty spec", + spec: configv1beta1.KeyManagementProviderSpec{}, + expectErr: true, + }, + { + name: "missing inline provider required fields", + spec: configv1beta1.KeyManagementProviderSpec{ + Type: "inline", + Parameters: runtime.RawExtension{ + Raw: []byte("{\"type\": \"inline\"}"), + }, + }, + expectErr: true, + }, + { + name: "valid spec", + spec: configv1beta1.KeyManagementProviderSpec{ + Type: "inline", + Parameters: runtime.RawExtension{ + Raw: []byte(`{"type": "inline", "contentType": "certificate", "value": "-----BEGIN CERTIFICATE-----\nMIID2jCCAsKgAwIBAgIQXy2VqtlhSkiZKAGhsnkjbDANBgkqhkiG9w0BAQsFADBvMRswGQYDVQQD\nExJyYXRpZnkuZXhhbXBsZS5jb20xDzANBgNVBAsTBk15IE9yZzETMBEGA1UEChMKTXkgQ29tcGFu\neTEQMA4GA1UEBxMHUmVkbW9uZDELMAkGA1UECBMCV0ExCzAJBgNVBAYTAlVTMB4XDTIzMDIwMTIy\nNDUwMFoXDTI0MDIwMTIyNTUwMFowbzEbMBkGA1UEAxMScmF0aWZ5LmV4YW1wbGUuY29tMQ8wDQYD\nVQQLEwZNeSBPcmcxEzARBgNVBAoTCk15IENvbXBhbnkxEDAOBgNVBAcTB1JlZG1vbmQxCzAJBgNV\nBAgTAldBMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL10bM81\npPAyuraORABsOGS8M76Bi7Guwa3JlM1g2D8CuzSfSTaaT6apy9GsccxUvXd5cmiP1ffna5z+EFmc\nizFQh2aq9kWKWXDvKFXzpQuhyqD1HeVlRlF+V0AfZPvGt3VwUUjNycoUU44ctCWmcUQP/KShZev3\n6SOsJ9q7KLjxxQLsUc4mg55eZUThu8mGB8jugtjsnLUYvIWfHhyjVpGrGVrdkDMoMn+u33scOmrt\nsBljvq9WVo4T/VrTDuiOYlAJFMUae2Ptvo0go8XTN3OjLblKeiK4C+jMn9Dk33oGIT9pmX0vrDJV\nX56w/2SejC1AxCPchHaMuhlwMpftBGkCAwEAAaNyMHAwDgYDVR0PAQH/BAQDAgeAMAkGA1UdEwQC\nMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHwYDVR0jBBgwFoAU0eaKkZj+MS9jCp9Dg1zdv3v/aKww\nHQYDVR0OBBYEFNHmipGY/jEvYwqfQ4Nc3b97/2isMA0GCSqGSIb3DQEBCwUAA4IBAQBNDcmSBizF\nmpJlD8EgNcUCy5tz7W3+AAhEbA3vsHP4D/UyV3UgcESx+L+Nye5uDYtTVm3lQejs3erN2BjW+ds+\nXFnpU/pVimd0aYv6mJfOieRILBF4XFomjhrJOLI55oVwLN/AgX6kuC3CJY2NMyJKlTao9oZgpHhs\nLlxB/r0n9JnUoN0Gq93oc1+OLFjPI7gNuPXYOP1N46oKgEmAEmNkP1etFrEjFRgsdIFHksrmlOlD\nIed9RcQ087VLjmuymLgqMTFX34Q3j7XgN2ENwBSnkHotE9CcuGRW+NuiOeJalL8DBmFXXWwHTKLQ\nPp5g6m1yZXylLJaFLKz7tdMmO355\n-----END CERTIFICATE-----\n"}`), + }, + }, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := specToKeyManagementProviderProvider(tc.spec) + if tc.expectErr != (err != nil) { + t.Fatalf("Expected error to be %t, got %t", tc.expectErr, err != nil) + } + }) + } +} + +func TestWriteKMProviderStatus(t *testing.T) { + logger := logrus.WithContext(context.Background()) + lastFetchedTime := metav1.Now() + testCases := []struct { + name string + isSuccess bool + kmProvider *configv1beta1.KeyManagementProvider + errString string + reconciler client.StatusClient + }{ + { + name: "success status", + isSuccess: true, + errString: "", + kmProvider: &configv1beta1.KeyManagementProvider{}, + reconciler: &mockStatusClient{}, + }, + { + name: "error status", + isSuccess: false, + kmProvider: &configv1beta1.KeyManagementProvider{}, + errString: "a long error string that exceeds the max length of 30 characters", + reconciler: &mockStatusClient{}, + }, + { + name: "status update failed", + isSuccess: true, + kmProvider: &configv1beta1.KeyManagementProvider{}, + reconciler: &mockStatusClient{ + updateFailed: true, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + writeKMProviderStatus(context.Background(), tc.reconciler, tc.kmProvider, logger, tc.isSuccess, tc.errString, lastFetchedTime, nil) + + if tc.kmProvider.Status.IsSuccess != tc.isSuccess { + t.Fatalf("Expected isSuccess to be %+v , actual %+v", tc.isSuccess, tc.kmProvider.Status.IsSuccess) + } + + if tc.kmProvider.Status.Error != tc.errString { + t.Fatalf("Expected Error to be %+v , actual %+v", tc.errString, tc.kmProvider.Status.Error) + } + }) + } +} diff --git a/pkg/keymanagementprovider/azurekeyvault/auth.go b/pkg/keymanagementprovider/azurekeyvault/auth.go new file mode 100644 index 000000000..6769038eb --- /dev/null +++ b/pkg/keymanagementprovider/azurekeyvault/auth.go @@ -0,0 +1,116 @@ +/* +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 azurekeyvault + +// This class is based on implementation from azure secret store csi provider +// Source: https://github.com/Azure/secrets-store-csi-driver-provider-azure/tree/release-1.4/pkg/auth +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + "github.com/deislabs/ratify/pkg/utils/azureauth" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/adal" +) + +const ( + // the format for expires_on in UTC with AM/PM + expiresOnDateFormatPM = "1/2/2006 15:04:05 PM +00:00" + // the format for expires_on in UTC without AM/PM + expiresOnDateFormat = "1/2/2006 15:04:05 +00:00" + + tokenTypeBearer = "Bearer" + // For Azure AD Workload Identity, the audience recommended for use is + // "api://AzureADTokenExchange" + DefaultTokenAudience = "api://AzureADTokenExchange" //nolint +) + +// authResult contains the subset of results from token acquisition operation in ConfidentialClientApplication +// For details see https://aka.ms/msal-net-authenticationresult +type authResult struct { + accessToken string + expiresOn time.Time + grantedScopes []string + declinedScopes []string +} + +func getAuthorizerForWorkloadIdentity(ctx context.Context, tenantID, clientID, resource string) (autorest.Authorizer, error) { + scope := resource + // .default needs to be added to the scope + if !strings.Contains(resource, ".default") { + scope = fmt.Sprintf("%s/.default", resource) + } + + result, err := azureauth.GetAADAccessToken(ctx, tenantID, clientID, scope) + if err != nil { + return nil, fmt.Errorf("failed to acquire token: %w", err) + } + + token := adal.Token{ + AccessToken: result.AccessToken, + Resource: resource, + Type: tokenTypeBearer, + } + token.ExpiresOn, err = parseExpiresOn(result.ExpiresOn.UTC().Local().Format(expiresOnDateFormat)) + if err != nil { + return nil, fmt.Errorf("failed to parse expires_on: %w", err) + } + + return autorest.NewBearerAuthorizer(authResult{ + accessToken: result.AccessToken, + expiresOn: result.ExpiresOn, + grantedScopes: result.GrantedScopes, + declinedScopes: result.DeclinedScopes, + }), nil +} + +// OAuthToken implements the OAuthTokenProvider interface. It returns the current access token. +func (ar authResult) OAuthToken() string { + return ar.accessToken +} + +// Vendored from https://github.com/Azure/go-autorest/blob/79575dd7ba2e88e7ce7ab84e167ec6653dcb70c1/autorest/adal/token.go +// converts expires_on to the number of seconds +func parseExpiresOn(s interface{}) (json.Number, error) { + // the JSON unmarshaler treats JSON numbers unmarshaled into an interface{} as float64 + asFloat64, ok := s.(float64) + if ok { + // this is the number of seconds as int case + return json.Number(strconv.FormatInt(int64(asFloat64), 10)), nil + } + asStr, ok := s.(string) + if !ok { + return "", fmt.Errorf("unexpected expires_on type %T", s) + } + // convert the expiration date to the number of seconds from the unix epoch + timeToDuration := func(t time.Time) json.Number { + return json.Number(strconv.FormatInt(t.UTC().Unix(), 10)) + } + if _, err := json.Number(asStr).Int64(); err == nil { + // this is the number of seconds case, no conversion required + return json.Number(asStr), nil + } else if eo, err := time.Parse(expiresOnDateFormatPM, asStr); err == nil { + return timeToDuration(eo), nil + } else if eo, err := time.Parse(expiresOnDateFormat, asStr); err == nil { + return timeToDuration(eo), nil + } + return json.Number(""), fmt.Errorf("unknown expires_on format %s", asStr) +} diff --git a/pkg/keymanagementprovider/azurekeyvault/auth_test.go b/pkg/keymanagementprovider/azurekeyvault/auth_test.go new file mode 100644 index 000000000..a7d3f5980 --- /dev/null +++ b/pkg/keymanagementprovider/azurekeyvault/auth_test.go @@ -0,0 +1,67 @@ +/* +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 azurekeyvault + +import ( + "fmt" + "testing" + "time" +) + +// Vendored from https://github.com/Azure/go-autorest/blob/7dd32b67be4e6c9386b9ba7b1c44a51263f05270/autorest/adal/token_test.go +func TestParseExpiresOn(t *testing.T) { + n := time.Now().UTC() + amPM := "AM" + if n.Hour() >= 12 { + amPM = "PM" + } + testcases := []struct { + Name string + String string + Value int64 + }{ + { + Name: "integer", + String: "3600", + Value: 3600, + }, + { + Name: "timestamp with AM/PM", + String: fmt.Sprintf("%d/%d/%d %d:%02d:%02d %s +00:00", n.Month(), n.Day(), n.Year(), n.Hour(), n.Minute(), n.Second(), amPM), + Value: n.Unix(), + }, + { + Name: "timestamp without AM/PM", + String: fmt.Sprintf("%02d/%02d/%02d %02d:%02d:%02d +00:00", n.Month(), n.Day(), n.Year(), n.Hour(), n.Minute(), n.Second()), + Value: n.Unix(), + }, + } + for _, tc := range testcases { + t.Run(tc.Name, func(subT *testing.T) { + jn, err := parseExpiresOn(tc.String) + if err != nil { + subT.Error(err) + } + i, err := jn.Int64() + if err != nil { + subT.Error(err) + } + if i != tc.Value { + subT.Logf("expected %d, got %d", tc.Value, i) + subT.Fail() + } + }) + } +} diff --git a/pkg/keymanagementprovider/azurekeyvault/provider.go b/pkg/keymanagementprovider/azurekeyvault/provider.go new file mode 100644 index 000000000..3230ae0ff --- /dev/null +++ b/pkg/keymanagementprovider/azurekeyvault/provider.go @@ -0,0 +1,323 @@ +/* +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 azurekeyvault + +// This class is based on implementation from azure secret store csi provider +// Source: https://github.com/Azure/secrets-store-csi-driver-provider-azure/tree/release-1.4/pkg/provider +import ( + "context" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "reflect" + "strings" + "time" + + re "github.com/deislabs/ratify/errors" + "github.com/deislabs/ratify/internal/logger" + "github.com/deislabs/ratify/pkg/keymanagementprovider" + "github.com/deislabs/ratify/pkg/keymanagementprovider/azurekeyvault/types" + "github.com/deislabs/ratify/pkg/keymanagementprovider/config" + "github.com/deislabs/ratify/pkg/keymanagementprovider/factory" + "github.com/deislabs/ratify/pkg/metrics" + "golang.org/x/crypto/pkcs12" + + kv "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" + "github.com/Azure/go-autorest/autorest/azure" +) + +const ( + providerName string = "azurekeyvault" + PKCS12ContentType string = "application/x-pkcs12" + PEMContentType string = "application/x-pem-file" +) + +var logOpt = logger.Option{ + ComponentType: logger.CertProvider, +} + +type AKVKeyManagementProviderConfig struct { + Type string `json:"type"` + VaultURI string `json:"vaultURI"` + TenantID string `json:"tenantID"` + ClientID string `json:"clientID"` + CloudName string `json:"cloudName,omitempty"` + Certificates []types.KeyVaultCertificate `json:"certificates,omitempty"` +} + +type akvKMProvider struct { + provider string + vaultURI string + tenantID string + clientID string + cloudName string + certificates []types.KeyVaultCertificate + cloudEnv *azure.Environment +} +type akvKMProviderFactory struct{} + +// init calls to register the provider +func init() { + factory.Register(providerName, &akvKMProviderFactory{}) +} + +// Create creates a new instance of the provider after marshalling and validating the configuration +func (f *akvKMProviderFactory) Create(_ string, keyManagementProviderConfig config.KeyManagementProviderConfig, _ string) (keymanagementprovider.KeyManagementProvider, error) { + conf := AKVKeyManagementProviderConfig{} + + keyManagementProviderConfigBytes, err := json.Marshal(keyManagementProviderConfig) + if err != nil { + return nil, re.ErrorCodeConfigInvalid.WithError(err).WithComponentType(re.KeyManagementProvider) + } + + if err := json.Unmarshal(keyManagementProviderConfigBytes, &conf); err != nil { + return nil, re.ErrorCodeConfigInvalid.NewError(re.CertProvider, "", re.EmptyLink, err, "failed to parse AKV key management provider configuration", re.HideStackTrace) + } + + azureCloudEnv, err := parseAzureEnvironment(conf.CloudName) + if err != nil { + return nil, re.ErrorCodeConfigInvalid.NewError(re.CertProvider, providerName, re.EmptyLink, nil, fmt.Sprintf("cloudName %s is not valid", conf.CloudName), re.HideStackTrace) + } + + if len(conf.Certificates) == 0 { + return nil, re.ErrorCodeConfigInvalid.NewError(re.CertProvider, providerName, re.EmptyLink, nil, "no keyvault certificates configured", re.HideStackTrace) + } + + provider := &akvKMProvider{ + provider: providerName, + vaultURI: strings.TrimSpace(conf.VaultURI), + tenantID: strings.TrimSpace(conf.TenantID), + clientID: strings.TrimSpace(conf.ClientID), + cloudName: strings.TrimSpace(conf.CloudName), + certificates: conf.Certificates, + cloudEnv: azureCloudEnv, + } + if err := provider.validate(); err != nil { + return nil, err + } + + return provider, nil +} + +// GetCertificates returns an array of certificates based on certificate properties defined in config +// get certificate retrieve the entire cert chain using getSecret API call +func (s *akvKMProvider) GetCertificates(ctx context.Context) (map[keymanagementprovider.KMPMapKey][]*x509.Certificate, keymanagementprovider.KeyManagementProviderStatus, error) { + logger.GetLogger(ctx, logOpt).Debugf("vaultURI %s", s.vaultURI) + + kvClient, err := initializeKvClient(ctx, s.cloudEnv.KeyVaultEndpoint, s.tenantID, s.clientID) + if err != nil { + return nil, nil, re.ErrorCodePluginInitFailure.NewError(re.CertProvider, providerName, re.AKVLink, err, "failed to get keyvault client", re.HideStackTrace) + } + + certsMap := map[keymanagementprovider.KMPMapKey][]*x509.Certificate{} + certsStatus := []map[string]string{} + for _, keyVaultCert := range s.certificates { + logger.GetLogger(ctx, logOpt).Debugf("fetching secret from key vault, certName %v, keyvault %v", keyVaultCert.Name, s.vaultURI) + + // fetch the object from Key Vault + // GetSecret is required so we can fetch the entire cert chain. See issue https://github.com/deislabs/ratify/issues/695 for details + startTime := time.Now() + secretBundle, err := kvClient.GetSecret(ctx, s.vaultURI, keyVaultCert.Name, keyVaultCert.Version) + + if err != nil { + return nil, nil, fmt.Errorf("failed to get secret objectName:%s, objectVersion:%s, error: %w", keyVaultCert.Name, keyVaultCert.Version, err) + } + + certResult, certProperty, err := getCertsFromSecretBundle(ctx, secretBundle, keyVaultCert.Name) + + if err != nil { + return nil, nil, fmt.Errorf("failed to get certificates from secret bundle:%w", err) + } + + metrics.ReportAKVCertificateDuration(ctx, time.Since(startTime).Milliseconds(), keyVaultCert.Name) + certsStatus = append(certsStatus, certProperty...) + certMapKey := keymanagementprovider.KMPMapKey{Name: keyVaultCert.Name, Version: keyVaultCert.Version} + certsMap[certMapKey] = certResult + } + + return certsMap, getCertStatusMap(certsStatus), nil +} + +// azure keyvault provider certificate status is a map from "certificates" key to an array of of certificate status +func getCertStatusMap(certsStatus []map[string]string) keymanagementprovider.KeyManagementProviderStatus { + status := keymanagementprovider.KeyManagementProviderStatus{} + status[types.CertificatesStatus] = certsStatus + return status +} + +// return a certificate status object that consist of the cert name, version and last refreshed time +func getCertStatusProperty(certificateName, version, lastRefreshed string) map[string]string { + certProperty := map[string]string{} + certProperty[types.CertificateName] = certificateName + certProperty[types.CertificateVersion] = version + certProperty[types.CertificateLastRefreshed] = lastRefreshed + return certProperty +} + +// formatKeyVaultCertificate formats the fields in KeyVaultCertificate +func formatKeyVaultCertificate(object *types.KeyVaultCertificate) { + if object == nil { + return + } + objectPtr := reflect.ValueOf(object) + objectValue := objectPtr.Elem() + + for i := 0; i < objectValue.NumField(); i++ { + field := objectValue.Field(i) + if field.Type() != reflect.TypeOf("") { + continue + } + str := field.Interface().(string) + str = strings.TrimSpace(str) + field.SetString(str) + } +} + +// parseAzureEnvironment returns azure environment by name +func parseAzureEnvironment(cloudName string) (*azure.Environment, error) { + var env azure.Environment + var err error + if cloudName == "" { + env = azure.PublicCloud + } else { + env, err = azure.EnvironmentFromName(cloudName) + } + return &env, err +} + +func initializeKvClient(ctx context.Context, keyVaultEndpoint, tenantID, clientID string) (*kv.BaseClient, error) { + kvClient := kv.New() + kvEndpoint := strings.TrimSuffix(keyVaultEndpoint, "/") + + err := kvClient.AddToUserAgent("ratify") + if err != nil { + return nil, re.ErrorCodeConfigInvalid.NewError(re.CertProvider, 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.CertProvider, providerName, re.AKVLink, err, "failed to get authorizer for keyvault client", re.PrintStackTrace) + } + return &kvClient, nil +} + +// Parse the secret bundle and return an array of certificates +// 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.CertProvider, 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) + + // This aligns with notation akv implementation + // 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.CertProvider, 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{} + certsStatus := []map[string]string{} + lastRefreshed := time.Now().Format(time.RFC3339) + + data := []byte(*secretBundle.Value) + + if *secretBundle.ContentType == PKCS12ContentType { + p12, err := base64.StdEncoding.DecodeString(*secretBundle.Value) + if err != nil { + return nil, nil, re.ErrorCodeCertInvalid.NewError(re.CertProvider, providerName, re.EmptyLink, err, fmt.Sprintf("azure keyvault certificate 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.CertProvider, providerName, re.EmptyLink, err, fmt.Sprintf("azure keyvault certificate provider: failed to convert PKCS12 Value to PEM. Certificate %s, version %s", certName, version), re.HideStackTrace) + } + + var pemData []byte + for _, b := range blocks { + pemData = append(pemData, pem.EncodeToMemory(b)...) + } + data = pemData + } + + block, rest := pem.Decode(data) + + for block != nil { + switch block.Type { + case "PRIVATE KEY": + logger.GetLogger(ctx, logOpt).Warnf("azure keyvault certificate provider: certificate %s, version %s private key skipped. Please see doc to learn how to create a new certificate in keyvault with non exportable keys. https://learn.microsoft.com/en-us/azure/key-vault/certificates/how-to-export-certificate?tabs=azure-cli#exportable-and-non-exportable-keys", certName, version) + case "CERTIFICATE": + var pemData []byte + pemData = append(pemData, pem.EncodeToMemory(block)...) + decodedCerts, err := keymanagementprovider.DecodeCertificates(pemData) + if err != nil { + return nil, nil, re.ErrorCodeCertInvalid.NewError(re.CertProvider, providerName, re.EmptyLink, err, fmt.Sprintf("azure keyvault certificate provider: failed to decode Certificate %s, version %s", certName, version), re.HideStackTrace) + } + for _, cert := range decodedCerts { + results = append(results, cert) + certProperty := getCertStatusProperty(certName, version, lastRefreshed) + certsStatus = append(certsStatus, certProperty) + } + default: + logger.GetLogger(ctx, logOpt).Warnf("certificate '%s', version '%s': azure keyvault certificate provider detected unknown block type %s", certName, version, block.Type) + } + + block, rest = pem.Decode(rest) + if block == nil && len(rest) > 0 { + return nil, nil, re.ErrorCodeCertInvalid.NewError(re.CertProvider, providerName, re.EmptyLink, nil, fmt.Sprintf("certificate '%s', version '%s': azure keyvault certificate 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) + return results, certsStatus, nil +} + +// getObjectVersion parses the id to retrieve the version +// of object fetched +// example id format - https://kindkv.vault.azure.net/secrets/actual/1f304204f3624873aab40231241243eb +// TODO (aramase) follow up on https://github.com/Azure/azure-rest-api-specs/issues/10825 to provide +// a native way to obtain the version +func getObjectVersion(id string) string { + splitID := strings.Split(id, "/") + return splitID[len(splitID)-1] +} + +// validate checks vaultURI, tenantID, clientID are set and all certificates have a name +// removes all whitespace from key vault certificate fields +func (s *akvKMProvider) validate() error { + if s.vaultURI == "" { + return re.ErrorCodeConfigInvalid.NewError(re.CertProvider, providerName, re.EmptyLink, nil, "vaultURI is not set", re.HideStackTrace) + } + if s.tenantID == "" { + return re.ErrorCodeConfigInvalid.NewError(re.CertProvider, providerName, re.EmptyLink, nil, "tenantID is not set", re.HideStackTrace) + } + if s.clientID == "" { + return re.ErrorCodeConfigInvalid.NewError(re.CertProvider, providerName, re.EmptyLink, nil, "clientID is not set", re.HideStackTrace) + } + + // all certificates must have a name + for i := range s.certificates { + // remove whitespace from all fields in key vault cert + formatKeyVaultCertificate(&s.certificates[i]) + if s.certificates[i].Name == "" { + return re.ErrorCodeConfigInvalid.NewError(re.CertProvider, providerName, re.EmptyLink, nil, fmt.Sprintf("certificate name is not set for certificate %d", i), re.HideStackTrace) + } + } + + return nil +} diff --git a/pkg/keymanagementprovider/azurekeyvault/provider_test.go b/pkg/keymanagementprovider/azurekeyvault/provider_test.go new file mode 100644 index 000000000..f850bf0b6 --- /dev/null +++ b/pkg/keymanagementprovider/azurekeyvault/provider_test.go @@ -0,0 +1,337 @@ +/* +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 azurekeyvault + +// This class is based on implementation from azure secret store csi provider +// Source: https://github.com/Azure/secrets-store-csi-driver-provider-azure/tree/release-1.4/pkg/provider +import ( + "context" + "reflect" + "strings" + "testing" + "time" + + kv "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/deislabs/ratify/pkg/keymanagementprovider/azurekeyvault/types" + "github.com/deislabs/ratify/pkg/keymanagementprovider/config" + "github.com/stretchr/testify/assert" +) + +// TestParseAzureEnvironment tests the parseAzureEnvironment function +func TestParseAzureEnvironment(t *testing.T) { + envNamesArray := []string{"AZURECHINACLOUD", "AZUREGERMANCLOUD", "AZUREPUBLICCLOUD", "AZUREUSGOVERNMENTCLOUD", ""} + for _, envName := range envNamesArray { + azureEnv, err := parseAzureEnvironment(envName) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if strings.EqualFold(envName, "") && !strings.EqualFold(azureEnv.Name, "AZUREPUBLICCLOUD") { + t.Fatalf("string doesn't match, expected AZUREPUBLICCLOUD, got %s", azureEnv.Name) + } else if !strings.EqualFold(envName, "") && !strings.EqualFold(envName, azureEnv.Name) { + t.Fatalf("string doesn't match, expected %s, got %s", envName, azureEnv.Name) + } + } + + wrongEnvName := "AZUREWRONGCLOUD" + _, err := parseAzureEnvironment(wrongEnvName) + if err == nil { + t.Fatalf("expected error for wrong azure environment name") + } +} + +// TestFormatKeyVaultCertificate tests the formatKeyVaultCertificate function +func TestFormatKeyVaultCertificate(t *testing.T) { + cases := []struct { + desc string + keyVaultObject types.KeyVaultCertificate + expectedKeyVaultObject types.KeyVaultCertificate + }{ + { + desc: "leading and trailing whitespace trimmed from all fields", + keyVaultObject: types.KeyVaultCertificate{ + Name: "cert1 ", + Version: "", + }, + expectedKeyVaultObject: types.KeyVaultCertificate{ + Name: "cert1", + Version: "", + }, + }, + { + desc: "no data loss for already sanitized object", + keyVaultObject: types.KeyVaultCertificate{ + Name: "cert1", + Version: "version1", + }, + expectedKeyVaultObject: types.KeyVaultCertificate{ + Name: "cert1", + Version: "version1", + }, + }, + } + + for i, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + formatKeyVaultCertificate(&cases[i].keyVaultObject) + if !reflect.DeepEqual(cases[i].keyVaultObject, tc.expectedKeyVaultObject) { + t.Fatalf("expected: %+v, but got: %+v", tc.expectedKeyVaultObject, cases[i].keyVaultObject) + } + }) + } +} + +func SkipTestInitializeKVClient(t *testing.T) { + testEnvs := []azure.Environment{ + azure.PublicCloud, + azure.GermanCloud, + azure.ChinaCloud, + azure.USGovernmentCloud, + } + + for i := range testEnvs { + kvBaseClient, err := initializeKvClient(context.TODO(), testEnvs[i].KeyVaultEndpoint, "", "") + assert.NoError(t, err) + assert.NotNil(t, kvBaseClient) + assert.NotNil(t, kvBaseClient.Authorizer) + assert.Contains(t, kvBaseClient.UserAgent, "ratify") + } +} + +// TestCreate tests the Create function +func TestCreate(t *testing.T) { + factory := &akvKMProviderFactory{} + testCases := []struct { + name string + config config.KeyManagementProviderConfig + expectErr bool + }{ + { + name: "valid config", + config: config.KeyManagementProviderConfig{ + "inline": "azurekeyvault", + "vaultURI": "https://testkv.vault.azure.net/", + "tenantID": "tid", + "clientID": "clientid", + "certificates": []map[string]interface{}{ + { + "name": "cert1", + }, + }, + }, + expectErr: false, + }, + { + name: "keyvault uri not provided", + config: config.KeyManagementProviderConfig{}, + expectErr: true, + }, + { + name: "tenantID not provided", + config: config.KeyManagementProviderConfig{ + "vaultUri": "https://testkv.vault.azure.net/", + }, + expectErr: true, + }, + { + name: "clientID not provided", + config: config.KeyManagementProviderConfig{ + "vaultUri": "https://testkv.vault.azure.net/", + "tenantID": "tid", + }, + expectErr: true, + }, + { + name: "invalid cloud name", + config: config.KeyManagementProviderConfig{ + "vaultUri": "https://testkv.vault.azure.net/", + "tenantID": "tid", + "cloudName": "AzureCloud", + }, + expectErr: true, + }, + { + name: "certificates array not set", + config: config.KeyManagementProviderConfig{ + "vaultUri": "https://testkv.vault.azure.net/", + "tenantID": "tid", + "useVMManagedIdentity": "true", + }, + expectErr: true, + }, + { + name: "certificates empty", + config: config.KeyManagementProviderConfig{ + "vaultUri": "https://testkv.vault.azure.net/", + "tenantID": "tid", + "useVMManagedIdentity": "true", + "certificates": []map[string]interface{}{}, + }, + expectErr: true, + }, + { + name: "invalid certificate name", + config: config.KeyManagementProviderConfig{ + "vaultUri": "https://testkv.vault.azure.net/", + "tenantID": "tid", + "clientID": "clientid", + "certificates": []map[string]interface{}{ + { + "name": "", + "version": "version1", + }, + }, + }, + expectErr: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := factory.Create("v1", tc.config, "") + if tc.expectErr != (err != nil) { + t.Fatalf("error = %v, expectErr = %v", err, tc.expectErr) + } + }) + } +} + +// TestGetCertificates tests the GetCertificates function +func TestGetCertificates(t *testing.T) { + factory := &akvKMProviderFactory{} + config := config.KeyManagementProviderConfig{ + "vaultUri": "https://testkv.vault.azure.net/", + "tenantID": "tid", + "clientID": "clientid", + "certificates": []map[string]interface{}{ + { + "name": "cert1", + "version": "", + }, + }, + } + + provider, err := factory.Create("v1", config, "") + if err != nil { + t.Fatalf("expected no err but got error = %v", err) + } + + certs, certStatus, err := provider.GetCertificates(context.Background()) + assert.NotNil(t, err) + assert.Nil(t, certs) + assert.Nil(t, certStatus) +} + +// TestGetCertStatusMap tests the getCertStatusMap function +func TestGetCertStatusMap(t *testing.T) { + certsStatus := []map[string]string{} + certsStatus = append(certsStatus, map[string]string{ + "CertName": "Cert1", + "CertVersion": "VersionABC", + }) + certsStatus = append(certsStatus, map[string]string{ + "CertName": "Cert2", + "CertVersion": "VersionEDF", + }) + + actual := getCertStatusMap(certsStatus) + assert.NotNil(t, actual[types.CertificatesStatus]) +} + +// TestGetObjectVersion tests the getObjectVersion function +func TestGetObjectVersion(t *testing.T) { + id := "https://kindkv.vault.azure.net/secrets/cert1/c55925c29c6743dcb9bb4bf091be03b0" + expectedVersion := "c55925c29c6743dcb9bb4bf091be03b0" + actual := getObjectVersion(id) + assert.Equal(t, expectedVersion, actual) +} + +// TestGetCertStatus tests the getCertStatusProperty function +func TestGetCertStatusProperty(t *testing.T) { + timeNow := time.Now().String() + certName := "certName" + certVersion := "versionABC" + + status := getCertStatusProperty(certName, certVersion, timeNow) + assert.Equal(t, certName, status[types.CertificateName]) + assert.Equal(t, timeNow, status[types.CertificateLastRefreshed]) + assert.Equal(t, certVersion, status[types.CertificateVersion]) +} + +// TestGetCertsFromSecretBundle tests the getCertsFromSecretBundle function +func TestGetCertsFromSecretBundle(t *testing.T) { + cases := []struct { + desc string + value string + contentType string + id string + expectedErr bool + }{ + { + desc: "Pem Content Type", + value: "-----BEGIN CERTIFICATE-----\nMIIC8TCCAdmgAwIBAgIUaNrwbhs/I1ecqUYdzD2xuAVNdmowDQYJKoZIhvcNAQEL\nBQAwKjEPMA0GA1UECgwGUmF0aWZ5MRcwFQYDVQQDDA5SYXRpZnkgUm9vdCBDQTAe\nFw0yMzA2MjEwMTIyMzdaFw0yNDA2MjAwMTIyMzdaMBkxFzAVBgNVBAMMDnJhdGlm\neS5kZWZhdWx0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtskG1BUt\n4Fw2lbm53KbwZb1hnLmWdwRotZyznhhk/yrUDcq3uF6klwpk/E2IKfUKIo6doHSk\nXaEZXR68UtXygvA4wdg7xZ6kKpXy0gu+RxGE6CGtDHTyDDzITu+NBjo21ZSsyGpQ\nJeIKftUCHdwdygKf0CdJx8A29GBRpHGCmJadmt7tTzOnYjmbuPVLeqJo/Ex9qXcG\nZbxoxnxr5NCocFeKx+EbLo+k/KjdFB2PKnhgzxAaMMMP6eXPr8l5AlzkC83EmPvN\ntveuaBbamdlFkD+53TZeZlxt3GIdq93Iw/UpbQ/pvhbrztMT+UVEkm15sShfX8Xn\nL2st5A4n0V+66QIDAQABoyAwHjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIH\ngDANBgkqhkiG9w0BAQsFAAOCAQEAGpOqozyfDSBjoTepsRroxxcZ4sq65gw45Bme\nm36BS6FG0WHIg3cMy6KIIBefTDSKrPkKNTtuF25AeGn9jM+26cnfDM78ZH0+Lnn7\n7hs0MA64WMPQaWs9/+89aM9NADV9vp2zdG4xMi6B7DruvKWyhJaNoRqK/qP6LdSQ\nw8M+21sAHvXgrRkQtJlVOzVhgwt36NOb1hzRlQiZB+nhv2Wbw7fbtAaADk3JAumf\nvM+YdPS1KfAFaYefm4yFd+9/C0KOkHico3LTbELO5hG0Mo/EYvtjM+Fljb42EweF\n3nAx1GSPe5Tn8p3h6RyJW5HIKozEKyfDuLS0ccB/nqT3oNjcTw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDRTCCAi2gAwIBAgIUcC33VfaMhOnsl7avNTRVQozoVtUwDQYJKoZIhvcNAQEL\nBQAwKjEPMA0GA1UECgwGUmF0aWZ5MRcwFQYDVQQDDA5SYXRpZnkgUm9vdCBDQTAe\nFw0yMzA2MjEwMTIyMzZaFw0yMzA2MjIwMTIyMzZaMCoxDzANBgNVBAoMBlJhdGlm\neTEXMBUGA1UEAwwOUmF0aWZ5IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB\nDwAwggEKAoIBAQDDFhDnyPrVDZaeRu6Tbg1a/iTwus+IuX+h8aKhKS1yHz4EF/Lz\nxCy7lNSQ9srGMMVumWuNom/ydIphff6PejZM1jFKPU6OQR/0JX5epcVIjbKa562T\nDguUxJ+h5V3EIyM4RqOWQ2g/xZo86x5TzyNJXiVdHHRvmDvUNwPpMeDjr/EHVAni\n5YQObxkJRiiZ7XOa5zz3YztVm8sSZAwPWroY1HIfvtP+KHpiNDIKSymmuJkH4SEr\nJn++iqN8na18a9DFBPTTrLPe3CxATGrMfosCMZ6LP3iFLLc/FaSpwcnugWdewsUK\nYs+sUY7jFWR7x7/1nyFWyRrQviM4f4TY+K7NAgMBAAGjYzBhMB0GA1UdDgQWBBQH\nYePW7QPP2p1utr3r6gqzEkKs+DAfBgNVHSMEGDAWgBQHYePW7QPP2p1utr3r6gqz\nEkKs+DAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwICBDANBgkqhkiG9w0B\nAQsFAAOCAQEAjKp4vx3bFaKVhAbQeTsDjWJgmXLK2vLgt74MiUwSF6t0wehlfszE\nIcJagGJsvs5wKFf91bnwiqwPjmpse/thPNBAxh1uEoh81tOklv0BN790vsVpq3t+\ncnUvWPiCZdRlAiGGFtRmKk3Keq4sM6UdiUki9s+wnxypHVb4wIpVxu5R271Lnp5I\n+rb2EQ48iblt4XZPczf/5QJdTgbItjBNbuO8WVPOqUIhCiFuAQziLtNUq3p81dHO\nQ2BPgmaitCpIUYHVYighLauBGCH8xOFzj4a4KbOxKdxyJTd0La/vRCKaUtJX67Lc\nfQYVR9HXQZ0YlmwPcmIG5v7wBfcW34NUvA==\n-----END CERTIFICATE-----\n", + contentType: "application/x-pem-file", + id: "https://notarycerts.vault.azure.net/secrets/testCert6212/431ad135165741dcb95a46cf3e6686fb", + expectedErr: false, + }, + { + desc: "PKCS12 Content Type", + value: "MIIKwAIBAzCCCnwGCSqGSIb3DQEHAaCCCm0EggppMIIKZTCCBhYGCSqGSIb3DQEHAaCCBgcEggYDMIIF/zCCBfsGCyqGSIb3DQEMCgECoIIE/jCCBPowHAYKKoZIhvcNAQwBAzAOBAhT2weR+ffbdgICB9AEggTY/fKh5zG3I4/5Xz2t8F0+FR8jyPUt98wZbGChS0e2u6ksaNm/GUT5oCmizPnTCLzGmi01nD6fZDsN6GuW3b70q8lkexACQyvkVwhdBhEVloOFpShBeWk+bycRMFO6F4aUJDgxzEzo9PaWK4xAq4V+g9pUo8opEzn73pxT664rEsvhrCVxBbWamVLJyQwQ6jkpcWDRKSNy46Pd/G4nqlE/Urf/N3VnmTDqqA8jHcACggPzmo3YfssiDabFgxztfHcQFZiTsCv6RcvmQ3e0yzGukQ7TuwnXmuiXYo+rAynK8aIrcgD4Csx8o4KKXyDjZhbODLdzQ701+B1MK8W269vwrtX2ukufHW1M55fxsLfqxbFYpblI3pj7oG9KYNlUG3Flc7GKgyQPETKxFxXsi9ZIUYZbWeMpXOG5v6Q/0YC9jDvWChlWqF+38UIQeFY/0aEFK9W2uYkVUvT4X9E8QrpuXL+5X1q1d5OKx1dWsLIAfFg2o4ZK1HpFrmRh4ptBElcrd623AcDPA/XSUcKQOdcJW8bnjmQt/+tHmF2a7QFYaLT3gH+V88sfG94aO7ArESaXFrWRw18FwzJVUprGE5kVfNpQcmJ4ls8gg/3c1T48vvSJYpeHcl9ShbfKPQj7KI9mn8sxeg8GLz3wM7fWN9/wK1/Z+NLLk0s2BtkM42acUh+2p2bLJwgKoA7rwv7pOytpi2oVUp+LSm3nyOnhYY/ZiO1yy3NXZ8qNzrzrns+RBp2/UM3jm5Cx+G1FLjxsO+twFUATS+numH93MvBF+YFlVcKxs082s7bkDuUyqAlZstPjlR8/dGobqAXKG8Fq3QLYXP95C4PzMzq61R7AHLi7Ojzl6hCK3kBD0aLmDy7D/p4tOkbhAJylyfX4lSA0zGTnobHVcNDzOhDWY3L+VzYuKQVPyqPKRwPYpfc/I97SUqtpz5Fx8D3tR6lHZ0BG2QDqPF6Rlx7S+oJlHwkfFzhsbYpi72zT7IV1/LV56d1/TOFVvqzX440j3zTh3upi+jQoIMVGLyu8ZtQw12pz8EdBenbiS3rkGHJLu1y0m0UiYzyowQrD4SogrsmSOR3x+pmGCj8QTKscEbmypTqMFXtIJqPt+mlS/B0x5ezeEC9NctYo21S5spmAV+X9HX2KN29kdRaBg+2AhMXWRklRt9DXZj2yd82RVsm9eL/dVkx6LvMksSqHHVy9/G2lWOIJy4d+i5hQ1QCeckmfot/udcR8vOwaJxc+gH8UlZpiNhix+xRi3rdqxJ26pEX9oYHjSTb8gZL3kbjHHtd0KyN1CTHhfSP/0d61ttYWhMp8umi1rV9pSV5rbyqbcKK0Q4NBUwAD7ZIOO7euh7m42r1/fjjhlxsmgO6KLXew5uIC/Di7I34rTBQLPfApg5PSgGGUxs2Vv6pg3Y8gqFajxt+b6uIodZo5LUWqhJxwFPgGc/N1aKe+hz+nEG7pD1AxX4OVMcc2r1y1TlQc8m06IjBSGhLXnp+JoL1UurEvQolR+xG+bs9YKgmzDgbxx1wajxfBsCDpYxhPO2VWMcV1J3MOzUcAAZjoV6AQq1V2+ggY5Cv33Khszqyk6jPjHvsQf0lJqhsByh3/wGll3DnOLzqy4o6OV/hJ8Jhv4mzhZRyEXbDqpZYQavt8VCB78zGB6TATBgkqhkiG9w0BCRUxBgQEAQAAADBXBgkqhkiG9w0BCRQxSh5IAGUAZgAyADQAZABhAGUANAAtAGQAYwBlADQALQA0AGIAMgBjAC0AOABjADEAMgAtAGYAYgBmAGIANAAzADAAZAA4ADIANwAwMHkGCSsGAQQBgjcRATFsHmoATQBpAGMAcgBvAHMAbwBmAHQAIABFAG4AaABhAG4AYwBlAGQAIABSAFMAQQAgAGEAbgBkACAAQQBFAFMAIABDAHIAeQBwAHQAbwBnAHIAYQBwAGgAaQBjACAAUAByAG8AdgBpAGQAZQByMIIERwYJKoZIhvcNAQcGoIIEODCCBDQCAQAwggQtBgkqhkiG9w0BBwEwHAYKKoZIhvcNAQwBAzAOBAimXLppRwdpdQICB9CAggQAv5+xRbONQxXaSgWoKOGeN/8CX3tzP0c0Mr4bC420v/IXZuUpaUplt4IBHRazdDRtMfcfb1pQig32j6aYnftUO7J62qwea7UT2t3+JYLye/lJ/EFeF++yqzXge5QQaK3s1E2YgSuSWdTNk4VaPZghA/7ar5UGluWac/112Uhdfn65ime2ysJvd5BHzZFFNy5TqrVN/POzGYM+NdhYtFV9Uy/v2/6zvr9Un4Ns6KhwSHyG4VL3dM2f9FFvW4sjErkWnkxeRLSGdzVPoWF8vO15V0/C6HIV6ug7WPoRODgnTdmWPDctyY+rjy//0jhA45AhIb2TIjdLjNi4RtP4uEGZ5WE8A61QZbJlp/nYKFggpEOqfQMOCYDEo5RhmZ3tEN9m/gLlFKxVswb/VjxHL0fHSRCA+2fmC/RuXw+ZspUFJEW7+SPM0GSq6trz6zYtCD8iVR+OgMY3CdGS5TRudArQLkcwL9vJm9IuAHW5IgvC25zGzM0BdPYylyws7XfMBmClXxBkWAd6WhjN+F9YR62Shk77Jj4rX/7460UzdWW4spZZnSPF/gAzHqUzYkTNJFqYCT3BDbYextG2cLaXB2H2CLwHlQIPGGhMBh/GpqYKCr726vBKlODhMAaZBrV6KzwXDVw75c04BWqRTEQ3xlvXsqP2CmzkHoF+WiOrl7eNs2RJhD/Ul7DN5GUVpanjBvPSxB04d/AXX3Rn4hrZWxtxjLVpQpZedjXA03kmjj/8tIQ3Fs0rAgqT+CZxpvplrdD3uWxWTH8xqAJHTXoNyFhnwv8oBkmkqw6AxoaHs+yFwS8vw2tO1aj1ky6HYxKQkt3U/rTiHSCUUPegvmBsk+obbuRG5r0gMasfXyU41sBq4kFjP+YcpqyyyFI1wKRY2Sgio8Rf6pd6NjcwE7IrTJywUVaLdaKOHR+AaY50I+UB1DApflYv32cN07XoiazZYu3uARD4PQEatWUps96rvJ6i2vhC0q2+qru+kpM89OEKO1uKPCBMy3m3g/cWofg/yGk62dbNWQu4WnOo0G+Cdg5UBwRRpg1dL4/JNur2F7LzuG4eQ2HAQhuZkaKcuhEFbGdCaqEWnM7uPdpEKmh5shKUtaHnq2sRQfAj/oprRhOv+XiFV79bjYUKSvUJ8ZE1W463mc53ygNKp12D1D2u/WSwrtc1DHvnNS3Sgu2X2SOIcQplssTGRpOpjN+guUOSQCeXmpo9gqCrkG1dpDnMDNb5Km/+kurqEH6ebG1iZ+xUItX7EXAymCMWpNgvY2Fuw9cK0xUaYS1SyNStSJgd3udB3o/mxuFd0sP28ojmloIBCroC5Cm0zgCg3+l/TeaCmLL/6VwI6yKr2bBG03gq4IYX+zA7MB8wBwYFKw4DAhoEFHBrDFC1fmAxcvGwsyS/Tl46Ox2eBBTWbe5YACqUwXIPT/K3bixCBGNytQICB9A=", + contentType: "application/x-pkcs12", + id: "https://notarycerts.vault.azure.net/secrets/testCert6212/431ad135165741dcb95a46cf3e6686fb", + expectedErr: false, + }, + { + desc: "Invalid PKCS12 Content", + value: "IKwAIBAzCCCnwGCSqGSIb3DQEHAaCCCm0EggppMIIKZTCCBhYGCSqGSIb3DQEHAaCCBgcEggYDMIIF/zCCBfsGCyqGSIb3DQEMCgECoIIE/jCCBPowHAYKKoZIhvcNAQwBAzAOBAhT2weR+ffbdgICB9AEggTY/fKh5zG3I4/5Xz2t8F0+FR8jyPUt98wZbGChS0e2u6ksaNm/GUT5oCmizPnTCLzGmi01nD6fZDsN6GuW3b70q8lkexACQyvkVwhdBhEVloOFpShBeWk+bycRMFO6F4aUJDgxzEzo9PaWK4xAq4V+g9pUo8opEzn73pxT664rEsvhrCVxBbWamVLJyQwQ6jkpcWDRKSNy46Pd/G4nqlE/Urf/N3VnmTDqqA8jHcACggPzmo3YfssiDabFgxztfHcQFZiTsCv6RcvmQ3e0yzGukQ7TuwnXmuiXYo+rAynK8aIrcgD4Csx8o4KKXyDjZhbODLdzQ701+B1MK8W269vwrtX2ukufHW1M55fxsLfqxbFYpblI3pj7oG9KYNlUG3Flc7GKgyQPETKxFxXsi9ZIUYZbWeMpXOG5v6Q/0YC9jDvWChlWqF+38UIQeFY/0aEFK9W2uYkVUvT4X9E8QrpuXL+5X1q1d5OKx1dWsLIAfFg2o4ZK1HpFrmRh4ptBElcrd623AcDPA/XSUcKQOdcJW8bnjmQt/+tHmF2a7QFYaLT3gH+V88sfG94aO7ArESaXFrWRw18FwzJVUprGE5kVfNpQcmJ4ls8gg/3c1T48vvSJYpeHcl9ShbfKPQj7KI9mn8sxeg8GLz3wM7fWN9/wK1/Z+NLLk0s2BtkM42acUh+2p2bLJwgKoA7rwv7pOytpi2oVUp+LSm3nyOnhYY/ZiO1yy3NXZ8qNzrzrns+RBp2/UM3jm5Cx+G1FLjxsO+twFUATS+numH93MvBF+YFlVcKxs082s7bkDuUyqAlZstPjlR8/dGobqAXKG8Fq3QLYXP95C4PzMzq61R7AHLi7Ojzl6hCK3kBD0aLmDy7D/p4tOkbhAJylyfX4lSA0zGTnobHVcNDzOhDWY3L+VzYuKQVPyqPKRwPYpfc/I97SUqtpz5Fx8D3tR6lHZ0BG2QDqPF6Rlx7S+oJlHwkfFzhsbYpi72zT7IV1/LV56d1/TOFVvqzX440j3zTh3upi+jQoIMVGLyu8ZtQw12pz8EdBenbiS3rkGHJLu1y0m0UiYzyowQrD4SogrsmSOR3x+pmGCj8QTKscEbmypTqMFXtIJqPt+mlS/B0x5ezeEC9NctYo21S5spmAV+X9HX2KN29kdRaBg+2AhMXWRklRt9DXZj2yd82RVsm9eL/dVkx6LvMksSqHHVy9/G2lWOIJy4d+i5hQ1QCeckmfot/udcR8vOwaJxc+gH8UlZpiNhix+xRi3rdqxJ26pEX9oYHjSTb8gZL3kbjHHtd0KyN1CTHhfSP/0d61ttYWhMp8umi1rV9pSV5rbyqbcKK0Q4NBUwAD7ZIOO7euh7m42r1/fjjhlxsmgO6KLXew5uIC/Di7I34rTBQLPfApg5PSgGGUxs2Vv6pg3Y8gqFajxt+b6uIodZo5LUWqhJxwFPgGc/N1aKe+hz+nEG7pD1AxX4OVMcc2r1y1TlQc8m06IjBSGhLXnp+JoL1UurEvQolR+xG+bs9YKgmzDgbxx1wajxfBsCDpYxhPO2VWMcV1J3MOzUcAAZjoV6AQq1V2+ggY5Cv33Khszqyk6jPjHvsQf0lJqhsByh3/wGll3DnOLzqy4o6OV/hJ8Jhv4mzhZRyEXbDqpZYQavt8VCB78zGB6TATBgkqhkiG9w0BCRUxBgQEAQAAADBXBgkqhkiG9w0BCRQxSh5IAGUAZgAyADQAZABhAGUANAAtAGQAYwBlADQALQA0AGIAMgBjAC0AOABjADEAMgAtAGYAYgBmAGIANAAzADAAZAA4ADIANwAwMHkGCSsGAQQBgjcRATFsHmoATQBpAGMAcgBvAHMAbwBmAHQAIABFAG4AaABhAG4AYwBlAGQAIABSAFMAQQAgAGEAbgBkACAAQQBFAFMAIABDAHIAeQBwAHQAbwBnAHIAYQBwAGgAaQBjACAAUAByAG8AdgBpAGQAZQByMIIERwYJKoZIhvcNAQcGoIIEODCCBDQCAQAwggQtBgkqhkiG9w0BBwEwHAYKKoZIhvcNAQwBAzAOBAimXLppRwdpdQICB9CAggQAv5+xRbONQxXaSgWoKOGeN/8CX3tzP0c0Mr4bC420v/IXZuUpaUplt4IBHRazdDRtMfcfb1pQig32j6aYnftUO7J62qwea7UT2t3+JYLye/lJ/EFeF++yqzXge5QQaK3s1E2YgSuSWdTNk4VaPZghA/7ar5UGluWac/112Uhdfn65ime2ysJvd5BHzZFFNy5TqrVN/POzGYM+NdhYtFV9Uy/v2/6zvr9Un4Ns6KhwSHyG4VL3dM2f9FFvW4sjErkWnkxeRLSGdzVPoWF8vO15V0/C6HIV6ug7WPoRODgnTdmWPDctyY+rjy//0jhA45AhIb2TIjdLjNi4RtP4uEGZ5WE8A61QZbJlp/nYKFggpEOqfQMOCYDEo5RhmZ3tEN9m/gLlFKxVswb/VjxHL0fHSRCA+2fmC/RuXw+ZspUFJEW7+SPM0GSq6trz6zYtCD8iVR+OgMY3CdGS5TRudArQLkcwL9vJm9IuAHW5IgvC25zGzM0BdPYylyws7XfMBmClXxBkWAd6WhjN+F9YR62Shk77Jj4rX/7460UzdWW4spZZnSPF/gAzHqUzYkTNJFqYCT3BDbYextG2cLaXB2H2CLwHlQIPGGhMBh/GpqYKCr726vBKlODhMAaZBrV6KzwXDVw75c04BWqRTEQ3xlvXsqP2CmzkHoF+WiOrl7eNs2RJhD/Ul7DN5GUVpanjBvPSxB04d/AXX3Rn4hrZWxtxjLVpQpZedjXA03kmjj/8tIQ3Fs0rAgqT+CZxpvplrdD3uWxWTH8xqAJHTXoNyFhnwv8oBkmkqw6AxoaHs+yFwS8vw2tO1aj1ky6HYxKQkt3U/rTiHSCUUPegvmBsk+obbuRG5r0gMasfXyU41sBq4kFjP+YcpqyyyFI1wKRY2Sgio8Rf6pd6NjcwE7IrTJywUVaLdaKOHR+AaY50I+UB1DApflYv32cN07XoiazZYu3uARD4PQEatWUps96rvJ6i2vhC0q2+qru+kpM89OEKO1uKPCBMy3m3g/cWofg/yGk62dbNWQu4WnOo0G+Cdg5UBwRRpg1dL4/JNur2F7LzuG4eQ2HAQhuZkaKcuhEFbGdCaqEWnM7uPdpEKmh5shKUtaHnq2sRQfAj/oprRhOv+XiFV79bjYUKSvUJ8ZE1W463mc53ygNKp12D1D2u/WSwrtc1DHvnNS3Sgu2X2SOIcQplssTGRpOpjN+guUOSQCeXmpo9gqCrkG1dpDnMDNb5Km/+kurqEH6ebG1iZ+xUItX7EXAymCMWpNgvY2Fuw9cK0xUaYS1SyNStSJgd3udB3o/mxuFd0sP28ojmloIBCroC5Cm0zgCg3+l/TeaCmLL/6VwI6yKr2bBG03gq4IYX+zA7MB8wBwYFKw4DAhoEFHBrDFC1fmAxcvGwsyS/Tl46Ox2eBBTWbe5YACqUwXIPT/K3bixCBGNytQICB9A=", + contentType: "application/x-pkcs12", + id: "https://notarycerts.vault.azure.net/secrets/testCert6212/431ad135165741dcb95a46cf3e6686fb", + expectedErr: true, + }, + { + desc: "Secret Text File", + value: "text", + contentType: "text", + id: "https://notarycerts.vault.azure.net/secrets/testCert6212/431ad135165741dcb95a46cf3e6686fb", + expectedErr: true, + }, + { + desc: "Test empty", + value: "", + contentType: "", + id: "", + expectedErr: true, + }, + } + + for i, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + testdata := kv.SecretBundle{ + Value: &cases[i].value, + ID: &cases[i].id, + ContentType: &cases[i].contentType, + } + + certs, status, err := getCertsFromSecretBundle(context.Background(), testdata, "certName") + if tc.expectedErr { + assert.NotNil(t, err) + assert.Nil(t, certs) + assert.Nil(t, status) + } else { + assert.Nil(t, err) + } + }) + } +} diff --git a/pkg/keymanagementprovider/azurekeyvault/types/types.go b/pkg/keymanagementprovider/azurekeyvault/types/types.go new file mode 100644 index 000000000..f6caa11ba --- /dev/null +++ b/pkg/keymanagementprovider/azurekeyvault/types/types.go @@ -0,0 +1,36 @@ +/* +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 types + +const ( + // Static string for certificate type + CertificateType = "CERTIFICATE" + // key of the certificate status property + CertificatesStatus = "Certificates" + // Static string for certificate name for the certificate status property + CertificateName = "Name" + // Certificate version string for the certificate status property + CertificateVersion = "Version" + // Last refreshed string for the certificate status property + CertificateLastRefreshed = "LastRefreshed" +) + +// KeyVaultCertificate holds keyvault certificate related config +type KeyVaultCertificate struct { + // the name of the Azure Key Vault certificate + Name string `json:"name" yaml:"name"` + // the version of the Azure Key Vault certificate + Version string `json:"version" yaml:"version"` +} diff --git a/pkg/keymanagementprovider/config/config.go b/pkg/keymanagementprovider/config/config.go new file mode 100644 index 000000000..9bd393961 --- /dev/null +++ b/pkg/keymanagementprovider/config/config.go @@ -0,0 +1,25 @@ +/* +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 config + +// KeyManagementProviderConfig is a map containing name and provider-specific configuration +type KeyManagementProviderConfig map[string]interface{} + +type KeyManagementProvidersConfig struct { + Version string `json:"version,omitempty"` + PluginBinDirs []string `json:"pluginBinDirs,omitempty"` + KeyManagementProviders []KeyManagementProviderConfig `json:"plugins,omitempty"` +} diff --git a/pkg/keymanagementprovider/factory/factory.go b/pkg/keymanagementprovider/factory/factory.go new file mode 100644 index 000000000..150093b54 --- /dev/null +++ b/pkg/keymanagementprovider/factory/factory.go @@ -0,0 +1,65 @@ +/* +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 factory + +import ( + "fmt" + + "github.com/deislabs/ratify/pkg/keymanagementprovider" + "github.com/deislabs/ratify/pkg/keymanagementprovider/config" + "github.com/deislabs/ratify/pkg/keymanagementprovider/types" +) + +// map of key management provider names to key management provider factories +var builtInKeyManagementProviders = make(map[string]KeyManagementProviderFactory) + +// KeyManagementProviderFactory is an interface for creating key management provider providers +type KeyManagementProviderFactory interface { + Create(version string, keyManagementProviderConfig config.KeyManagementProviderConfig, pluginDirectory string) (keymanagementprovider.KeyManagementProvider, error) +} + +// Register registers a key management provider factory by name +func Register(name string, factory KeyManagementProviderFactory) { + if factory == nil { + panic("key management provider factory cannot be nil") + } + _, registered := builtInKeyManagementProviders[name] + if registered { + panic(fmt.Sprintf("key management provider factory named %s already registered", name)) + } + + builtInKeyManagementProviders[name] = factory +} + +// CreateKeyManagementProviderFromConfig creates a key management provider from config +func CreateKeyManagementProviderFromConfig(keyManagementProviderConfig config.KeyManagementProviderConfig, configVersion string, pluginDirectory string) (keymanagementprovider.KeyManagementProvider, error) { + keyManagementProvider, ok := keyManagementProviderConfig[types.Type] + if !ok { + return nil, fmt.Errorf("failed to find key management provider name in the certificate stores config with key %s", types.Type) + } + + keyManagementProviderStr := fmt.Sprintf("%s", keyManagementProvider) + if keyManagementProviderStr == "" { + return nil, fmt.Errorf("key management provider type cannot be empty") + } + + factory, ok := builtInKeyManagementProviders[keyManagementProviderStr] + if !ok { + return nil, fmt.Errorf("key management provider factory with name %s not found", keyManagementProviderStr) + } + + return factory.Create(configVersion, keyManagementProviderConfig, pluginDirectory) +} diff --git a/pkg/keymanagementprovider/factory/factory_test.go b/pkg/keymanagementprovider/factory/factory_test.go new file mode 100644 index 000000000..9a89904ad --- /dev/null +++ b/pkg/keymanagementprovider/factory/factory_test.go @@ -0,0 +1,120 @@ +/* +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 factory + +import ( + "testing" + + "github.com/deislabs/ratify/pkg/keymanagementprovider" + "github.com/deislabs/ratify/pkg/keymanagementprovider/config" + "github.com/deislabs/ratify/pkg/keymanagementprovider/mocks" +) + +type TestKeyManagementProviderFactory struct{} + +func (f TestKeyManagementProviderFactory) Create(_ string, _ config.KeyManagementProviderConfig, _ string) (keymanagementprovider.KeyManagementProvider, error) { + return &mocks.TestKeyManagementProvider{}, nil +} + +// TestRegister tests the Register function +func TestRegister(t *testing.T) { + // test that the key management provider is registered + Register("test-kmprovider", &TestKeyManagementProviderFactory{}) + if _, ok := builtInKeyManagementProviders["test-kmprovider"]; !ok { + t.Fatalf("key management provider not registered") + } + + defer func() { + if r := recover(); r == nil { + t.Fatalf("Register should have panicked") + } + }() + // test that Register panics on duplicate registration + Register("test-kmprovider", &TestKeyManagementProviderFactory{}) +} + +// TestRegisterPanicsOnNil tests that Register panics on nil factory passed in +func TestRegisterPanicsOnNil(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatalf("Register should have panicked") + } + }() + // test that Register panics on nil factory + Register("test-kmprovider", nil) +} + +// TestCreateKeyManagementProvidersFromConfig_BuiltInKeyManagementProviders_ReturnsExpected checks the correct registered key management provider is invoked based on config +func TestCreateKeyManagementProvidersFromConfig_BuiltInKeyManagementProvider_ReturnsExpected(t *testing.T) { + builtInKeyManagementProviders = map[string]KeyManagementProviderFactory{ + "test-kmprovider": TestKeyManagementProviderFactory{}, + } + + config := config.KeyManagementProviderConfig{ + "type": "test-kmprovider", + } + + _, err := CreateKeyManagementProviderFromConfig(config, "", "") + if err != nil { + t.Fatalf("create key management provider should not have failed: %v", err) + } +} + +// TestCreateKeyManagementProvidersFromConfig_NonexistentKeyManagementProviders_ReturnsExpected checks the key management provider creation fails if key management provider specified does not exist +func TestCreateKeyManagementProvidersFromConfig_NonexistentKeyManagementProviders_ReturnsExpected(t *testing.T) { + builtInKeyManagementProviders = map[string]KeyManagementProviderFactory{ + "testkeymanagementprovider": TestKeyManagementProviderFactory{}, + } + + config := config.KeyManagementProviderConfig{ + "type": "test-nonexistent", + } + + _, err := CreateKeyManagementProviderFromConfig(config, "", "") + if err == nil { + t.Fatal("create key management provider should have failed") + } +} + +// TestCreateKeyManagementProvidersFromConfig_MissingType_ReturnsExpected checks the key management provider creation fails if type field is missing in config +func TestCreateKeyManagementProvidersFromConfig_MissingType_ReturnsExpected(t *testing.T) { + builtInKeyManagementProviders = map[string]KeyManagementProviderFactory{ + "testkeymanagementprovider": TestKeyManagementProviderFactory{}, + } + + config := config.KeyManagementProviderConfig{ + "nonexistent": "test-nonexistent", + } + + _, err := CreateKeyManagementProviderFromConfig(config, "", "") + if err == nil { + t.Fatal("create key management provider should have failed") + } +} + +// TestCreateKeyManagementProvidersFromConfig_EmptyType_ReturnsExpected checks the key management provider creation fails if type field is empty in config +func TestCreateKeyManagementProvidersFromConfig_EmptyType_ReturnsExpected(t *testing.T) { + builtInKeyManagementProviders = map[string]KeyManagementProviderFactory{ + "testkeymanagementprovider": TestKeyManagementProviderFactory{}, + } + + config := config.KeyManagementProviderConfig{} + + _, err := CreateKeyManagementProviderFromConfig(config, "", "") + if err == nil { + t.Fatal("create key management provider should have failed") + } +} diff --git a/pkg/keymanagementprovider/inline/provider.go b/pkg/keymanagementprovider/inline/provider.go new file mode 100644 index 000000000..4a480d603 --- /dev/null +++ b/pkg/keymanagementprovider/inline/provider.go @@ -0,0 +1,95 @@ +/* +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 inline + +import ( + "context" + "crypto/x509" + "encoding/json" + + "github.com/deislabs/ratify/errors" + "github.com/deislabs/ratify/pkg/keymanagementprovider" + "github.com/deislabs/ratify/pkg/keymanagementprovider/config" + "github.com/deislabs/ratify/pkg/keymanagementprovider/factory" +) + +const ( + // ValueParameter is the name of the parameter that contains the certificate (chain) as a string in PEM format + ValueParameter = "value" + providerName string = "inline" + certificateContentType string = "certificate" + certificatesMapKey string = "certs" +) + +//nolint:revive +type InlineKMProviderConfig struct { + Type string `json:"type"` + ContentType string `json:"contentType"` + Value string `json:"value"` +} + +type inlineKMProvider struct { + certs map[keymanagementprovider.KMPMapKey][]*x509.Certificate + contentType string +} +type inlineKMProviderFactory struct{} + +// init calls to register the provider +func init() { + factory.Register(providerName, &inlineKMProviderFactory{}) +} + +// Create creates a new instance of the inline key management provider provider +// checks contentType is set to 'certificate' and value is set to a valid certificate +func (f *inlineKMProviderFactory) Create(_ string, keyManagementProviderConfig config.KeyManagementProviderConfig, _ string) (keymanagementprovider.KeyManagementProvider, error) { + conf := InlineKMProviderConfig{} + + keyManagementProviderConfigBytes, err := json.Marshal(keyManagementProviderConfig) + if err != nil { + return nil, errors.ErrorCodeConfigInvalid.WithError(err).WithComponentType(errors.KeyManagementProvider) + } + + if err := json.Unmarshal(keyManagementProviderConfigBytes, &conf); err != nil { + return nil, errors.ErrorCodeConfigInvalid.NewError(errors.KeyManagementProvider, "", errors.EmptyLink, err, "failed to parse AKV key management provider configuration", errors.HideStackTrace) + } + + if conf.ContentType == "" { + return nil, errors.ErrorCodeConfigInvalid.WithComponentType(errors.KeyManagementProvider).WithDetail("contentType parameter is not set") + } + + // only support certificate content type for now + if conf.ContentType != certificateContentType { + return nil, errors.ErrorCodeConfigInvalid.WithComponentType(errors.KeyManagementProvider).WithDetail("contentType parameter is not set to 'certificate'") + } + + if conf.Value == "" { + return nil, errors.ErrorCodeConfigInvalid.WithComponentType(errors.KeyManagementProvider).WithDetail("value parameter is not set") + } + + certs, err := keymanagementprovider.DecodeCertificates([]byte(conf.Value)) + if err != nil { + return nil, errors.ErrorCodeCertInvalid.WithComponentType(errors.KeyManagementProvider) + } + certMap := map[keymanagementprovider.KMPMapKey][]*x509.Certificate{ + {}: certs, + } + return &inlineKMProvider{certs: certMap, contentType: conf.ContentType}, nil +} + +// GetCertificates returns previously fetched certificates +func (s *inlineKMProvider) GetCertificates(_ context.Context) (map[keymanagementprovider.KMPMapKey][]*x509.Certificate, keymanagementprovider.KeyManagementProviderStatus, error) { + return s.certs, nil, nil +} diff --git a/pkg/keymanagementprovider/inline/provider_test.go b/pkg/keymanagementprovider/inline/provider_test.go new file mode 100644 index 000000000..9b7f93fcf --- /dev/null +++ b/pkg/keymanagementprovider/inline/provider_test.go @@ -0,0 +1,118 @@ +/* +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 inline + +import ( + "context" + "testing" + + "github.com/deislabs/ratify/pkg/keymanagementprovider" + "github.com/deislabs/ratify/pkg/keymanagementprovider/config" + "github.com/stretchr/testify/assert" +) + +// TestCreate tests the Create method +func TestCreate(t *testing.T) { + cases := []struct { + desc string + config config.KeyManagementProviderConfig + expectedErr bool + }{ + { + desc: "contentType not provided", + config: config.KeyManagementProviderConfig{ + "type": "inline", + }, + expectedErr: true, + }, + { + desc: "unsupported contentType", + config: config.KeyManagementProviderConfig{ + "type": "inline", + "contentType": "unsupported", + }, + expectedErr: true, + }, + { + desc: "value not provided", + config: config.KeyManagementProviderConfig{ + "type": "inline", + "contentType": "certificate", + }, + expectedErr: true, + }, + { + desc: "invalid certificate", + config: config.KeyManagementProviderConfig{ + "type": "inline", + "contentType": "certificate", + "value": "-----BEGIN CERTIFICATE-----\nbaddata\n-----END CERTIFICATE-----\n", + }, + expectedErr: true, + }, + } + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + factory := &inlineKMProviderFactory{} + _, err := factory.Create("v1.0", tc.config, "") + if tc.expectedErr != (err != nil) { + t.Fatalf("failed to create provider: %v", err) + } + }) + } +} + +// TestGetCertificates tests the GetCertificates method +func TestGetCertificates(t *testing.T) { + cases := []struct { + desc string + config config.KeyManagementProviderConfig + expectedErr bool + expectedCerts int + }{ + { + desc: "single certificate", + config: config.KeyManagementProviderConfig{ + "type": "inline", + "contentType": "certificate", + "value": "-----BEGIN CERTIFICATE-----\nMIID2jCCAsKgAwIBAgIQXy2VqtlhSkiZKAGhsnkjbDANBgkqhkiG9w0BAQsFADBvMRswGQYDVQQD\nExJyYXRpZnkuZXhhbXBsZS5jb20xDzANBgNVBAsTBk15IE9yZzETMBEGA1UEChMKTXkgQ29tcGFu\neTEQMA4GA1UEBxMHUmVkbW9uZDELMAkGA1UECBMCV0ExCzAJBgNVBAYTAlVTMB4XDTIzMDIwMTIy\nNDUwMFoXDTI0MDIwMTIyNTUwMFowbzEbMBkGA1UEAxMScmF0aWZ5LmV4YW1wbGUuY29tMQ8wDQYD\nVQQLEwZNeSBPcmcxEzARBgNVBAoTCk15IENvbXBhbnkxEDAOBgNVBAcTB1JlZG1vbmQxCzAJBgNV\nBAgTAldBMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL10bM81\npPAyuraORABsOGS8M76Bi7Guwa3JlM1g2D8CuzSfSTaaT6apy9GsccxUvXd5cmiP1ffna5z+EFmc\nizFQh2aq9kWKWXDvKFXzpQuhyqD1HeVlRlF+V0AfZPvGt3VwUUjNycoUU44ctCWmcUQP/KShZev3\n6SOsJ9q7KLjxxQLsUc4mg55eZUThu8mGB8jugtjsnLUYvIWfHhyjVpGrGVrdkDMoMn+u33scOmrt\nsBljvq9WVo4T/VrTDuiOYlAJFMUae2Ptvo0go8XTN3OjLblKeiK4C+jMn9Dk33oGIT9pmX0vrDJV\nX56w/2SejC1AxCPchHaMuhlwMpftBGkCAwEAAaNyMHAwDgYDVR0PAQH/BAQDAgeAMAkGA1UdEwQC\nMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHwYDVR0jBBgwFoAU0eaKkZj+MS9jCp9Dg1zdv3v/aKww\nHQYDVR0OBBYEFNHmipGY/jEvYwqfQ4Nc3b97/2isMA0GCSqGSIb3DQEBCwUAA4IBAQBNDcmSBizF\nmpJlD8EgNcUCy5tz7W3+AAhEbA3vsHP4D/UyV3UgcESx+L+Nye5uDYtTVm3lQejs3erN2BjW+ds+\nXFnpU/pVimd0aYv6mJfOieRILBF4XFomjhrJOLI55oVwLN/AgX6kuC3CJY2NMyJKlTao9oZgpHhs\nLlxB/r0n9JnUoN0Gq93oc1+OLFjPI7gNuPXYOP1N46oKgEmAEmNkP1etFrEjFRgsdIFHksrmlOlD\nIed9RcQ087VLjmuymLgqMTFX34Q3j7XgN2ENwBSnkHotE9CcuGRW+NuiOeJalL8DBmFXXWwHTKLQ\nPp5g6m1yZXylLJaFLKz7tdMmO355\n-----END CERTIFICATE-----\n", + }, + expectedCerts: 1, + }, + { + desc: "certificate chain", + config: config.KeyManagementProviderConfig{ + "type": "inline", + "contentType": "certificate", + "value": "-----BEGIN CERTIFICATE-----\nMIIEeDCCAmCgAwIBAgIUbztxbi/gSPMZGN53oZQZW1h/lbAwDQYJKoZIhvcNAQEL\nBQAwazELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdSZWRtb25k\nMRMwEQYDVQQKDApNeSBDb21wYW55MQ8wDQYDVQQLDAZNeSBPcmcxFzAVBgNVBAMM\nDmNhLmV4YW1wbGUuY29tMB4XDTIzMDIwMTIyNTMxMFoXDTI0MDIwMTIyNTMxMFow\nbTEZMBcGA1UEAxMQbGVhZi5leGFtcGxlLmNvbTEPMA0GA1UECxMGTXkgT3JnMRMw\nEQYDVQQKEwpNeSBDb21wYW55MRAwDgYDVQQHEwdSZWRtb25kMQswCQYDVQQIEwJX\nQTELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCs\nMixf+WT34edYXs2c80zOg4Z/cxOVHU05gywjuISeaP+KS+Joc3emgbub1t5dPclk\nieIwrj3Olk3tvkrPiarOJIcrNR2zfBmQAufR4AUjoc4n1GQSp/voGgw1Hvh0wTkO\nYjhzLomrF242Ond8WTVO3Vq6/tfApfZMFM59eK9LMBkuvwTV4NeLnEnPvpLAoAvV\n9ZvCu7FuQ849R93Aoag2bZc3Tc3UCbahoJs9rTE/rnAqOhJWMGv2J1Y2Wu2eIvkD\n2uCmcVlY+7owG3TwLHTuIOBFl/5MXMvfR+B7yp1OkG23rTwwuSEBlMhYRzJFvssv\n8FX0sea7zhIg5dtoRjIlAgMBAAGjEjAQMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG\n9w0BAQsFAAOCAgEANRUu+9aBAuRf3OsdUWJflMAvuyzREp3skWSOUs4dw0MhcB6x\nb7BSyNdrBgPImLBpqYzNU6IT2eIlLXrYnKLehvPyZQx7LHvIeompA09aKMFFesqi\ngVoW5GRtp3qL3oNuuZJ80r/uKlB6Cj51TWqUbLcctBGHX7TWxFeWmFRnN0Bki00U\nJW/ElaTsr4GB+ltgZM+5USUqSNQqTa8t3d+vH6oVikyV1oYunM41xAfiRZtID04z\no15sLSkWTjavfmZ3+NjllipXFY2tnLqymCcObgdKtJHmTMFSDRngDjY+3+RVj4EY\npNaCCCepvtmXz1C5f06tlgY4ofaautJuAL7K93p/Q9ZcsIhmYWkCUZ0dkWq+eMdT\n9/lB9rQHbrDTaRxEQNIUezFMQEBxR9eC5JQfpw98LobAgA3r4vizQjQPsN0UZ54h\ncAHiyoo1VeckkotXaToRsoLjixPO9Fmss4H3urJTLpcU0drbVoG3emNh4K289vgR\nrjV11TenqvvR3+jJ2AX2ewSsF25m0afheZbrq2ZtyITPAbOqwMwTbTOvJT3HUztt\nhUP3qwsKNPR/hF3FSqZewiYOSqJi5Dk28Vd6mUEQzZa/Ma9RpBR+BAmfgH3Z9gX5\n0TqmAVQn1P8yh+bhEjiNa20bTJ+y5vQ9OrA7fiQ+6vpZCio4NFiEbYK4UBI=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFxzCCA6+gAwIBAgIUY1fnGcYJFIGNk8fevKdGtOuZ5vcwDQYJKoZIhvcNAQEL\nBQAwazELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdSZWRtb25k\nMRMwEQYDVQQKDApNeSBDb21wYW55MQ8wDQYDVQQLDAZNeSBPcmcxFzAVBgNVBAMM\nDmNhLmV4YW1wbGUuY29tMB4XDTIzMDIwMTIyNTIwN1oXDTMzMDEyOTIyNTIwN1ow\nazELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdSZWRtb25kMRMw\nEQYDVQQKDApNeSBDb21wYW55MQ8wDQYDVQQLDAZNeSBPcmcxFzAVBgNVBAMMDmNh\nLmV4YW1wbGUuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtQXd\n+qySVVMHx7iGz9xRdpDKb+zATK3asMFnMXWBn0MCkcOMJvhjajA8yrCpfMudjiW0\nReQ6xcjEfnJqkzwxrK7tPE9cQ6JzQGCxsmscRzKf1NoJL2ske5xBteKuJhSfZ9la\ncIZn/EU2F6eMAl9U4Y+ncIIrl4UoB5H/AJJj62WMl5QAvzXXwCBwlLHQe4/T3Axu\n9xmD3HTC7iQExOUFLdJx86fK3ym0futi1RgOUgD+OrnyDEIkD8mGxffPYPgszS71\nUuJX+NTsLZ/JW3ER/PMAPnBsMsMTTxEIGnrp1CXf2RnwQnDHVsxZFMgkLTS6dksT\nTGevnulTNtVvSKsZ7MpzE01j7zDie4V4dQJzBJMktbeVq9KRoPIEX0WcKpg8bGS8\nd5p5pr+Lu/NOv6ur+av8M649qCPwJAv5i2P6ggT4YMNtY0wMD2kjcHJ9/l2gYpZj\n3DG0Hy3Xo8uKUmTSC7iGhLsSjleNhJkKyh3RCsuMKB9juE4qeXPoaoPWBIuarbkq\neVVZJu2PlgN8UcxM/ym+9GNJIfNJ18WGWm+5P3IDvfBbJ39yvzZlG2czju4BUzYn\nlPOHA/Z18TxZhlPrPVnyKSVbeg7sW17yMUI2LCeaFIOYdIFvM09RaCyLIGrQwhpe\nkLh56xXk702oNHaLxyh/v5kz8EyxnpXIlDntis0CAwEAAaNjMGEwHQYDVR0OBBYE\nFCESfoahHMx7GyBdTBARen7mc37nMB8GA1UdIwQYMBaAFCESfoahHMx7GyBdTBAR\nen7mc37nMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3\nDQEBCwUAA4ICAQBst1MbRJd3FLSY36qaNuDhGncvcXIcYqP2A/SXVhjuAhVOTsrE\nejDYBSkjfxCgoA3LZnQLTcbcPwRL5dTBEvqBqzbyArOjc+G3kbjOeuZYv09M8kUU\nYG1bVnxXV17GMk7zcBUnr1iwnp4E0pzB9gTv+Jv1oV+EtAHe5QOTOmW9mm2SUXbH\nmIya+KlzkIgVJJ8kiGOyXsr8i4wpyDXZf720tqTzPQFTf6rUXo9PhYzOWrrj8c7V\n+bmJurV3XkgvdOiwNase17wXUG9Ad8FhVYpUicq3Csfx5M87IXUIlx51AxaOQK3G\n3skyJyAm8R8pHzhcsEuVV7bGZlbFPZeWAHpbIEwpKHlLoN6qMINk2kEbcVahL2Mu\nXBUcvJdO2LbmEvfS+1imr32YbJ5Ufetru+G4mwAp6a0P6u5FU8lbE1ZoFHhflsVq\nErvOcRKhKjAim4iwIVGS55Xyx1IpF7YSTYpL89vlMmaJsssEoCQAkf+lxmC3eEuy\n2kBu3QB3cUHZXJK+01krC5MqeHcEVuc/fbNcTsCBp+RYXNRjYsp6IzGjqltUfInE\n3Tywg3P0ZLPO06WLNjNeAFZw6T8yV5gLcTJ1xc5pEf4UiY1uCf4NmDpeWhT+vvto\n2AxC/+7x7EkEfZnYiD6tcyHyY+iuroptws8lc5wRis859kydnq3vtxbXPQ==\n-----END CERTIFICATE-----\n", + }, + expectedCerts: 2, + }, + } + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + factory := &inlineKMProviderFactory{} + provider, err := factory.Create("v1.0", tc.config, "") + if err != nil { + t.Fatalf("failed to create provider: %v", err) + } + certs, _, err := provider.GetCertificates(context.TODO()) + + assert.Equal(t, tc.expectedErr, err != nil) + assert.Equal(t, tc.expectedCerts, len(keymanagementprovider.FlattenKMPMap(certs))) + }) + } +} diff --git a/pkg/keymanagementprovider/keymanagementprovider.go b/pkg/keymanagementprovider/keymanagementprovider.go new file mode 100644 index 000000000..8478480ab --- /dev/null +++ b/pkg/keymanagementprovider/keymanagementprovider.go @@ -0,0 +1,101 @@ +/* +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 keymanagementprovider + +import ( + "context" + "crypto/x509" + "encoding/pem" + "sync" + + "github.com/deislabs/ratify/errors" +) + +// This is a map of properties for fetched certificates/keys +// The key and values are specific to each provider +// +//nolint:revive +type KeyManagementProviderStatus map[string]interface{} + +// KMPMapKey is a key for the map of certificates fetched for a single key management provider resource +type KMPMapKey struct { + Name string + Version 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 + GetCertificates(ctx context.Context) (map[KMPMapKey][]*x509.Certificate, KeyManagementProviderStatus, error) +} + +// static concurreny-safe map to store certificates fetched from key management provider +var certificatesMap sync.Map + +// DecodeCertificates decodes PEM-encoded bytes into an x509.Certificate chain. +func DecodeCertificates(value []byte) ([]*x509.Certificate, error) { + var certs []*x509.Certificate + block, rest := pem.Decode(value) + if block == nil && len(rest) > 0 { + return nil, errors.ErrorCodeCertInvalid.WithComponentType(errors.CertProvider).WithDetail("failed to decode pem block") + } + + for block != nil { + if block.Type == "CERTIFICATE" { + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, errors.ErrorCodeCertInvalid.WithComponentType(errors.CertProvider).WithDetail("error parsing x509 certificate") + } + certs = append(certs, cert) + } + block, rest = pem.Decode(rest) + if block == nil && len(rest) > 0 { + return nil, errors.ErrorCodeCertInvalid.WithComponentType(errors.CertProvider).WithDetail("failed to decode pem block") + } + } + + return certs, nil +} + +// SetCertificatesInMap sets the certificates in the map +// it is concurrency-safe +func SetCertificatesInMap(resource string, certs map[KMPMapKey][]*x509.Certificate) { + certificatesMap.Store(resource, certs) +} + +// GetCertificatesFromMap gets the certificates from the map and returns an empty map of certificate arrays if not found +func GetCertificatesFromMap(resource string) map[KMPMapKey][]*x509.Certificate { + certs, ok := certificatesMap.Load(resource) + if !ok { + return map[KMPMapKey][]*x509.Certificate{} + } + return certs.(map[KMPMapKey][]*x509.Certificate) +} + +// DeleteCertificatesFromMap deletes the certificates from the map +// it is concurrency-safe +func DeleteCertificatesFromMap(resource string) { + certificatesMap.Delete(resource) +} + +// FlattenKMPMap flattens the map of certificates fetched for a single key management provider resource and returns a single array +func FlattenKMPMap(certMap map[KMPMapKey][]*x509.Certificate) []*x509.Certificate { + var certs []*x509.Certificate + for _, v := range certMap { + certs = append(certs, v...) + } + return certs +} diff --git a/pkg/keymanagementprovider/keymanagementprovider_test.go b/pkg/keymanagementprovider/keymanagementprovider_test.go new file mode 100644 index 000000000..97b300772 --- /dev/null +++ b/pkg/keymanagementprovider/keymanagementprovider_test.go @@ -0,0 +1,174 @@ +/* +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 keymanagementprovider + +import ( + "crypto/x509" + "errors" + "testing" + + ratifyerrors "github.com/deislabs/ratify/errors" + "github.com/stretchr/testify/assert" +) + +// TestDecodeCertificates tests the DecodeCertificates method +func TestDecodeCertificates(t *testing.T) { + cases := []struct { + desc string + pemString string + expectedErr bool + expectedCerts int + }{ + { + desc: "empty string", + expectedErr: false, + }, + { + desc: "invalid certificate", + pemString: "-----BEGIN CERTIFICATE-----\nbaddata\n-----END CERTIFICATE-----\n", + expectedErr: true, + }, + { + desc: "single certificate", + pemString: "-----BEGIN CERTIFICATE-----\nMIID2jCCAsKgAwIBAgIQXy2VqtlhSkiZKAGhsnkjbDANBgkqhkiG9w0BAQsFADBvMRswGQYDVQQD\nExJyYXRpZnkuZXhhbXBsZS5jb20xDzANBgNVBAsTBk15IE9yZzETMBEGA1UEChMKTXkgQ29tcGFu\neTEQMA4GA1UEBxMHUmVkbW9uZDELMAkGA1UECBMCV0ExCzAJBgNVBAYTAlVTMB4XDTIzMDIwMTIy\nNDUwMFoXDTI0MDIwMTIyNTUwMFowbzEbMBkGA1UEAxMScmF0aWZ5LmV4YW1wbGUuY29tMQ8wDQYD\nVQQLEwZNeSBPcmcxEzARBgNVBAoTCk15IENvbXBhbnkxEDAOBgNVBAcTB1JlZG1vbmQxCzAJBgNV\nBAgTAldBMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL10bM81\npPAyuraORABsOGS8M76Bi7Guwa3JlM1g2D8CuzSfSTaaT6apy9GsccxUvXd5cmiP1ffna5z+EFmc\nizFQh2aq9kWKWXDvKFXzpQuhyqD1HeVlRlF+V0AfZPvGt3VwUUjNycoUU44ctCWmcUQP/KShZev3\n6SOsJ9q7KLjxxQLsUc4mg55eZUThu8mGB8jugtjsnLUYvIWfHhyjVpGrGVrdkDMoMn+u33scOmrt\nsBljvq9WVo4T/VrTDuiOYlAJFMUae2Ptvo0go8XTN3OjLblKeiK4C+jMn9Dk33oGIT9pmX0vrDJV\nX56w/2SejC1AxCPchHaMuhlwMpftBGkCAwEAAaNyMHAwDgYDVR0PAQH/BAQDAgeAMAkGA1UdEwQC\nMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHwYDVR0jBBgwFoAU0eaKkZj+MS9jCp9Dg1zdv3v/aKww\nHQYDVR0OBBYEFNHmipGY/jEvYwqfQ4Nc3b97/2isMA0GCSqGSIb3DQEBCwUAA4IBAQBNDcmSBizF\nmpJlD8EgNcUCy5tz7W3+AAhEbA3vsHP4D/UyV3UgcESx+L+Nye5uDYtTVm3lQejs3erN2BjW+ds+\nXFnpU/pVimd0aYv6mJfOieRILBF4XFomjhrJOLI55oVwLN/AgX6kuC3CJY2NMyJKlTao9oZgpHhs\nLlxB/r0n9JnUoN0Gq93oc1+OLFjPI7gNuPXYOP1N46oKgEmAEmNkP1etFrEjFRgsdIFHksrmlOlD\nIed9RcQ087VLjmuymLgqMTFX34Q3j7XgN2ENwBSnkHotE9CcuGRW+NuiOeJalL8DBmFXXWwHTKLQ\nPp5g6m1yZXylLJaFLKz7tdMmO355\n-----END CERTIFICATE-----\n", + expectedCerts: 1, + }, + { + desc: "certificate chain", + pemString: "-----BEGIN CERTIFICATE-----\nMIIEeDCCAmCgAwIBAgIUbztxbi/gSPMZGN53oZQZW1h/lbAwDQYJKoZIhvcNAQEL\nBQAwazELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdSZWRtb25k\nMRMwEQYDVQQKDApNeSBDb21wYW55MQ8wDQYDVQQLDAZNeSBPcmcxFzAVBgNVBAMM\nDmNhLmV4YW1wbGUuY29tMB4XDTIzMDIwMTIyNTMxMFoXDTI0MDIwMTIyNTMxMFow\nbTEZMBcGA1UEAxMQbGVhZi5leGFtcGxlLmNvbTEPMA0GA1UECxMGTXkgT3JnMRMw\nEQYDVQQKEwpNeSBDb21wYW55MRAwDgYDVQQHEwdSZWRtb25kMQswCQYDVQQIEwJX\nQTELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCs\nMixf+WT34edYXs2c80zOg4Z/cxOVHU05gywjuISeaP+KS+Joc3emgbub1t5dPclk\nieIwrj3Olk3tvkrPiarOJIcrNR2zfBmQAufR4AUjoc4n1GQSp/voGgw1Hvh0wTkO\nYjhzLomrF242Ond8WTVO3Vq6/tfApfZMFM59eK9LMBkuvwTV4NeLnEnPvpLAoAvV\n9ZvCu7FuQ849R93Aoag2bZc3Tc3UCbahoJs9rTE/rnAqOhJWMGv2J1Y2Wu2eIvkD\n2uCmcVlY+7owG3TwLHTuIOBFl/5MXMvfR+B7yp1OkG23rTwwuSEBlMhYRzJFvssv\n8FX0sea7zhIg5dtoRjIlAgMBAAGjEjAQMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG\n9w0BAQsFAAOCAgEANRUu+9aBAuRf3OsdUWJflMAvuyzREp3skWSOUs4dw0MhcB6x\nb7BSyNdrBgPImLBpqYzNU6IT2eIlLXrYnKLehvPyZQx7LHvIeompA09aKMFFesqi\ngVoW5GRtp3qL3oNuuZJ80r/uKlB6Cj51TWqUbLcctBGHX7TWxFeWmFRnN0Bki00U\nJW/ElaTsr4GB+ltgZM+5USUqSNQqTa8t3d+vH6oVikyV1oYunM41xAfiRZtID04z\no15sLSkWTjavfmZ3+NjllipXFY2tnLqymCcObgdKtJHmTMFSDRngDjY+3+RVj4EY\npNaCCCepvtmXz1C5f06tlgY4ofaautJuAL7K93p/Q9ZcsIhmYWkCUZ0dkWq+eMdT\n9/lB9rQHbrDTaRxEQNIUezFMQEBxR9eC5JQfpw98LobAgA3r4vizQjQPsN0UZ54h\ncAHiyoo1VeckkotXaToRsoLjixPO9Fmss4H3urJTLpcU0drbVoG3emNh4K289vgR\nrjV11TenqvvR3+jJ2AX2ewSsF25m0afheZbrq2ZtyITPAbOqwMwTbTOvJT3HUztt\nhUP3qwsKNPR/hF3FSqZewiYOSqJi5Dk28Vd6mUEQzZa/Ma9RpBR+BAmfgH3Z9gX5\n0TqmAVQn1P8yh+bhEjiNa20bTJ+y5vQ9OrA7fiQ+6vpZCio4NFiEbYK4UBI=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFxzCCA6+gAwIBAgIUY1fnGcYJFIGNk8fevKdGtOuZ5vcwDQYJKoZIhvcNAQEL\nBQAwazELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdSZWRtb25k\nMRMwEQYDVQQKDApNeSBDb21wYW55MQ8wDQYDVQQLDAZNeSBPcmcxFzAVBgNVBAMM\nDmNhLmV4YW1wbGUuY29tMB4XDTIzMDIwMTIyNTIwN1oXDTMzMDEyOTIyNTIwN1ow\nazELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdSZWRtb25kMRMw\nEQYDVQQKDApNeSBDb21wYW55MQ8wDQYDVQQLDAZNeSBPcmcxFzAVBgNVBAMMDmNh\nLmV4YW1wbGUuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtQXd\n+qySVVMHx7iGz9xRdpDKb+zATK3asMFnMXWBn0MCkcOMJvhjajA8yrCpfMudjiW0\nReQ6xcjEfnJqkzwxrK7tPE9cQ6JzQGCxsmscRzKf1NoJL2ske5xBteKuJhSfZ9la\ncIZn/EU2F6eMAl9U4Y+ncIIrl4UoB5H/AJJj62WMl5QAvzXXwCBwlLHQe4/T3Axu\n9xmD3HTC7iQExOUFLdJx86fK3ym0futi1RgOUgD+OrnyDEIkD8mGxffPYPgszS71\nUuJX+NTsLZ/JW3ER/PMAPnBsMsMTTxEIGnrp1CXf2RnwQnDHVsxZFMgkLTS6dksT\nTGevnulTNtVvSKsZ7MpzE01j7zDie4V4dQJzBJMktbeVq9KRoPIEX0WcKpg8bGS8\nd5p5pr+Lu/NOv6ur+av8M649qCPwJAv5i2P6ggT4YMNtY0wMD2kjcHJ9/l2gYpZj\n3DG0Hy3Xo8uKUmTSC7iGhLsSjleNhJkKyh3RCsuMKB9juE4qeXPoaoPWBIuarbkq\neVVZJu2PlgN8UcxM/ym+9GNJIfNJ18WGWm+5P3IDvfBbJ39yvzZlG2czju4BUzYn\nlPOHA/Z18TxZhlPrPVnyKSVbeg7sW17yMUI2LCeaFIOYdIFvM09RaCyLIGrQwhpe\nkLh56xXk702oNHaLxyh/v5kz8EyxnpXIlDntis0CAwEAAaNjMGEwHQYDVR0OBBYE\nFCESfoahHMx7GyBdTBARen7mc37nMB8GA1UdIwQYMBaAFCESfoahHMx7GyBdTBAR\nen7mc37nMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3\nDQEBCwUAA4ICAQBst1MbRJd3FLSY36qaNuDhGncvcXIcYqP2A/SXVhjuAhVOTsrE\nejDYBSkjfxCgoA3LZnQLTcbcPwRL5dTBEvqBqzbyArOjc+G3kbjOeuZYv09M8kUU\nYG1bVnxXV17GMk7zcBUnr1iwnp4E0pzB9gTv+Jv1oV+EtAHe5QOTOmW9mm2SUXbH\nmIya+KlzkIgVJJ8kiGOyXsr8i4wpyDXZf720tqTzPQFTf6rUXo9PhYzOWrrj8c7V\n+bmJurV3XkgvdOiwNase17wXUG9Ad8FhVYpUicq3Csfx5M87IXUIlx51AxaOQK3G\n3skyJyAm8R8pHzhcsEuVV7bGZlbFPZeWAHpbIEwpKHlLoN6qMINk2kEbcVahL2Mu\nXBUcvJdO2LbmEvfS+1imr32YbJ5Ufetru+G4mwAp6a0P6u5FU8lbE1ZoFHhflsVq\nErvOcRKhKjAim4iwIVGS55Xyx1IpF7YSTYpL89vlMmaJsssEoCQAkf+lxmC3eEuy\n2kBu3QB3cUHZXJK+01krC5MqeHcEVuc/fbNcTsCBp+RYXNRjYsp6IzGjqltUfInE\n3Tywg3P0ZLPO06WLNjNeAFZw6T8yV5gLcTJ1xc5pEf4UiY1uCf4NmDpeWhT+vvto\n2AxC/+7x7EkEfZnYiD6tcyHyY+iuroptws8lc5wRis859kydnq3vtxbXPQ==\n-----END CERTIFICATE-----\n", + expectedCerts: 2, + }, + { + desc: "certificate chain with private key", + pemString: "-----BEGIN CERTIFICATE-----\nMIIDAjCCAeqgAwIBAgIUd75qExchDrnhbpWleyCIfnSWUUAwDQYJKoZIhvcNAQEL\nBQAwKjEPMA0GA1UECgwGUmF0aWZ5MRcwFQYDVQQDDA5SYXRpZnkgUm9vdCBDQTAe\nFw0yMzAxMjcxOTE5MzBaFw0yNDAxMjcxOTE5MzBaMCMxITAfBgNVBAMMGHJhdGlm\neS5nYXRla2VlcGVyLXN5c3RlbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBAJkAFk9oH65zB4NwvpzZZEkVD8mIBmsNTa5f3XTdcDdrAsVCWiZHGwo8PsXH\n6MIEXMNat/Rvww/1XsBcbMAjNm62nLTLKup3tkIJbEFCshaniWefqElLyHA2C7ra\nFH2SMHfHJE8veDyrKERiJa638o4RXlKtbWk9V6adJssHBq+UmVDRfKOO54ToLdnX\nGE4uc6wk40cW2YIlbNFnGJRFlCMCBT+8weZDTwMEc7jNObzJC9tBUqJKJwoQsvpw\nvQxROH+sIhtgq6kuxlGJhT8wertWbmFx5ZZqJulDL3IEdQNGzUn97ct0d7kYn3/l\ng/5KTqBxE8b+J/WoIk7WNYN4990CAwEAAaMnMCUwIwYDVR0RBBwwGoIYcmF0aWZ5\nLmdhdGVrZWVwZXItc3lzdGVtMA0GCSqGSIb3DQEBCwUAA4IBAQB9SxMyk/jV1Yr4\nnPmTVvbPfjRuSwxZXyL8EZ/UtVyEWR/t+eXwqm+NHVh7zmyDbfaTliufeLG6Jcuu\n9gu3g1aIvYhgDC3EnEdarIj8LJyNcnfueup2zCuzuqC2+MmnTEJeQbBSPFI1C5eI\nQcJSg5bWSCllWgvncmqEYJqvg470iEWR3c5oaay8viBvpHpNCBLlZM519/hQehnp\n91yFCEyNEysYytWsZJNiwOdZETulJoNJKuzIfqf4tS9u34nhcfySyu9mDO/nIPqd\n6A6xKd6iXlTxhclWg2bq8R7FAmv+1doAfcWLU4/RSFv5NQ3VzGwgKXTh4Fs681NX\nw5lcS5MK\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDNTCCAh2gAwIBAgIUcUNPTu9ivrZtdfWSNsjkYVgLXIEwDQYJKoZIhvcNAQEL\nBQAwKjEPMA0GA1UECgwGUmF0aWZ5MRcwFQYDVQQDDA5SYXRpZnkgUm9vdCBDQTAe\nFw0yMzAxMjcxOTE5MjlaFw0yMzAxMjgxOTE5MjlaMCoxDzANBgNVBAoMBlJhdGlm\neTEXMBUGA1UEAwwOUmF0aWZ5IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB\nDwAwggEKAoIBAQCx/QONvaFNaozlaC6ud0YBBHk0Ic6I+3MYWDPC0jP2WllA2iZ2\nM2FHFYQtxl8FirTCTgO3DBzJpFVYVFUPfPaf9cMpInzNR4eZ5CD5Jze/v/Z0G31Y\n+ZbxvQ5zrsqvGCUniizqpGiWy/F4y/qw6EO2LRmsOGoAT9P7t1cZIYTKVOuoSH3r\njrAU0q4za+TqPGNmcoP+awKI1dtyw1r/V1tLZINDohMdqkp+soy551VP1jM6zWbe\nGiyKvwUDb2UzPaWtd1SeCg3LSu5e8coreAAZbtjlaNTlxdo/r8Hxv8S0aRSygA71\n9XkAPNX4DvUoUN1MELlQrSnF0qAj9PCZuPZvAgMBAAGjUzBRMB0GA1UdDgQWBBSe\n42hEgFzqWnbaiWhXygaykP/yrjAfBgNVHSMEGDAWgBSe42hEgFzqWnbaiWhXygay\nkP/yrjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAfa+VVqAM1\nQ+UYZkMG1Nsw30onJxSOk0O4YZWSWJxnRHUM3zWD2IwjEaehJ0T6c47R+sALLdQq\ngx15/RSISvw4CQovrQoCxg6MOhvAPKx0xtkuR02Q8kcY4fJ93cBsAVD6Khb+QIKs\n1JB7cOMSLTRbfjW5Tf4DNBkGrrHD62IfCwu1pPxTlyx8CRzONd04oecFEd5yNK61\nLG61lO83x+kQND9Sv/kdY64Cy2mluaQeXaLy/GSjty03O84teTSxVb6QotzmN2UV\nAEHgoB2sp4v9Q358LcpamLk3Ie4iPj/dVL9sNWQaTiwlOH6Jknk3/jgm+B6mxMdO\ncVBH1VjkwzuC\n-----END CERTIFICATE-----\n-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCZABZPaB+ucweD\ncL6c2WRJFQ/JiAZrDU2uX9103XA3awLFQlomRxsKPD7Fx+jCBFzDWrf0b8MP9V7A\nXGzAIzZutpy0yyrqd7ZCCWxBQrIWp4lnn6hJS8hwNgu62hR9kjB3xyRPL3g8qyhE\nYiWut/KOEV5SrW1pPVemnSbLBwavlJlQ0XyjjueE6C3Z1xhOLnOsJONHFtmCJWzR\nZxiURZQjAgU/vMHmQ08DBHO4zTm8yQvbQVKiSicKELL6cL0MUTh/rCIbYKupLsZR\niYU/MHq7Vm5hceWWaibpQy9yBHUDRs1J/e3LdHe5GJ9/5YP+Sk6gcRPG/if1qCJO\n1jWDePfdAgMBAAECggEAAY6hq384y1K6YdkU543C2oePWJK81fwVrU+mdlkGmlnJ\ndm59cmRI3yrLzMGDGe5nb0mOE7vLdW8e3sBSDwaMuEW9hI2Iy0gan8NuyZ8/JsHf\nwSE72jseOB4ksmsjyD9jpORu9ytZguyPBVsmXQfcPRvqJNdFBMwuBzEUQv64T7Mk\ncK5xqXJQmN230L7AZahScVqqFHQjIexXBJfLIbrAmWEBw9bLGkSMJ0YEyyay6Y6E\ntJ4JNjEoKBge6ZmcK/rzbGNy454lncgVE9IteYAAl6+E+anerKZ5/c3segIOSVyh\nPRIAcMcucyPTVMi/HN/w2CGdPyqMozPj5xvTiJ30AQKBgQDLE47QUIhcXFVbXjsN\nSJjlyBqRKuh9n5yFDJMphB5IGfu+NuSoiGNrX4an0TffnalobuSq7KPhYwSzluFf\nULtPNFvsSq6iA18wxWCb75A4SYWvXIM9TFBwHBCXAGmHxtRLNClcrgDDX2MuS7Qv\nOmShb5sCkUoJy3NX/tIalpCxAQKBgQDA36fd4KjgraJL+OWt+7aF9IDAN7tgFWZi\nnPv0npc+KvNqZ24wwWpIQ9trgOrquBkDUr75wco3WvJHpujhiUJ4Uwnd5cqC9V/z\nO9iU/zohjo7cgJPy7rI5MECwRByKb+jf2XrxiRA19Ecbw7UET5UvRidKGP8JJoyk\nUOM8JYYq3QKBgAMoU7Ejf2tIOD+KcIqdVVtFSDx3mVPStoFPF76ugjYGyWZEvjts\nm3cg7hwP4bmFXwvzpXSO52Fqw7jzIJ/1xmPN4ZwD8UEtoj5E42KpT+nAIub+HkBG\nvn1vwkZGyF1HFyfwMLBzOCnRgt5GaQ/O7Z+g950Lm0YZtrpoiOXG74sBAoGAOqyP\nY7cxiNApnFUGgiwd9ZhRBqitrugzsnIxT9RjDD2CuW7nnZtpWryR5p1cWbVRnqow\ngMhMXRSkudlz5RCdkP8p9EAwoDBHVTZyh7kxFP5KRZgz6eZlf3JHa5f82rx6qoZ9\nmTbqII/EhhS+X6ZaKvx7fVYnV8BLbr1Qs35y110CgYBwooD3OTNTVVUAqOcTQGtO\nY+USSQ3Mrp2r6ufQUSgAYPwpF5VEmmwcJEQIT5vFCMxoUpOboy5iANr+V2yy4nNY\nSJU6EbK/kq9oZeY9pe3YQrYBpxWEuoX5T5IDd6Mf2o5ihv4R8OtFKjEynNDFuBDP\ny5KBUeNvlaHx06iIQZ+pzw==\n-----END PRIVATE KEY-----\n", + expectedCerts: 2, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + certs, err := DecodeCertificates([]byte(tc.pemString)) + + assert.Equal(t, tc.expectedErr, err != nil) + assert.Equal(t, tc.expectedCerts, len(certs)) + }) + } +} + +// TestDecodeCertificates_ByteArrayToCertificates checks certificate byte deserialization +func TestDecodeCertificates_ByteArrayToCertificates(t *testing.T) { + certString := "-----BEGIN CERTIFICATE-----\nMIIDsDCCApigAwIBAgIQMdNmNTKwQ9aOe6iuMRokDzANBgkqhkiG9w0BAQsFADBa\nMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzAN\nBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIy\nMTIxNDIxNTAzMVoXDTIzMTIxNDIyMDAzMVowWjELMAkGA1UEBhMCVVMxCzAJBgNV\nBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNV\nBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\nAQoCggEBAOP6AHCFz41kRqsAiv6guFtQVsrzMgzoCX7o9NtQ57rr8BESP1LTGRAO\n4bjyP0i+at5uwIm4tdz0gW+g0P+f9bmfiScYgOFuxAJxLkMkBWPN3dJ9ulP/OGgB\n6mSCsEGreB3uaGc5rMbWCRaux65bMPjEzx5ex0qRSsn+fFMTwINPQUJpXSvi/W2/\n1umEWE1x59x0vlkP2dN7CXtB5/Bh01QNNbMdKU9saYn0kaBrCYZLwr6AxFRzLqLM\nQggy/6bOp/+cTTVqTiChMcdyIX52GRr2lChRsB34dDPYxDeKSI5LoRy07bveLjex\n4wm9+vx/WOSS5z0QPvE/v8avuIkMXR0CAwEAAaNyMHAwDgYDVR0PAQH/BAQDAgeA\nMAkGA1UdEwQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHwYDVR0jBBgwFoAUwVvE\nvqQPxnE6j6pfX6jpSyv2dOAwHQYDVR0OBBYEFMFbxL6kD8ZxOo+qX1+o6Usr9nTg\nMA0GCSqGSIb3DQEBCwUAA4IBAQDE61FLbagvlCcXf0zcv+mUQ+0HvDVs7ofQe3Yw\naz7gAwxgTspr+jIFQWnPOOBupsyx/jucoz78ndbc5DGWPs2Qz/pIEGnLto2W/PYy\nas/9n8xHxembS4n/Mxxp60PF6ladi/nJAtDJds67sBeqLOfJzh6jV2uQvW7PXe1P\nOMSUHbBn8AfArZ/9njusiLs75+XcAgpnBFqKVv2Vd/INp2YQpVzusuiodeM8A9Qt\n/5xykjdCJw3ceZxD7dSkHgchKZPINFBYHt/EkN/d8mXFOKjGXZyntp4PO6PJ2HYN\nhMMDwdNu4mBmlMTdZMPEpIZIeW7G0P9KpCuvvD7po7NxdBgI\n-----END CERTIFICATE-----\n" + + c1 := []byte(certString) + + r, err := DecodeCertificates(c1) + if err != nil { + t.Fatalf(err.Error()) + } + + expectedLen := 1 + if len(r) != expectedLen { + t.Fatalf("unexpected count of certificate, expected %+v, got %+v", expectedLen, len(r)) + } + + cert1 := r[0] + serialNumber1 := cert1.SerialNumber.String() + + expectedserialNumber1 := "66229819451171253920043613209346319375" + if serialNumber1 != expectedserialNumber1 { + t.Fatalf("unexpected certificate, expected %+v, got %+v", expectedserialNumber1, serialNumber1) + } +} + +// TestDecodeCertificates_FailedToDecode verifies expected error failure for malformed pem +func TestDecodeCertificates_FailedToDecode(t *testing.T) { + certString := "foo" + + c1 := []byte(certString) + + _, err := DecodeCertificates(c1) + if err == nil { + t.Fatalf("DecodeCertificates should return an error") + } + + expectedError := ratifyerrors.ErrorCodeCertInvalid.WithDetail("failed to decode pem block") + if !errors.Is(err, expectedError) { + t.Fatalf("unexpected error, expected %+v, got %+v", expectedError, err.Error()) + } +} + +// TestDecodeCertificates_FailedX509ParseError verifies expected error failure for malformed x509 certificate +func TestDecodeCertificates_FailedX509ParseError(t *testing.T) { + certString := "-----BEGIN CERTIFICATE-----\nMIIDsDCCApigAwIBAgIQFJMQeqR8TRuHqNu+x0MuEDANBgkqhkiG9w0BAQsFABAD\nMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzAN\nBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIz\nMDExMTE5MjAxMloXDTI0MDExMTE5MzAxMlowWjELMAkGA1UEBhMCVVMxCzAJBgNV\nBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNV\nBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\nAQoCggEBAMh7F6sZyeiQRva83SvQu0PbsyD4zkEeWAyj03n1dx91FEeEXItCr+Y1\nghQKgdBOY/wJQmSq/We1e+17NoNICrzy2Y1sOVMYR5sx8H/UxO3q8oS7bxctFy+e\nHs4BxlHIqeIiWnz9bFAJFqV6BkJDVjp9k5QfHlkqH08WBvm/D8YTpWzvEPn+71ZG\nN1RKqFUeeM949oGGnC63vVMRRYIx2LoJliNZXdj9qoOHZksDrX2jkgPykkOYcmfo\n9CH9v0JNX+0t0Enp0ruUFK1pSZW+TicI22GvENYHGZNZ0m+6oD5ePRZoYhWyAzgZ\nndHO5bYh3yC7DMc6ssOEJeNN0I2+iLUCAwEAAaNyMHAwDgYDVR0PAQH/BAQDAgeA\nMAkGA1UdEwQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHwYDVR0jBBgwFoAUYhhf\nPFgAqU8PF3ClvfKs67HmpWwwHQYDVR0OBBYEFGIYXzxYAKlPDxdwpb3yrOux5qVs\nMA0GCSqGSIb3DQEBCwUAA4IBAQCXu1w+6s2RO2/KPmC+29m9EjbDReI4bGlDGgiv\nwk1fmvPvDrqL4Ebpcrb1nstNlsxpKYQP+3Vi8gPiqNQ7JvPStd1NBu+ViCXdvOe5\nCtN7tBFTCBgdgXNZ9bvIM2dS+xW/ZAJdyHbV9Hn77+rs/uCDHtbaQMJ3N9LGW8GR\nGY+uYylrrCrjb9fzotMaONnF9c1GKiANskc9371wbaninpxcwMNA5j027XzfnMEW\nm807wjlNV3Kuf4fdDpzBLe940iplfTlQMylWMqgANpEw4EqHCrBJPQAHfQEpQlo+\n9H72lrqOiYNNwApfB9P+UqMDi1B7T2yzfvXcqQ75FpSRIBAD\n-----END CERTIFICATE-----\n" + + c1 := []byte(certString) + + _, err := DecodeCertificates(c1) + if err == nil { + t.Fatalf("DecodeCertificates should return an error") + } + + expectedError := ratifyerrors.ErrorCodeCertInvalid.WithDetail("error parsing x509 certificate: x509: malformed issuer") + if !errors.Is(err, expectedError) { + t.Fatalf("unexpected error, expected %+v, got %+v", expectedError, err.Error()) + } +} + +// TestSetCertificatesInMap checks if certificates are set in the map +func TestSetCertificatesInMap(t *testing.T) { + certificatesMap.Delete("test") + SetCertificatesInMap("test", map[KMPMapKey][]*x509.Certificate{{}: {{Raw: []byte("testcert")}}}) + if _, ok := certificatesMap.Load("test"); !ok { + t.Fatalf("certificatesMap should have been set for key") + } +} + +// TestGetCertificatesFromMap checks if certificates are fetched from the map +func TestGetCertificatesFromMap(t *testing.T) { + certificatesMap.Delete("test") + SetCertificatesInMap("test", map[KMPMapKey][]*x509.Certificate{{}: {{Raw: []byte("testcert")}}}) + certs := GetCertificatesFromMap("test") + if len(certs) != 1 { + t.Fatalf("certificates should have been fetched from the map") + } +} + +// TestGetCertificatesFromMap_FailedToFetch checks if certificates are fetched from the map +func TestGetCertificatesFromMap_FailedToFetch(t *testing.T) { + certificatesMap.Delete("test") + certs := GetCertificatesFromMap("test") + if len(certs) != 0 { + t.Fatalf("certificates should not have been fetched from the map") + } +} + +// TestDeleteCertificatesFromMap checks if certificates are deleted from the map +func TestDeleteCertificatesFromMap(t *testing.T) { + certificatesMap.Delete("test") + SetCertificatesInMap("test", map[KMPMapKey][]*x509.Certificate{{}: {{Raw: []byte("testcert")}}}) + DeleteCertificatesFromMap("test") + if _, ok := certificatesMap.Load("test"); ok { + t.Fatalf("certificatesMap should have been deleted for key") + } +} + +// TestFlattenKMPMap checks if certificates in map are flattened to a single array +func TestFlattenKMPMap(t *testing.T) { + certs := FlattenKMPMap(map[KMPMapKey][]*x509.Certificate{{}: {{Raw: []byte("testcert")}}}) + if len(certs) != 1 { + t.Fatalf("certificates should have been flattened") + } +} diff --git a/pkg/keymanagementprovider/mocks/types.go b/pkg/keymanagementprovider/mocks/types.go new file mode 100644 index 000000000..6bbf2dc80 --- /dev/null +++ b/pkg/keymanagementprovider/mocks/types.go @@ -0,0 +1,33 @@ +/* +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 mocks + +import ( + "context" + "crypto/x509" + + "github.com/deislabs/ratify/pkg/keymanagementprovider" +) + +type TestKeyManagementProvider struct { + certificates map[keymanagementprovider.KMPMapKey][]*x509.Certificate + status keymanagementprovider.KeyManagementProviderStatus + err error +} + +func (c *TestKeyManagementProvider) GetCertificates(_ context.Context) (map[keymanagementprovider.KMPMapKey][]*x509.Certificate, keymanagementprovider.KeyManagementProviderStatus, error) { + return c.certificates, c.status, c.err +} diff --git a/pkg/keymanagementprovider/types/types.go b/pkg/keymanagementprovider/types/types.go new file mode 100644 index 000000000..c15354dff --- /dev/null +++ b/pkg/keymanagementprovider/types/types.go @@ -0,0 +1,23 @@ +/* +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 types + +const ( + SpecVersion string = "0.1.0" + Version string = "version" + Type string = "type" + Source string = "source" +) diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go index f43573c1d..36cd2cd09 100644 --- a/pkg/manager/manager.go +++ b/pkg/manager/manager.go @@ -237,6 +237,13 @@ func StartManager(certRotatorReady chan struct{}, probeAddr string) { setupLog.Error(err, "unable to create controller", "controller", "Policy") os.Exit(1) } + if err = (&controllers.KeyManagementProviderReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Key Management Provider") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/pkg/verifier/notation/truststore.go b/pkg/verifier/notation/truststore.go index 048c0df5f..883879604 100644 --- a/pkg/verifier/notation/truststore.go +++ b/pkg/verifier/notation/truststore.go @@ -23,6 +23,7 @@ import ( "github.com/deislabs/ratify/internal/logger" "github.com/deislabs/ratify/pkg/controllers" + "github.com/deislabs/ratify/pkg/keymanagementprovider" "github.com/deislabs/ratify/pkg/utils" "github.com/notaryproject/notation-go/verifier/truststore" ) @@ -57,7 +58,14 @@ func (s trustStore) getCertificatesInternal(ctx context.Context, namedStore stri logger.GetLogger(ctx, logOpt).Debugf("truststore getting certStore %v", certStore) result := certificatesMap[certStore] if len(result) == 0 { - logger.GetLogger(ctx, logOpt).Warnf("no certificate fetched for certStore %+v", certStore) + // check key management provider if certificate store does not have certificates. + // NOTE: certificate store and key management provider cannot be configured together. + // This will be enforced by the controller/CLI + result = keymanagementprovider.FlattenKMPMap(keymanagementprovider.GetCertificatesFromMap(certStore)) + // notation verifier does not consider specific named/versioned certificates within a key management provider resource + if len(result) == 0 { + logger.GetLogger(ctx, logOpt).Warnf("no certificate fetched for certStore %+v", certStore) + } } certs = append(certs, result...) } diff --git a/scripts/azure-ci-test.sh b/scripts/azure-ci-test.sh index 2479aa51e..9f0438c93 100755 --- a/scripts/azure-ci-test.sh +++ b/scripts/azure-ci-test.sh @@ -66,8 +66,8 @@ deploy_ratify() { --set gatekeeper.version=${GATEKEEPER_VERSION} \ --set akvCertConfig.enabled=true \ --set akvCertConfig.vaultURI=${VAULT_URI} \ - --set akvCertConfig.certificates[0].certificateName=${NOTATION_PEM_NAME} \ - --set akvCertConfig.certificates[1].certificateName=${NOTATION_CHAIN_PEM_NAME} \ + --set akvCertConfig.certificates[0].name=${NOTATION_PEM_NAME} \ + --set akvCertConfig.certificates[1].name=${NOTATION_CHAIN_PEM_NAME} \ --set akvCertConfig.tenantId=${TENANT_ID} \ --set oras.authProviders.azureWorkloadIdentityEnabled=true \ --set azureWorkloadIdentity.clientId=${IDENTITY_CLIENT_ID} \ diff --git a/test/bats/azure-test.bats b/test/bats/azure-test.bats index a0eb9ae43..9e151f11f 100644 --- a/test/bats/azure-test.bats +++ b/test/bats/azure-test.bats @@ -45,7 +45,7 @@ SLEEP_TIME=1 @test "validate image signed by leaf cert" { teardown() { - wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete certificatestores.config.ratify.deislabs.io/certstore-inline --namespace default --ignore-not-found=true' + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete keymanagementproviders.config.ratify.deislabs.io/keymanagementprovider-inline --namespace default --ignore-not-found=true' wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod demo-leaf --namespace default --force --ignore-not-found=true' wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod demo-leaf2 --namespace default --force --ignore-not-found=true' @@ -64,13 +64,13 @@ SLEEP_TIME=1 assert_success # add the leaf certificate as an inline certificate store - cat ~/.config/notation/truststore/x509/ca/leaf-test/leaf.crt | sed 's/^/ /g' >>./test/bats/tests/config/config_v1beta1_certstore_inline.yaml - run kubectl apply -f ./test/bats/tests/config/config_v1beta1_certstore_inline.yaml + cat ~/.config/notation/truststore/x509/ca/leaf-test/leaf.crt | sed 's/^/ /g' >>./test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml + run kubectl apply -f ./test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml assert_success - sed -i '9,$d' ./test/bats/tests/config/config_v1beta1_certstore_inline.yaml + sed -i '10,$d' ./test/bats/tests/config/config_v1beta1_certstore_inline.yaml - # configure the notation verifier to use the inline certificate store - run kubectl apply -f ./test/bats/tests/config/config_v1beta1_verifier_notation.yaml + # configure the notation verifier to use the inline key management provider + run kubectl apply -f ./test/bats/tests/config/config_v1beta1_verifier_notation_kmprovider.yaml assert_success # wait for the httpserver cache to be invalidated @@ -250,7 +250,7 @@ SLEEP_TIME=1 assert_failure echo "Add notation verifier and validate deployment succeeds" - run kubectl apply -f ./config/samples/config_v1beta1_verifier_notation_certstore.yaml + run kubectl apply -f ./config/samples/config_v1beta1_verifier_notation_kmprovider.yaml assert_success # wait for the httpserver cache to be invalidated @@ -304,7 +304,7 @@ SLEEP_TIME=1 @test "validate mutation tag to digest" { teardown() { echo "cleaning up" - wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod mutate-demo --namespace default' + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod mutate-demo --namespace default --ignore-not-found=true' } run kubectl apply -f ./library/default/template.yaml assert_success diff --git a/test/bats/base-test.bats b/test/bats/base-test.bats index e9671c3d2..66cbc7ab7 100644 --- a/test/bats/base-test.bats +++ b/test/bats/base-test.bats @@ -34,8 +34,8 @@ RATIFY_NAMESPACE=gatekeeper-system run kubectl apply -f ./library/default/samples/constraint.yaml assert_success sleep 5 - # validate certificate store status property shows success - run bash -c "kubectl get certificatestores.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -n ${RATIFY_NAMESPACE} -o yaml | grep 'issuccess: true'" + # validate key management provider status property shows success + run bash -c "kubectl get keymanagementproviders.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -n ${RATIFY_NAMESPACE} -o yaml | grep 'issuccess: true'" assert_success run kubectl run demo --namespace default --image=registry:5000/notation:signed assert_success @@ -86,8 +86,8 @@ RATIFY_NAMESPACE=gatekeeper-system assert_success sleep 5 - # validate certificate store status property shows success - run bash -c "kubectl get certificatestores.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -n ${RATIFY_NAMESPACE} -o yaml | grep 'issuccess: true'" + # validate key management provider status property shows success + run bash -c "kubectl get keymanagementproviders.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -n ${RATIFY_NAMESPACE} -o yaml | grep 'issuccess: true'" assert_success run kubectl run demo --namespace default --image=registry:5000/notation:signed assert_success @@ -103,10 +103,10 @@ RATIFY_NAMESPACE=gatekeeper-system wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod demo1 --namespace default --force --ignore-not-found=true' # restore cert store in ratify namespace - run bash -c "kubectl get certificatestores.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -o yaml -n default > certStore.yaml" - run kubectl delete certificatestores.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -n default - sed 's/default/gatekeeper-system/' certStore.yaml > certStoreNewNS.yaml - run kubectl apply -f certStoreNewNS.yaml + run bash -c "kubectl get keymanagementproviders.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -o yaml -n default > kmprovider.yaml" + run kubectl delete keymanagementproviders.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -n default + sed 's/default/gatekeeper-system/' kmprovider.yaml > kmproviderNewNS.yaml + run kubectl apply -f kmproviderNewNS.yaml assert_success # restore the original notation verifier for other tests @@ -120,17 +120,17 @@ RATIFY_NAMESPACE=gatekeeper-system assert_success sleep 5 - # apply the certstore to default namespace - run bash -c "kubectl get certificatestores.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -o yaml -n ${RATIFY_NAMESPACE} > certStore.yaml" + # apply the key management provider to default namespace + run bash -c "kubectl get keymanagementproviders.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -o yaml -n ${RATIFY_NAMESPACE} > kmprovider.yaml" assert_success - sed 's/gatekeeper-system/default/' certStore.yaml > certStoreNewNS.yaml - run kubectl apply -f certStoreNewNS.yaml + sed 's/gatekeeper-system/default/' kmprovider.yaml > kmproviderNewNS.yaml + run kubectl apply -f kmproviderNewNS.yaml assert_success - run kubectl delete certificatestores.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -n ${RATIFY_NAMESPACE} + run kubectl delete keymanagementproviders.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -n ${RATIFY_NAMESPACE} assert_success # configure the notation verifier to use inline certificate store with specific namespace - run kubectl apply -f ./config/samples/config_v1beta1_verifier_notation_specificnscertstore.yaml + run kubectl apply -f ./config/samples/config_v1beta1_verifier_notation_specificnskmprovider.yaml assert_success run kubectl run demo --namespace default --image=registry:5000/notation:signed @@ -255,15 +255,20 @@ RATIFY_NAMESPACE=gatekeeper-system assert_mutate_success } -@test "validate inline cert provider" { +@test "validate inline certificate store provider" { teardown() { - wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete certificatestores.config.ratify.deislabs.io/certstore-inline --namespace default --ignore-not-found=true' + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete certificatestores.config.ratify.deislabs.io/certstore-inline --namespace ${RATIFY_NAMESPACE} --ignore-not-found=true' wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod demo-alternate --namespace default --force --ignore-not-found=true' + # restore the original key management provider provider + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl apply -f kmprovider_staging.yaml' # restore the original notation verifier for other tests wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl apply -f ./config/samples/config_v1beta1_verifier_notation.yaml' } + # save the existing key management provider inline resource to restore later + run bash -c "kubectl get keymanagementproviders.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -n ${RATIFY_NAMESPACE} -o yaml > kmprovider_staging.yaml" + assert_success # configure the default template/constraint run kubectl apply -f ./library/default/template.yaml assert_success @@ -274,6 +279,9 @@ RATIFY_NAMESPACE=gatekeeper-system run kubectl run demo-alternate --namespace default --image=registry:5000/notation:signed-alternate assert_failure + # delete the existing key management provider inline resource since certificate store and key management provider cannot be used together + run kubectl delete keymanagementproviders.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -n ${RATIFY_NAMESPACE} + assert_success # add the alternate certificate as an inline certificate store cat ~/.config/notation/truststore/x509/ca/alternate-cert/alternate-cert.crt | sed 's/^/ /g' >>./test/bats/tests/config/config_v1beta1_certstore_inline.yaml run kubectl apply -f ./test/bats/tests/config/config_v1beta1_certstore_inline.yaml --namespace ${RATIFY_NAMESPACE} @@ -290,6 +298,43 @@ RATIFY_NAMESPACE=gatekeeper-system assert_success } +@test "validate inline key management provider provider" { + teardown() { + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete keymanagementproviders.config.ratify.deislabs.io/keymanagementprovider-inline --namespace ${RATIFY_NAMESPACE} --ignore-not-found=true' + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod demo-alternate --namespace default --force --ignore-not-found=true' + + # restore the original notation verifier for other tests + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl apply -f ./config/samples/config_v1beta1_verifier_notation.yaml' + } + + # configure the default template/constraint + run kubectl apply -f ./library/default/template.yaml + assert_success + run kubectl apply -f ./library/default/samples/constraint.yaml + assert_success + + # verify that the image cannot be run due to an invalid cert + sleep 10 + run kubectl run demo-alternate --namespace default --image=registry:5000/notation:signed-alternate + assert_failure + sleep 10 + + # add the alternate certificate as an inline key management provider + cat ~/.config/notation/truststore/x509/ca/alternate-cert/alternate-cert.crt | sed 's/^/ /g' >>./test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml + run kubectl apply -f ./test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml --namespace ${RATIFY_NAMESPACE} + assert_success + sed -i '10,$d' ./test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml + + # configure the notation verifier to use the inline key management provider + run kubectl apply -f ./test/bats/tests/config/config_v1beta1_verifier_notation_kmprovider.yaml + assert_success + sleep 10 + + # verify that the image can now be run + run kubectl run demo-alternate --namespace default --image=registry:5000/notation:signed-alternate + assert_success +} + @test "validate K8s secrets ORAS auth provider" { teardown() { echo "cleaning up" @@ -316,7 +361,7 @@ RATIFY_NAMESPACE=gatekeeper-system @test "validate image signed by leaf cert" { teardown() { - wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete certificatestores.config.ratify.deislabs.io/certstore-inline --namespace ${RATIFY_NAMESPACE} --ignore-not-found=true' + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete keymanagementproviders.config.ratify.deislabs.io/keymanagementprovider-inline --namespace ${RATIFY_NAMESPACE} --ignore-not-found=true' wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod demo-leaf --namespace default --force --ignore-not-found=true' wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod demo-leaf2 --namespace default --force --ignore-not-found=true' @@ -330,25 +375,25 @@ RATIFY_NAMESPACE=gatekeeper-system run kubectl apply -f ./library/default/samples/constraint.yaml assert_success - # add the root certificate as an inline certificate store - cat ~/.config/notation/truststore/x509/ca/leaf-test/root.crt | sed 's/^/ /g' >>./test/bats/tests/config/config_v1beta1_certstore_inline.yaml - run kubectl apply -f ./test/bats/tests/config/config_v1beta1_certstore_inline.yaml --namespace ${RATIFY_NAMESPACE} + # add the root certificate as an inline key management provider + cat ~/.config/notation/truststore/x509/ca/leaf-test/root.crt | sed 's/^/ /g' >>./test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml + run kubectl apply -f ./test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml --namespace ${RATIFY_NAMESPACE} assert_success - sed -i '9,$d' ./test/bats/tests/config/config_v1beta1_certstore_inline.yaml + sed -i '10,$d' ./test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml - # configure the notation verifier to use the inline certificate store - run kubectl apply -f ./test/bats/tests/config/config_v1beta1_verifier_notation.yaml + # configure the notation verifier to use the inline key management provider + run kubectl apply -f ./test/bats/tests/config/config_v1beta1_verifier_notation_kmprovider.yaml assert_success # verify that the image can be run with a root cert run kubectl run demo-leaf --namespace default --image=registry:5000/notation:leafSigned assert_success - # add the root certificate as an inline certificate store - cat ~/.config/notation/truststore/x509/ca/leaf-test/leaf.crt | sed 's/^/ /g' >>./test/bats/tests/config/config_v1beta1_certstore_inline.yaml - run kubectl apply -f ./test/bats/tests/config/config_v1beta1_certstore_inline.yaml --namespace ${RATIFY_NAMESPACE} + # add the root certificate as an inline key management provider + cat ~/.config/notation/truststore/x509/ca/leaf-test/leaf.crt | sed 's/^/ /g' >>./test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml + run kubectl apply -f ./test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml --namespace ${RATIFY_NAMESPACE} assert_success - sed -i '9,$d' ./test/bats/tests/config/config_v1beta1_certstore_inline.yaml + sed -i '10,$d' ./test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml # wait for the httpserver cache to be invalidated sleep 15 diff --git a/test/bats/high-availability.bats b/test/bats/high-availability.bats index 22670ae9c..e0a11bd72 100644 --- a/test/bats/high-availability.bats +++ b/test/bats/high-availability.bats @@ -31,8 +31,8 @@ SLEEP_TIME=1 run kubectl apply -f ./library/default/samples/constraint.yaml assert_success sleep 5 - # validate certificate store status property shows success - run bash -c "kubectl get certificatestores.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -n gatekeeper-system -o yaml | grep 'issuccess: true'" + # validate key management provider status property shows success + run bash -c "kubectl get keymanagementproviders.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -n gatekeeper-system -o yaml | grep 'issuccess: true'" assert_success run kubectl run demo --namespace default --image=registry:5000/notation:signed assert_success diff --git a/test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml b/test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml new file mode 100644 index 000000000..b0984cbe5 --- /dev/null +++ b/test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml @@ -0,0 +1,9 @@ +apiVersion: config.ratify.deislabs.io/v1beta1 +kind: KeyManagementProvider +metadata: + name: keymanagementprovider-inline +spec: + type: inline + parameters: + contentType: certificate + value: | diff --git a/test/bats/tests/config/config_v1beta1_verifier_notation_akv.yaml b/test/bats/tests/config/config_v1beta1_verifier_notation_akv.yaml index 1113d0a04..031180229 100644 --- a/test/bats/tests/config/config_v1beta1_verifier_notation_akv.yaml +++ b/test/bats/tests/config/config_v1beta1_verifier_notation_akv.yaml @@ -8,7 +8,7 @@ spec: parameters: verificationCertStores: certs: - - certstore-akv + - kmprovider-akv trustPolicyDoc: version: "1.0" trustPolicies: diff --git a/test/bats/tests/config/config_v1beta1_verifier_notation_kmprovider.yaml b/test/bats/tests/config/config_v1beta1_verifier_notation_kmprovider.yaml new file mode 100644 index 000000000..c90ccc5fd --- /dev/null +++ b/test/bats/tests/config/config_v1beta1_verifier_notation_kmprovider.yaml @@ -0,0 +1,23 @@ +apiVersion: config.ratify.deislabs.io/v1beta1 +kind: Verifier +metadata: + name: verifier-notation +spec: + name: notation + artifactTypes: application/vnd.cncf.notary.signature + parameters: + verificationCertStores: + certs: + - keymanagementprovider-inline + trustPolicyDoc: + version: "1.0" + trustPolicies: + - name: default + registryScopes: + - "*" + signatureVerification: + level: strict + trustStores: + - ca:certs + trustedIdentities: + - "*" From 6d495420f14967ab9206f80da6dd87e155e8d0fe Mon Sep 17 00:00:00 2001 From: akashsinghal Date: Sat, 16 Mar 2024 01:12:39 +0000 Subject: [PATCH 02/10] address comments --- errors/errors.go | 7 +++++++ pkg/controllers/certificatestore_controller.go | 3 ++- pkg/controllers/keymanagementprovider_controller.go | 3 ++- test/bats/azure-test.bats | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/errors/errors.go b/errors/errors.go index 8f98e6def..6aba5e34a 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -118,4 +118,11 @@ var ( Message: "data encoding failure", Description: "Failed to encode data. Please verify the encoding data.", }) + + // ErrorCodeKeyManagementConflict is returned when key management provider and certificate store are configured together. + ErrorCodeKeyManagementConflict = Register("errcode", ErrorDescriptor{ + Value: "KEY_MANAGEMENT_CONFLICT", + Message: "key management provider and certificate store cannot be configured together", + Description: "Key management provider and certificate store cannot be configured together. Please remove one of them.", + }) ) diff --git a/pkg/controllers/certificatestore_controller.go b/pkg/controllers/certificatestore_controller.go index 69fef7b6d..affa093e8 100644 --- a/pkg/controllers/certificatestore_controller.go +++ b/pkg/controllers/certificatestore_controller.go @@ -25,6 +25,7 @@ import ( _ "github.com/deislabs/ratify/pkg/certificateprovider/inline" // register inline certificate provider "github.com/deislabs/ratify/pkg/utils" + re "github.com/deislabs/ratify/errors" "github.com/sirupsen/logrus" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -87,7 +88,7 @@ func (r *CertificateStoreReconciler) Reconcile(ctx context.Context, req ctrl.Req } if len(keyManagementProviderList.Items) > 0 { - err := fmt.Errorf("key management provider already exists: key management provider and certificate store cannot be configured together") + err := re.ErrorCodeKeyManagementConflict.WithComponentType(re.CertProvider).WithPluginName(resource).WithDetail("key management provider already exists") logger.Error(err) writeCertStoreStatus(ctx, r, certStore, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) // Note: for backwards compatibility in upgrade scenarios, we are not returning an error here diff --git a/pkg/controllers/keymanagementprovider_controller.go b/pkg/controllers/keymanagementprovider_controller.go index 424f21364..b00905281 100644 --- a/pkg/controllers/keymanagementprovider_controller.go +++ b/pkg/controllers/keymanagementprovider_controller.go @@ -32,6 +32,7 @@ import ( configv1beta1 "github.com/deislabs/ratify/api/v1beta1" c "github.com/deislabs/ratify/config" + re "github.com/deislabs/ratify/errors" "github.com/deislabs/ratify/pkg/keymanagementprovider" "github.com/deislabs/ratify/pkg/keymanagementprovider/config" "github.com/deislabs/ratify/pkg/keymanagementprovider/factory" @@ -78,7 +79,7 @@ func (r *KeyManagementProviderReconciler) Reconcile(ctx context.Context, req ctr } // if certificate store is configured, return error. Only one of certificate store and key management provider can be configured if len(certificateStoreList.Items) > 0 { - err := fmt.Errorf("certificate store already exists: key management provider and certificate store cannot be configured together") + err := re.ErrorCodeKeyManagementConflict.WithComponentType(re.KeyManagementProvider).WithPluginName(resource).WithDetail("certificate store already exists") logger.Error(err) writeKMProviderStatus(ctx, r, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) // Note: for backwards compatibility in upgrade scenarios, we are not returning an error here diff --git a/test/bats/azure-test.bats b/test/bats/azure-test.bats index 9e151f11f..b638b3716 100644 --- a/test/bats/azure-test.bats +++ b/test/bats/azure-test.bats @@ -67,7 +67,7 @@ SLEEP_TIME=1 cat ~/.config/notation/truststore/x509/ca/leaf-test/leaf.crt | sed 's/^/ /g' >>./test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml run kubectl apply -f ./test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml assert_success - sed -i '10,$d' ./test/bats/tests/config/config_v1beta1_certstore_inline.yaml + sed -i '10,$d' ./test/bats/tests/config/config_v1beta1_keymanagementprovider_inline.yaml # configure the notation verifier to use the inline key management provider run kubectl apply -f ./test/bats/tests/config/config_v1beta1_verifier_notation_kmprovider.yaml From 297f3886670b1cf90f9729223fc8de0f56d16bd2 Mon Sep 17 00:00:00 2001 From: akashsinghal Date: Wed, 20 Mar 2024 18:11:11 +0000 Subject: [PATCH 03/10] update --- dev.helmfile.yaml | 1 + pkg/controllers/certificatestore_controller.go | 15 --------------- .../keymanagementprovider_controller.go | 10 +++++----- .../keymanagementprovider_controller_test.go | 2 +- 4 files changed, 7 insertions(+), 21 deletions(-) diff --git a/dev.helmfile.yaml b/dev.helmfile.yaml index e68c77d94..4fd0eb057 100644 --- a/dev.helmfile.yaml +++ b/dev.helmfile.yaml @@ -61,6 +61,7 @@ releases: - "verifiers.config.ratify.deislabs.io" - "certificatestores.config.ratify.deislabs.io" - "policies.config.ratify.deislabs.io" + - "keymanagementproviders.config.ratify.deislabs.io" - events: ["postuninstall"] showlogs: true command: "kubectl" diff --git a/pkg/controllers/certificatestore_controller.go b/pkg/controllers/certificatestore_controller.go index affa093e8..8615b3b79 100644 --- a/pkg/controllers/certificatestore_controller.go +++ b/pkg/controllers/certificatestore_controller.go @@ -25,7 +25,6 @@ import ( _ "github.com/deislabs/ratify/pkg/certificateprovider/inline" // register inline certificate provider "github.com/deislabs/ratify/pkg/utils" - re "github.com/deislabs/ratify/errors" "github.com/sirupsen/logrus" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -80,20 +79,6 @@ func (r *CertificateStoreReconciler) Reconcile(ctx context.Context, req ctrl.Req lastFetchedTime := metav1.Now() isFetchSuccessful := false - // ensure that certificate store and key management provider are not configured together - var keyManagementProviderList configv1beta1.KeyManagementProviderList - if err := r.List(ctx, &keyManagementProviderList); err != nil { - logger.Error(err, "unable to list key management providers") - return ctrl.Result{}, err - } - - if len(keyManagementProviderList.Items) > 0 { - err := re.ErrorCodeKeyManagementConflict.WithComponentType(re.CertProvider).WithPluginName(resource).WithDetail("key management provider already exists") - logger.Error(err) - writeCertStoreStatus(ctx, r, certStore, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) - // Note: for backwards compatibility in upgrade scenarios, we are not returning an error here - } - // get cert provider attributes attributes, err := getCertStoreConfig(certStore.Spec) diff --git a/pkg/controllers/keymanagementprovider_controller.go b/pkg/controllers/keymanagementprovider_controller.go index b00905281..c20502f63 100644 --- a/pkg/controllers/keymanagementprovider_controller.go +++ b/pkg/controllers/keymanagementprovider_controller.go @@ -80,12 +80,12 @@ func (r *KeyManagementProviderReconciler) Reconcile(ctx context.Context, req ctr // if certificate store is configured, return error. Only one of certificate store and key management provider can be configured if len(certificateStoreList.Items) > 0 { err := re.ErrorCodeKeyManagementConflict.WithComponentType(re.KeyManagementProvider).WithPluginName(resource).WithDetail("certificate store already exists") + // Note: for backwards compatibility in upgrade scenarios, Ratify will only log an error. + // In v2.0.0 or later, Ratify will return an error to the user. logger.Error(err) - writeKMProviderStatus(ctx, r, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) - // Note: for backwards compatibility in upgrade scenarios, we are not returning an error here } - provider, err := specToKeyManagementProviderProvider(keyManagementProvider.Spec) + provider, err := specToKeyManagementProvider(keyManagementProvider.Spec) if err != nil { writeKMProviderStatus(ctx, r, &keyManagementProvider, logger, isFetchSuccessful, err.Error(), lastFetchedTime, nil) return ctrl.Result{}, err @@ -119,8 +119,8 @@ func (r *KeyManagementProviderReconciler) SetupWithManager(mgr ctrl.Manager) err Complete(r) } -// specToKeyManagementProviderProvider creates KeyManagementProviderProvider from KeyManagementProviderSpec config -func specToKeyManagementProviderProvider(spec configv1beta1.KeyManagementProviderSpec) (keymanagementprovider.KeyManagementProvider, error) { +// specToKeyManagementProvider creates KeyManagementProviderProvider from KeyManagementProviderSpec config +func specToKeyManagementProvider(spec configv1beta1.KeyManagementProviderSpec) (keymanagementprovider.KeyManagementProvider, error) { kmProviderConfig, err := rawToKeyManagementProviderConfig(spec.Parameters.Raw, spec.Type) if err != nil { return nil, fmt.Errorf("failed to parse key management provider config: %w", err) diff --git a/pkg/controllers/keymanagementprovider_controller_test.go b/pkg/controllers/keymanagementprovider_controller_test.go index 039d825fb..cb746f416 100644 --- a/pkg/controllers/keymanagementprovider_controller_test.go +++ b/pkg/controllers/keymanagementprovider_controller_test.go @@ -209,7 +209,7 @@ func TestSpecToKeyManagementProviderProvider(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - _, err := specToKeyManagementProviderProvider(tc.spec) + _, err := specToKeyManagementProvider(tc.spec) if tc.expectErr != (err != nil) { t.Fatalf("Expected error to be %t, got %t", tc.expectErr, err != nil) } From 28c0c0f6592e1a18721003d92d98f1e910115875 Mon Sep 17 00:00:00 2001 From: akashsinghal Date: Wed, 20 Mar 2024 21:03:03 +0000 Subject: [PATCH 04/10] add test --- .../keymanagementprovider_controller.go | 2 +- test/bats/base-test.bats | 38 ++++++++++++++++++- ...nfig_v1beta1_certstore_inline_invalid.yaml | 28 ++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 test/bats/tests/config/config_v1beta1_certstore_inline_invalid.yaml diff --git a/pkg/controllers/keymanagementprovider_controller.go b/pkg/controllers/keymanagementprovider_controller.go index c20502f63..6d2c6f15e 100644 --- a/pkg/controllers/keymanagementprovider_controller.go +++ b/pkg/controllers/keymanagementprovider_controller.go @@ -72,6 +72,7 @@ func (r *KeyManagementProviderReconciler) Reconcile(ctx context.Context, req ctr isFetchSuccessful := false // get certificate store list to check if certificate store is configured + // TODO: remove check in v2.0.0+ var certificateStoreList configv1beta1.CertificateStoreList if err := r.List(ctx, &certificateStoreList); err != nil { logger.Error(err, "unable to list certificate stores") @@ -81,7 +82,6 @@ func (r *KeyManagementProviderReconciler) Reconcile(ctx context.Context, req ctr if len(certificateStoreList.Items) > 0 { err := re.ErrorCodeKeyManagementConflict.WithComponentType(re.KeyManagementProvider).WithPluginName(resource).WithDetail("certificate store already exists") // Note: for backwards compatibility in upgrade scenarios, Ratify will only log an error. - // In v2.0.0 or later, Ratify will return an error to the user. logger.Error(err) } diff --git a/test/bats/base-test.bats b/test/bats/base-test.bats index 66cbc7ab7..72936ef5c 100644 --- a/test/bats/base-test.bats +++ b/test/bats/base-test.bats @@ -260,7 +260,7 @@ RATIFY_NAMESPACE=gatekeeper-system wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete certificatestores.config.ratify.deislabs.io/certstore-inline --namespace ${RATIFY_NAMESPACE} --ignore-not-found=true' wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod demo-alternate --namespace default --force --ignore-not-found=true' - # restore the original key management provider provider + # restore the original key management provider wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl apply -f kmprovider_staging.yaml' # restore the original notation verifier for other tests wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl apply -f ./config/samples/config_v1beta1_verifier_notation.yaml' @@ -298,7 +298,7 @@ RATIFY_NAMESPACE=gatekeeper-system assert_success } -@test "validate inline key management provider provider" { +@test "validate inline key management provider" { teardown() { wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete keymanagementproviders.config.ratify.deislabs.io/keymanagementprovider-inline --namespace ${RATIFY_NAMESPACE} --ignore-not-found=true' wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod demo-alternate --namespace default --force --ignore-not-found=true' @@ -335,6 +335,40 @@ RATIFY_NAMESPACE=gatekeeper-system assert_success } +@test "validate inline key management provider with inline certificate store" { + # this test validates that if a key management provider and certificate store are both configured with the same name, + # the certificate store will take precedence and continue to work as expected + teardown() { + echo "cleaning up" + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod demo --namespace default --force --ignore-not-found=true' + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod demo1 --namespace default --force --ignore-not-found=true' + } + # configure the default template/constraint + run kubectl apply -f ./library/default/template.yaml + assert_success + run kubectl apply -f ./library/default/samples/constraint.yaml + assert_success + + # validate key management provider status property shows success + run bash -c "kubectl get keymanagementproviders.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -n ${RATIFY_NAMESPACE} -o yaml | grep 'issuccess: true'" + assert_success + run kubectl run demo --namespace default --image=registry:5000/notation:signed + assert_success + + sleep 10 + + # apply an invalid cert in an inline certificate store + run kubectl apply -f ./test/bats/tests/config/config_v1beta1_certstore_inline_invalid.yaml + assert_success + # validate key management provider status property shows success + run bash -c "kubectl get certificatestores.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -n ${RATIFY_NAMESPACE} -o yaml | grep 'issuccess: true'" + assert_success + # verification should fail as the certificate store will take precedence over the key management provider + run kubectl run demo1 --namespace default --image=registry:5000/notation:signed + assert_failure + +} + @test "validate K8s secrets ORAS auth provider" { teardown() { echo "cleaning up" diff --git a/test/bats/tests/config/config_v1beta1_certstore_inline_invalid.yaml b/test/bats/tests/config/config_v1beta1_certstore_inline_invalid.yaml new file mode 100644 index 000000000..0b6abcf96 --- /dev/null +++ b/test/bats/tests/config/config_v1beta1_certstore_inline_invalid.yaml @@ -0,0 +1,28 @@ +apiVersion: config.ratify.deislabs.io/v1beta1 +kind: CertificateStore +metadata: + name: ratify-notation-inline-cert-0 +spec: + provider: inline + parameters: + value: | + -----BEGIN CERTIFICATE----- + MIIDWDCCAkCgAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzEL + MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEb + MBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMCAXDTIyMTIwMjA4MDg0NFoYDzIx + MjIxMjAzMDgwODQ0WjBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNV + BAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5l + dHdvcmtzLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnoskJWB0 + ZsYcfbTvCYQMLqWaB/yN3Jf7Ryxvndrij83fWEQPBQJi8Mk8SpNqm2x9uP3gsQDc + L/73a0p6/D+hza2jQQVhebe/oB0LJtUoD5LXlJ83UQdZETLMYAzeBNcBR4kMecrY + CnE6yjHeiEWdAH+U7Mt39zJh+9lGIcbk0aUE5UOp8o3t5RWFDcl9hQ7QOXROwmpO + thLUIiY/bcPpsg/2nH1nzFjqiBef3sgopFCTgtJ7qF8B83Xy/+hJ5vD29xsbSwuB + 3iLE7qLxu2NxdIa4oL0Y2QKMh/getjI0xnvwAmPkFiFbzC7LFdDfd6+gA5GpUXxL + u6UmwucAgiljGQIDAQABoycwJTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYI + KwYBBQUHAwMwDQYJKoZIhvcNAQELBQADggEBAFvRW/mGjnnMNFKJc/e3o/+yiJor + dcrq/1UzyD7eNmOaASXz8rrrFT/6/TBXExPuB2OIf9OgRJFfPGLxmzCwVgaWQbK0 + VfTN4MQzRrSwPmNYsBAAwLxXbarYlMbm4DEmdJGyVikq08T2dZI51GC/YXEwzlnv + ldN0dBflb/FKkY5rAp0JgpHLGKeStxFvB62noBjWfrm7ShCf9gkn1CjmgvP/sYK0 + pJgA1FHPd6EeB6yRBpLV4EJgQYUJoOpbHz+us62jKj5fAXsX052LPmk9ArmP0uJ1 + CJLNdj+aShCs4paSWOObDmIyXHwCx3MxCvYsFk/Wsnwura6jGC+cNsjzSz5= + -----END CERTIFICATE----- From 7442562136fe6650d8dae7d3c956a6c6abd90fd1 Mon Sep 17 00:00:00 2001 From: akashsinghal Date: Wed, 20 Mar 2024 21:21:18 +0000 Subject: [PATCH 05/10] small fix --- test/bats/base-test.bats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/bats/base-test.bats b/test/bats/base-test.bats index 72936ef5c..e89bc7e5a 100644 --- a/test/bats/base-test.bats +++ b/test/bats/base-test.bats @@ -358,7 +358,7 @@ RATIFY_NAMESPACE=gatekeeper-system sleep 10 # apply an invalid cert in an inline certificate store - run kubectl apply -f ./test/bats/tests/config/config_v1beta1_certstore_inline_invalid.yaml + run kubectl apply -f ./test/bats/tests/config/config_v1beta1_certstore_inline_invalid.yaml -n ${RATIFY_NAMESPACE} assert_success # validate key management provider status property shows success run bash -c "kubectl get certificatestores.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -n ${RATIFY_NAMESPACE} -o yaml | grep 'issuccess: true'" From e951674b5c5bd2971ab2c1bae4cb328062802a4d Mon Sep 17 00:00:00 2001 From: akashsinghal Date: Wed, 20 Mar 2024 21:35:52 +0000 Subject: [PATCH 06/10] fix --- test/bats/base-test.bats | 1 + 1 file changed, 1 insertion(+) diff --git a/test/bats/base-test.bats b/test/bats/base-test.bats index e89bc7e5a..7316684b3 100644 --- a/test/bats/base-test.bats +++ b/test/bats/base-test.bats @@ -342,6 +342,7 @@ RATIFY_NAMESPACE=gatekeeper-system echo "cleaning up" wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod demo --namespace default --force --ignore-not-found=true' wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod demo1 --namespace default --force --ignore-not-found=true' + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete certificatestores.config.ratify.deislabs.io/ratify-notation-inline-cert-0 --namespace ${RATIFY_NAMESPACE} --ignore-not-found=true' } # configure the default template/constraint run kubectl apply -f ./library/default/template.yaml From 877b850d9fc443ad4a0314d6a5fe465f10c46b66 Mon Sep 17 00:00:00 2001 From: akashsinghal Date: Fri, 22 Mar 2024 18:12:05 +0000 Subject: [PATCH 07/10] update error message --- errors/errors.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/errors/errors.go b/errors/errors.go index 6aba5e34a..75ccbc784 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -122,7 +122,7 @@ var ( // ErrorCodeKeyManagementConflict is returned when key management provider and certificate store are configured together. ErrorCodeKeyManagementConflict = Register("errcode", ErrorDescriptor{ Value: "KEY_MANAGEMENT_CONFLICT", - Message: "key management provider and certificate store cannot be configured together", - Description: "Key management provider and certificate store cannot be configured together. Please remove one of them.", + Message: "key management provider and certificate store should not be configured together", + Description: "Key management provider and certificate store should not be configured together. Please migrate to key management provider and delete certificate store.", }) ) From 0cefe86f19e12e88e3f156c5f6a2e3bc54e1e2bd Mon Sep 17 00:00:00 2001 From: akashsinghal Date: Tue, 26 Mar 2024 01:23:26 +0000 Subject: [PATCH 08/10] update behavior so KMP is favored over CertificateStore --- pkg/verifier/notation/truststore.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/verifier/notation/truststore.go b/pkg/verifier/notation/truststore.go index 883879604..0acb7d4ad 100644 --- a/pkg/verifier/notation/truststore.go +++ b/pkg/verifier/notation/truststore.go @@ -56,13 +56,13 @@ func (s trustStore) getCertificatesInternal(ctx context.Context, namedStore stri if certGroup := s.certStores[namedStore]; len(certGroup) > 0 { for _, certStore := range certGroup { logger.GetLogger(ctx, logOpt).Debugf("truststore getting certStore %v", certStore) - result := certificatesMap[certStore] + result := keymanagementprovider.FlattenKMPMap(keymanagementprovider.GetCertificatesFromMap(certStore)) + // notation verifier does not consider specific named/versioned certificates within a key management provider resource if len(result) == 0 { - // check key management provider if certificate store does not have certificates. - // NOTE: certificate store and key management provider cannot be configured together. - // This will be enforced by the controller/CLI - result = keymanagementprovider.FlattenKMPMap(keymanagementprovider.GetCertificatesFromMap(certStore)) - // notation verifier does not consider specific named/versioned certificates within a key management provider resource + // check certificate store if key management provider does not have certificates. + // NOTE: certificate store and key management provider should not be configured together. + // User will be warned by the controller/CLI + result = certificatesMap[certStore] if len(result) == 0 { logger.GetLogger(ctx, logOpt).Warnf("no certificate fetched for certStore %+v", certStore) } From 9ee49f97bdd3fa79eb4adfa9c456b236647d17f9 Mon Sep 17 00:00:00 2001 From: akashsinghal Date: Tue, 26 Mar 2024 01:39:42 +0000 Subject: [PATCH 09/10] add logging for missing certs in KMP --- pkg/verifier/notation/truststore.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/verifier/notation/truststore.go b/pkg/verifier/notation/truststore.go index 0acb7d4ad..cf7373cef 100644 --- a/pkg/verifier/notation/truststore.go +++ b/pkg/verifier/notation/truststore.go @@ -59,12 +59,13 @@ func (s trustStore) getCertificatesInternal(ctx context.Context, namedStore stri result := keymanagementprovider.FlattenKMPMap(keymanagementprovider.GetCertificatesFromMap(certStore)) // notation verifier does not consider specific named/versioned certificates within a key management provider resource if len(result) == 0 { + logger.GetLogger(ctx, logOpt).Warnf("no certificate fetched for Key Management Provider %+v", certStore) // check certificate store if key management provider does not have certificates. // NOTE: certificate store and key management provider should not be configured together. // User will be warned by the controller/CLI result = certificatesMap[certStore] if len(result) == 0 { - logger.GetLogger(ctx, logOpt).Warnf("no certificate fetched for certStore %+v", certStore) + logger.GetLogger(ctx, logOpt).Warnf("no certificate fetched for Certificate Store %+v", certStore) } } certs = append(certs, result...) From 5da61c36c2979bdc2d41de4a5a4d5786714d3b0a Mon Sep 17 00:00:00 2001 From: akashsinghal Date: Tue, 26 Mar 2024 02:00:18 +0000 Subject: [PATCH 10/10] fix failing test --- test/bats/base-test.bats | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/bats/base-test.bats b/test/bats/base-test.bats index 7316684b3..53c79f5dd 100644 --- a/test/bats/base-test.bats +++ b/test/bats/base-test.bats @@ -337,7 +337,7 @@ RATIFY_NAMESPACE=gatekeeper-system @test "validate inline key management provider with inline certificate store" { # this test validates that if a key management provider and certificate store are both configured with the same name, - # the certificate store will take precedence and continue to work as expected + # the key management provider will take precedence and continue to work as expected teardown() { echo "cleaning up" wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod demo --namespace default --force --ignore-not-found=true' @@ -364,9 +364,9 @@ RATIFY_NAMESPACE=gatekeeper-system # validate key management provider status property shows success run bash -c "kubectl get certificatestores.config.ratify.deislabs.io/ratify-notation-inline-cert-0 -n ${RATIFY_NAMESPACE} -o yaml | grep 'issuccess: true'" assert_success - # verification should fail as the certificate store will take precedence over the key management provider + # verification should succeed as the existing KMP will take precedence over the new certificate store run kubectl run demo1 --namespace default --image=registry:5000/notation:signed - assert_failure + assert_success }