Repository: deislabs/ratify
Pull request #575 (2023-01-25 22:03:43)

feat: Certificate Store CRD
# Description

## What this PR does / why we need it:
- Adding CRD definition and controller for certificate store
- bringing back main.go to work around kubebuilder's error about missing file
- Updating notary getCertificate method to load certificate from verificationCertStores property
- adding doc to explain the new property
 
 
## Which issue(s) this PR fixes *(optional, using `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when the PR gets merged)*:
Fixes #

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

# How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Please also list any relevant details for your test configuration

- Valid Scenarios:
1.  notary successfully validated a image using a verification certs fetched from azure key vault
2.  CertStore Create/Update/Deletion and errors were handled correctly
3.  CertStore CRD containing multiple certs
4.  NotaryVerifier CRD containing multiple named store
 
- Invalid Scenarios:
6. validated notary returned failure when verification certs were invalid


# Checklist:

- [ ] Does the affected code have corresponding tests?
- [ ] Are the changes documented, not just with inline documentation, but also with conceptual documentation such as an overview of a new feature, or task-based documentation like a tutorial? Consider if this change should be announced on your project blog.
- [ ] Does this introduce breaking changes that would require an announcement or bumping the major version?
- [ ] Do all new files have appropriate license header?



Patch:
diff --git a/PROJECT b/PROJECT
index b662049..9ff0e08 100644
--- a/PROJECT
+++ b/PROJECT
@@ -22,4 +22,13 @@ resources:
   kind: Store
   path: github.com/deislabs/ratify/api/v1alpha1
   version: v1alpha1
+- api:
+    crdVersion: v1
+    namespaced: true
+  controller: true
+  domain: ratify.deislabs.io
+  group: config
+  kind: CertificateStore
+  path: github.com/deislabs/ratify/api/v1alpha1
+  version: v1alpha1
 version: "3"
diff --git a/api/v1alpha1/store_types.go b/api/v1alpha1/store_types.go
index 3650a06..065bd0f 100644
--- a/api/v1alpha1/store_types.go
+++ b/api/v1alpha1/store_types.go
@@ -25,7 +25,7 @@ import (
 type StoreSpec struct {
 	// Important: Run "make" to regenerate code after modifying this file
 
-	// Name of the verifier
+	// Name of the store
 	Name string `json:"name,omitempty"`
 	// Plugin path, optional
 	Address string `json:"address,omitempty"`
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index c4ab93a..d99c32a 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -25,6 +25,96 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CertificateStore) DeepCopyInto(out *CertificateStore) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	out.Status = in.Status
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateStore.
+func (in *CertificateStore) DeepCopy() *CertificateStore {
+	if in == nil {
+		return nil
+	}
+	out := new(CertificateStore)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *CertificateStore) 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 *CertificateStoreList) DeepCopyInto(out *CertificateStoreList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]CertificateStore, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateStoreList.
+func (in *CertificateStoreList) DeepCopy() *CertificateStoreList {
+	if in == nil {
+		return nil
+	}
+	out := new(CertificateStoreList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *CertificateStoreList) 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 *CertificateStoreSpec) DeepCopyInto(out *CertificateStoreSpec) {
+	*out = *in
+	in.Parameters.DeepCopyInto(&out.Parameters)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateStoreSpec.
+func (in *CertificateStoreSpec) DeepCopy() *CertificateStoreSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(CertificateStoreSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CertificateStoreStatus) DeepCopyInto(out *CertificateStoreStatus) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateStoreStatus.
+func (in *CertificateStoreStatus) DeepCopy() *CertificateStoreStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(CertificateStoreStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Store) DeepCopyInto(out *Store) {
 	*out = *in
diff --git a/charts/ratify/templates/_helpers.tpl b/charts/ratify/templates/_helpers.tpl
index afa96fb..5f13cac 100644
--- a/charts/ratify/templates/_helpers.tpl
+++ b/charts/ratify/templates/_helpers.tpl
@@ -5,12 +5,6 @@ Expand the name of the chart.
 {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
 {{- end }}
 
-{{/*
-Define secret provider class name.
-*/}}
-{{- define "ratify.akv.secretProviderClassName" -}}
-{{ include "ratify.fullname" . }}-akv-secret-provider
-{{- end }}
 
 {{- define "ratify.podLabels" -}}
 {{- if .Values.podLabels }}
diff --git a/charts/ratify/templates/akv-certificate-provider.yaml b/charts/ratify/templates/akv-certificate-provider.yaml
new file mode 100644
index 0000000..29ae13a
--- /dev/null
+++ b/charts/ratify/templates/akv-certificate-provider.yaml
@@ -0,0 +1,22 @@
+{{- if .Values.akvCertConfig.enabled }}
+apiVersion: config.ratify.deislabs.io/v1alpha1
+kind: CertificateStore
+metadata:
+  name: certstore-akv
+spec:
+  provider: azurekeyvault
+  parameters:
+    vaultURI: {{ required "vaultName must be provided when AKV cert config is enabled" .Values.akvCertConfig.vaultURI  }}
+    certificates:  |
+      array:
+        - |
+          certificateName: {{ required "cert1Name must be provided when AKV cert config is enabled" .Values.akvCertConfig.cert1Name  }}
+          certificateVersion: {{ .Values.akvCertConfig.cert1Version  }}       
+        {{- if .Values.akvCertConfig.cert2Name }}
+        - |
+          certificateName: {{ .Values.akvCertConfig.cert2Name  }}
+          certificateVersion: {{ .Values.akvCertConfig.cert2Version  }}        
+        {{ 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-secret-provider.yaml b/charts/ratify/templates/akv-secret-provider.yaml
deleted file mode 100644
index 3e0569d..0000000
--- a/charts/ratify/templates/akv-secret-provider.yaml
+++ /dev/null
@@ -1,32 +0,0 @@
-{{- if .Values.akvCertConfig.enabled }}
-apiVersion: secrets-store.csi.x-k8s.io/v1
-kind: SecretProviderClass
-metadata:
-  name: {{ include "ratify.akv.secretProviderClassName" . }}
-spec:
-  provider: azure
-  parameters:
-    keyvaultName: {{ required "vaultName must be provided when AKV cert config is enabled" .Values.akvCertConfig.vaultName  }}
-    objects:  |
-      array:
-        - |
-          objectName: {{ required "cert1Name must be provided when AKV cert config is enabled" .Values.akvCertConfig.cert1Name  }}
-          objectType: cert
-          objectAlias: ratify-test.crt
-        {{- if .Values.akvCertConfig.cert2Name }}
-        - |
-          objectName: {{ .Values.akvCertConfig.cert2Name  }}
-          objectType: cert
-          objectAlias: ratify-test2.crt
-        {{ end }}
-    tenantId: {{ required "tenantId must be provided when AKV cert config is enabled" .Values.akvCertConfig.tenantId  }}
-    {{- if $.Values.oras.authProviders.azureManagedIdentityEnabled }}
-    useVMManagedIdentity: "true" 
-    userAssignedIdentityID: {{ $.Values.azureManagedIdentity.clientId }}
-    {{- end }}
-    {{- if $.Values.oras.authProviders.azureWorkloadIdentityEnabled }}
-    usePodIdentity: "true"
-    useVMManagedIdentity: "false" 
-    clientID: {{ required "clientID must be provided when use workload identity in akv" .Values.azureWorkloadIdentity.clientId  }}
-    {{- end }}
-{{ end }}
\ No newline at end of file
diff --git a/charts/ratify/templates/deployment.yaml b/charts/ratify/templates/deployment.yaml
index 7389d52..3420d0b 100644
--- a/charts/ratify/templates/deployment.yaml
+++ b/charts/ratify/templates/deployment.yaml
@@ -51,13 +51,11 @@ spec:
           ports:
             - containerPort: 6001
           volumeMounts:
-            - mountPath: "/usr/local/ratify-certs/notary/truststore"
-            {{- if .Values.akvCertConfig.enabled }}
-              name: cert-from-akv
-            {{- else }}
+            {{- if eq .Values.akvCertConfig.enabled false }}      
+            - mountPath: "/usr/local/ratify-certs/notary/truststore"                    
               name: notary-certs
-            {{- end }}
               readOnly: true
+            {{- end }}              
             {{- if .Values.cosign.enabled }}
             - mountPath: "/usr/local/ratify-certs/cosign"
               name: cosign-certs
@@ -132,15 +130,7 @@ spec:
             items:
               - key: .dockerconfigjson
                 path: config.json
-        {{- end }}
-        {{- if .Values.akvCertConfig.enabled }}
-        - name: cert-from-akv
-          csi:
-            driver: secrets-store.csi.k8s.io
-            readOnly: true
-            volumeAttributes:
-              secretProviderClass: "{{ include "ratify.akv.secretProviderClassName" . }}"
-        {{- end }}
+        {{- end }}        
         - name: config
           configMap:
             name: {{ include "ratify.fullname" . }}-configuration
diff --git a/charts/ratify/templates/ratify_manager-role-clusterrole.yaml b/charts/ratify/templates/ratify_manager-role-clusterrole.yaml
index 8e182a7..f428bc4 100644
--- a/charts/ratify/templates/ratify_manager-role-clusterrole.yaml
+++ b/charts/ratify/templates/ratify_manager-role-clusterrole.yaml
@@ -57,4 +57,30 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - certificatestores
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - certificatestores/finalizers
+  verbs:
+  - update
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - certificatestores/status
+  verbs:
+  - get
+  - patch
+  - update
   {{- end }}
diff --git a/charts/ratify/templates/verifier.yaml b/charts/ratify/templates/verifier.yaml
index 16409ed..dd8468e 100644
--- a/charts/ratify/templates/verifier.yaml
+++ b/charts/ratify/templates/verifier.yaml
@@ -6,8 +6,14 @@ spec:
   name: notaryv2
   artifactTypes: application/vnd.cncf.notary.signature
   parameters:
+    {{- if .Values.akvCertConfig.enabled }}
+    verificationCertStores:
+      certs:
+        - certstore-akv
+    {{- else }}
     verificationCerts:
-    - /usr/local/ratify-certs/notary/truststore
+      - /usr/local/ratify-certs/notary/truststore
+    {{- end }}    
     trustPolicyDoc:
       version: "1.0"
       trustPolicies:
diff --git a/charts/ratify/values.yaml b/charts/ratify/values.yaml
index f63b4e2..71b9846 100644
--- a/charts/ratify/values.yaml
+++ b/charts/ratify/values.yaml
@@ -62,7 +62,9 @@ akvCertConfig:
   enabled: false
   vaultName:
   cert1Name:
+  cert1Version:
   cert2Name:
+  cert2Version:  
   tenantId:
 
 oras:
diff --git a/config/crd/bases/config.ratify.deislabs.io_stores.yaml b/config/crd/bases/config.ratify.deislabs.io_stores.yaml
index bc484f7..f0476b0 100644
--- a/config/crd/bases/config.ratify.deislabs.io_stores.yaml
+++ b/config/crd/bases/config.ratify.deislabs.io_stores.yaml
@@ -39,7 +39,7 @@ spec:
                 description: Plugin path, optional
                 type: string
               name:
-                description: Name of the verifier
+                description: Name of the store
                 type: string
               parameters:
                 description: Parameters of the store
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index ba983ce..397505d 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -4,6 +4,7 @@
 resources:
 - bases/config.ratify.deislabs.io_verifiers.yaml
 - bases/config.ratify.deislabs.io_stores.yaml
+- bases/config.ratify.deislabs.io_certificatestores.yaml
 #+kubebuilder:scaffold:crdkustomizeresource
 
 patchesStrategicMerge:
@@ -11,12 +12,14 @@ patchesStrategicMerge:
 # patches here are for enabling the conversion webhook for each CRD
 #- patches/webhook_in_verifiers.yaml
 #- patches/webhook_in_stores.yaml
+#- patches/webhook_in_certificatestores.yaml
 #+kubebuilder:scaffold:crdkustomizewebhookpatch
 
 # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
 # patches here are for enabling the CA injection for each CRD
 #- patches/cainjection_in_verifiers.yaml
 #- patches/cainjection_in_stores.yaml
+#- patches/cainjection_in_certificatestores.yaml
 #+kubebuilder:scaffold:crdkustomizecainjectionpatch
 
 # the following config is for teaching kustomize how to do kustomization for CRDs.
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 9f2e72a..918ca05 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -5,6 +5,32 @@ metadata:
   creationTimestamp: null
   name: manager-role
 rules:
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - certificatestores
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - certificatestores/finalizers
+  verbs:
+  - update
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - certificatestores/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - config.ratify.deislabs.io
   resources:
diff --git a/config/samples/config_v1alpha1_certstore_akv.yaml b/config/samples/config_v1alpha1_certstore_akv.yaml
new file mode 100644
index 0000000..f4f6ef7
--- /dev/null
+++ b/config/samples/config_v1alpha1_certstore_akv.yaml
@@ -0,0 +1,16 @@
+apiVersion: config.ratify.deislabs.io/v1alpha1
+kind: CertificateStore
+metadata:
+  name: certstore-akv
+spec:
+  provider: azurekeyvault
+  parameters:
+    vaultURI: https://yourkeyvault.vault.azure.net/
+    certificates:  |
+      array:
+        - |
+          certificateName: yourCertName
+          #Optional, fetch latest version if empty 
+          certificateVersion: yourCertVersion 
+    tenantID:
+    clientID: 
diff --git a/config/samples/config_v1alpha1_verifier_notary_certstore.yaml b/config/samples/config_v1alpha1_verifier_notary_certstore.yaml
new file mode 100644
index 0000000..65f409a
--- /dev/null
+++ b/config/samples/config_v1alpha1_verifier_notary_certstore.yaml
@@ -0,0 +1,27 @@
+apiVersion: config.ratify.deislabs.io/v1alpha1
+kind: Verifier
+metadata:
+  name: verifier-notary
+spec:
+  name: notaryv2
+  artifactTypes: application/vnd.cncf.notary.signature
+  parameters:
+    verificationCertStores:
+      certs:
+          - certstore-akv
+          - certstore-akv1
+      certs1:
+          - certstore-akv2
+          - certstore-akv3
+    trustPolicyDoc:
+      version: "1.0"
+      trustPolicies:
+        - name: default
+          registryScopes:
+            - "*"
+          signatureVerification:
+            level: strict
+          trustStores:
+            - ca:certs
+          trustedIdentities:
+            - "*"
diff --git a/docs/developer/verifier.md b/docs/developer/verifier.md
index 82e07b4..f972fb3 100644
--- a/docs/developer/verifier.md
+++ b/docs/developer/verifier.md
@@ -254,3 +254,52 @@ policy:
       ]
     }
 ```
+### Section 6: Built in verifiers
+
+#### NotaryV2
+NotaryV2 is a built in verifier to Ratify. Notary v2 currently supports X.509 based PKI and identities, and uses a trust store and trust policy to determine if a signed artifact is considered authentic.
+There are two ways to configure verification certificates:
+
+1. verificationCerts:  
+notary verifier will load all certificates from path specified in this array  
+
+2. verificationCertStores:  
+
+A [certificate store](../../config/samples/config_v1alpha1_certstore_akv.yaml) resource defines the list of certificate to fetch from a provider. It is recommended to pin to a specific certificate version, on certificate rotation, customer should update the custom resource to specify the latest version.
+
+ `CertificateStore` is only available in K8 runtime, VerificationCertStores supersedes verificationCerts.
+In the following example, the verifier's configuration references 4 `CertificateStore`, certStore-akv, certStore-akv1, certStore-akv2 and certStore-akv3.
+
+verificationCertStores property defines a collection of cert store objects. [Trust policy](https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md) is a policy language that indicates which identities are trusted to produce artifacts. The following example shows a generic and permissive policy. Here, ca:certs is the only trust store specified and the certs suffix corresponds to the certs certification collection listed in the verificationCertStores section.   
+
+A sample notary verifier with verificationCertStores defined:
+```json=
+apiVersion: config.ratify.deislabs.io/v1alpha1
+kind: Verifier
+metadata:
+  name: verifier-notary
+spec:
+  name: notaryv2
+  artifactTypes: application/vnd.cncf.notary.signature
+  parameters:
+    verificationCertStores:
+      certs:
+          - certStore-akv
+          - certStore-akv1
+      certs1:
+          - certStore-akv2
+          - certStore-akv3
+    trustPolicyDoc:
+      version: "1.0"
+      trustPolicies:
+        - name: default
+          registryScopes:
+            - "*"
+          signatureVerification:
+            level: strict
+          trustStores:
+            - ca:certs
+          trustedIdentities:
+            - "*"
+
+```
diff --git a/go.mod b/go.mod
index cc75d74..03c7112 100644
--- a/go.mod
+++ b/go.mod
@@ -22,6 +22,8 @@ require (
 	github.com/hashicorp/go-retryablehttp v0.7.2
 	github.com/notaryproject/notation-core-go v1.0.0-rc.1
 	github.com/notaryproject/notation-go v1.0.0-rc.1
+	github.com/onsi/ginkgo v1.16.5
+	github.com/onsi/gomega v1.19.0
 	github.com/open-policy-agent/frameworks/constraint v0.0.0-20220627162905-95c012350402
 	github.com/opencontainers/go-digest v1.0.0
 	github.com/opencontainers/image-spec v1.1.0-rc2
@@ -50,6 +52,12 @@ require (
 	github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
 	github.com/go-ldap/ldap/v3 v3.4.4 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+	github.com/nxadm/tail v1.4.8 // indirect
+	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
+)
+
+require (
+
 	bitbucket.org/creachadair/shell v0.0.7 // indirect
 	cloud.google.com/go/compute v1.12.1 // indirect
 	cloud.google.com/go/compute/metadata v0.2.1 // indirect
diff --git a/go.sum b/go.sum
index aea3657..a5957ee 100644
--- a/go.sum
+++ b/go.sum
@@ -757,6 +757,7 @@ github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9k
 github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
+github.com/onsi/ginkgo/v2 v2.1.3 h1:e/3Cwtogj0HA+25nMP1jCMDIf8RtRYbGwGGuBIFztkc=
 github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
 github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
diff --git a/pkg/certificateprovider/azurekeyvault/provider.go b/pkg/certificateprovider/azurekeyvault/provider.go
index 6e7c6ce..f683b50 100644
--- a/pkg/certificateprovider/azurekeyvault/provider.go
+++ b/pkg/certificateprovider/azurekeyvault/provider.go
@@ -25,7 +25,7 @@ import (
 	"strings"
 
 	"github.com/deislabs/ratify/pkg/certificateprovider/azurekeyvault/types"
-	"github.com/deislabs/ratify/pkg/common"
+	"github.com/sirupsen/logrus"
 
 	kv "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
 	"github.com/Azure/go-autorest/autorest/azure"
@@ -66,13 +66,13 @@ func GetCertificates(ctx context.Context, attrib map[string]string) ([]types.Cer
 		return nil, fmt.Errorf("certificates is not set")
 	}
 
-	common.LogDebug("certificates string defined in ratify certStore class, certificates %v", certificatesStrings)
+	logrus.Debugf("certificates string defined in ratify certStore class, certificates %v", certificatesStrings)
 
 	objects, err := types.GetCertificatesArray(certificatesStrings)
 	if err != nil {
 		return nil, fmt.Errorf("failed to yaml unmarshal objects, error: %w", err)
 	}
-	common.LogDebug("unmarshaled objects yaml, objectsArray %v", objects.Array)
+	logrus.Debugf("unmarshaled objects yaml, objectsArray %v", objects.Array)
 
 	keyVaultCerts := []types.KeyVaultCertificate{}
 	for i, object := range objects.Array {
@@ -86,13 +86,13 @@ func GetCertificates(ctx context.Context, attrib map[string]string) ([]types.Cer
 		keyVaultCerts = append(keyVaultCerts, keyVaultCert)
 	}
 
-	common.LogDebug("unmarshaled %v key vault objects, keyVaultObjects: %v", len(keyVaultCerts), keyVaultCerts)
+	logrus.Debugf("unmarshaled %v key vault objects, keyVaultObjects: %v", len(keyVaultCerts), keyVaultCerts)
 
 	if len(keyVaultCerts) == 0 {
 		return nil, fmt.Errorf("no keyvault certificate configured")
 	}
 
-	common.LogDebug("vaultURI %s", keyvaultUri)
+	logrus.Debugf("vaultURI %s", keyvaultUri)
 
 	kvClient, err := initializeKvClient(ctx, azureCloudEnv.KeyVaultEndpoint, tenantID, workloadIdentityClientID)
 	if err != nil {
@@ -101,7 +101,7 @@ func GetCertificates(ctx context.Context, attrib map[string]string) ([]types.Cer
 
 	certs := []types.Certificate{}
 	for _, keyVaultCert := range keyVaultCerts {
-		common.LogDebug("fetching object from key vault, certName %v,  keyvault %v", keyVaultCert.CertificateName, keyvaultUri)
+		logrus.Debugf("fetching object from key vault, certName %v,  keyvault %v", keyVaultCert.CertificateName, keyvaultUri)
 
 		// fetch the object from Key Vault
 		result, err := getCertificate(ctx, kvClient, keyvaultUri, keyVaultCert)
@@ -118,7 +118,7 @@ func GetCertificates(ctx context.Context, attrib map[string]string) ([]types.Cer
 		}
 
 		certs = append(certs, cert)
-		common.LogDebug("added certificates %v to response", cert.CertificateName)
+		logrus.Debugf("added certificates %v to response", cert.CertificateName)
 	}
 	return certs, nil
 }
diff --git a/pkg/certificateprovider/certificate_provider.go b/pkg/certificateprovider/certificate_provider.go
new file mode 100644
index 0000000..fbcd482
--- /dev/null
+++ b/pkg/certificateprovider/certificate_provider.go
@@ -0,0 +1,16 @@
+package certificateprovider
+
+import (
+	"context"
+
+	"github.com/deislabs/ratify/pkg/certificateprovider/azurekeyvault/types"
+)
+
+// This is a map containing Cert store configuration including name, tenantID, and cert object information
+type CertStoreConfig map[string]string
+
+// CertificateProvider is an interface that defines methods to be implemented by a each certificate provider
+type CertificateProvider interface {
+	// Returns an array of certificates based on certificate properties defined in attrib map
+	GetCertificatesContent(ctx context.Context, attrib map[string]string) ([]types.Certificate, error)
+}
diff --git a/pkg/common/logging.go b/pkg/common/logging.go
index e6d8948..0af37db 100644
--- a/pkg/common/logging.go
+++ b/pkg/common/logging.go
@@ -43,10 +43,3 @@ func SetLoggingLevel(level string, logger *logrus.Logger) {
 	logger.Infof("Setting log level to %s", logrusLevel)
 	logger.SetLevel(logrusLevel)
 }
-
-// logDebug only if log level is set to debug
-func LogDebug(format string, args ...interface{}) {
-	if logrus.IsLevelEnabled(logrus.DebugLevel) {
-		logrus.Debugf(format, args...)
-	}
-}
diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go
index f4c060b..7fa4bc9 100644
--- a/pkg/manager/manager.go
+++ b/pkg/manager/manager.go
@@ -176,6 +176,13 @@ func StartManager() {
 		setupLog.Error(err, "unable to create controller", "controller", "Store")
 		os.Exit(1)
 	}
+	if err = (&controllers.CertificateStoreReconciler{
+		Client: mgr.GetClient(),
+		Scheme: mgr.GetScheme(),
+	}).SetupWithManager(mgr); err != nil {
+		setupLog.Error(err, "unable to create controller", "controller", "Certificate Store")
+		os.Exit(1)
+	}
 	//+kubebuilder:scaffold:builder
 
 	if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
diff --git a/pkg/verifier/notaryv2/notaryv2.go b/pkg/verifier/notaryv2/notaryv2.go
index 4f4befa..8482be3 100644
--- a/pkg/verifier/notaryv2/notaryv2.go
+++ b/pkg/verifier/notaryv2/notaryv2.go
@@ -26,6 +26,9 @@ import (
 	ratifyconfig "github.com/deislabs/ratify/config"
 	"github.com/deislabs/ratify/pkg/common"
 	"github.com/deislabs/ratify/pkg/homedir"
+	"github.com/sirupsen/logrus"
+
+	"github.com/deislabs/ratify/pkg/controllers"
 	"github.com/deislabs/ratify/pkg/ocispecs"
 	"github.com/deislabs/ratify/pkg/referrerstore"
 	"github.com/deislabs/ratify/pkg/utils"
@@ -54,7 +57,8 @@ type NotaryV2VerifierConfig struct {
 
 	// VerificationCerts is array of directories containing certificates.
 	VerificationCerts []string `json:"verificationCerts"`
-
+	// VerificationCerts is map defining which keyvault certificates belong to which trust store
+	VerificationCertStores map[string][]string `json:"verificationCertStores"`
 	// TrustPolicyDoc represents a trustpolicy.json document. Reference: https://pkg.go.dev/github.com/notaryproject/notation-go@v0.12.0-beta.1.0.20221125022016-ab113ebd2a6c/verifier/trustpolicy#Document
 	TrustPolicyDoc trustpolicy.Document `json:"trustPolicyDoc"`
 }
@@ -65,7 +69,8 @@ type notaryV2Verifier struct {
 }
 
 type trustStore struct {
-	certPaths []string
+	certPaths  []string
+	certStores map[string][]string
 }
 
 type notaryv2VerifierFactory struct{}
@@ -78,14 +83,34 @@ func init() {
 // Note: this api gets invoked when Ratify calls verify API, so the certificates
 // will be loaded for each signature verification.
 func (s trustStore) GetCertificates(ctx context.Context, storeType truststore.Type, namedStore string) ([]*x509.Certificate, error) {
+	return s.getCertificatesInternal(ctx, storeType, namedStore, controllers.GetCertificatesMap())
+}
+
+func (s trustStore) getCertificatesInternal(ctx context.Context, storeType truststore.Type, namedStore string, certificatesMap map[string][]*x509.Certificate) ([]*x509.Certificate, error) {
 	certs := make([]*x509.Certificate, 0)
-	for _, path := range s.certPaths {
-		bundledCerts, err := utils.GetCertificatesFromPath(path)
-		if err != nil {
-			return nil, err
+
+	// certs configured for this namedStore overrides cert path
+	if certGroup := s.certStores[namedStore]; len(certGroup) > 0 {
+		for _, certStore := range certGroup {
+			result := certificatesMap[certStore]
+			if len(result) == 0 {
+				logrus.Warnf("no certificate fetched for certStore %+v", certStore)
+			}
+			certs = append(certs, result...)
+		}
+		if len(certs) == 0 {
+			return certs, fmt.Errorf("unable to fetch certificates for namedStore: %+v", namedStore)
+		}
+	} else {
+		for _, path := range s.certPaths {
+			bundledCerts, err := utils.GetCertificatesFromPath(path)
+			if err != nil {
+				return nil, err
+			}
+			certs = append(certs, bundledCerts...)
 		}
-		certs = append(certs, bundledCerts...)
 	}
+
 	return certs, nil
 }
 
@@ -166,7 +191,8 @@ func (v *notaryV2Verifier) Verify(ctx context.Context,
 
 func getVerifierService(conf *NotaryV2VerifierConfig) (notation.Verifier, error) {
 	store := &trustStore{
-		certPaths: conf.VerificationCerts,
+		certPaths:  conf.VerificationCerts,
+		certStores: conf.VerificationCertStores,
 	}
 
 	return notaryVerifier.New(&conf.TrustPolicyDoc, store, nil)
@@ -195,7 +221,6 @@ func parseVerifierConfig(verifierConfig config.VerifierConfig) (*NotaryV2Verifie
 
 	defaultCertsDir := paths.Join(homedir.Get(), ratifyconfig.ConfigFileDir, defaultCertPath)
 	conf.VerificationCerts = append(conf.VerificationCerts, defaultCertsDir)
-
 	return conf, nil
 }
 


Test patch:
diff --git a/api/v1alpha1/certificatestore_types.go b/api/v1alpha1/certificatestore_types.go
new file mode 100644
index 0000000..6876187
--- /dev/null
+++ b/api/v1alpha1/certificatestore_types.go
@@ -0,0 +1,66 @@
+/*
+Copyright 2022.
+
+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 v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.
+
+// CertificateStoreSpec defines the desired state of CertificateStore
+type CertificateStoreSpec struct {
+	// Important: Run "make" to regenerate code after modifying this file
+
+	// Name of the certificate store provider
+	Provider string `json:"provider,omitempty"`
+
+	// +kubebuilder:pruning:PreserveUnknownFields
+	// Parameters of the certificate store
+	Parameters runtime.RawExtension `json:"parameters,omitempty"`
+}
+
+// CertificateStoreStatus defines the observed state of CertificateStore
+type CertificateStoreStatus struct {
+	// Important: Run "make" to regenerate code after modifying this file
+}
+
+//+kubebuilder:object:root=true
+//+kubebuilder:subresource:status
+
+// CertificateStore is the Schema for the certificatestores API
+type CertificateStore struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   CertificateStoreSpec   `json:"spec,omitempty"`
+	Status CertificateStoreStatus `json:"status,omitempty"`
+}
+
+//+kubebuilder:object:root=true
+
+// CertificateStoreList contains a list of CertificateStore
+type CertificateStoreList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []CertificateStore `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&CertificateStore{}, &CertificateStoreList{})
+}
diff --git a/charts/ratify/crds/certificatestore-customresourcedefinition.yaml b/charts/ratify/crds/certificatestore-customresourcedefinition.yaml
new file mode 100644
index 0000000..c6963fd
--- /dev/null
+++ b/charts/ratify/crds/certificatestore-customresourcedefinition.yaml
@@ -0,0 +1,50 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.9.2
+  creationTimestamp: null
+  name: certificatestores.config.ratify.deislabs.io
+spec:
+  group: config.ratify.deislabs.io
+  names:
+    kind: CertificateStore
+    listKind: CertificateStoreList
+    plural: certificatestores
+    singular: certificatestore
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: CertificateStore is the Schema for the certificatestores 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: CertificateStoreSpec defines the desired state of CertificateStore
+            properties:
+              parameters:
+                description: Parameters of the store
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              provider:
+                description: Name of the of the certificate store provider
+                type: string
+            type: object
+          status:
+            description: CertificateStoreStatus defines the observed state of CertificateStore
+            type: object
+        type: object
+    served: true
+    storage: true
\ No newline at end of file
diff --git a/config/crd/bases/config.ratify.deislabs.io_certificatestores.yaml b/config/crd/bases/config.ratify.deislabs.io_certificatestores.yaml
new file mode 100644
index 0000000..2a77700
--- /dev/null
+++ b/config/crd/bases/config.ratify.deislabs.io_certificatestores.yaml
@@ -0,0 +1,51 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.9.2
+  creationTimestamp: null
+  name: certificatestores.config.ratify.deislabs.io
+spec:
+  group: config.ratify.deislabs.io
+  names:
+    kind: CertificateStore
+    listKind: CertificateStoreList
+    plural: certificatestores
+    singular: certificatestore
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: CertificateStore is the Schema for the certificatestores 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: CertificateStoreSpec defines the desired state of CertificateStore
+            properties:
+              parameters:
+                description: Parameters of the store
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              provider:
+                description: Name of the certificate store provider
+                type: string
+            type: object
+          status:
+            description: CertificateStoreStatus defines the observed state of CertificateStore
+            type: object
+        type: object
+    served: true
+    storage: true
diff --git a/config/crd/patches/cainjection_in_certificatestores.yaml b/config/crd/patches/cainjection_in_certificatestores.yaml
new file mode 100644
index 0000000..f79a022
--- /dev/null
+++ b/config/crd/patches/cainjection_in_certificatestores.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: certificatestores.config.ratify.deislabs.io
diff --git a/config/crd/patches/webhook_in_certificatestores.yaml b/config/crd/patches/webhook_in_certificatestores.yaml
new file mode 100644
index 0000000..214ee04
--- /dev/null
+++ b/config/crd/patches/webhook_in_certificatestores.yaml
@@ -0,0 +1,16 @@
+# The following patch enables a conversion webhook for the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: certificatestores.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/certificatestore_editor_role.yaml b/config/rbac/certificatestore_editor_role.yaml
new file mode 100644
index 0000000..3262307
--- /dev/null
+++ b/config/rbac/certificatestore_editor_role.yaml
@@ -0,0 +1,24 @@
+# permissions for end users to edit certificatestores.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: certificatestore-editor-role
+rules:
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - certificatestores
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - certificatestores/status
+  verbs:
+  - get
diff --git a/config/rbac/certificatestore_viewer_role.yaml b/config/rbac/certificatestore_viewer_role.yaml
new file mode 100644
index 0000000..d207851
--- /dev/null
+++ b/config/rbac/certificatestore_viewer_role.yaml
@@ -0,0 +1,20 @@
+# permissions for end users to view certificatestores.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: certificatestore-viewer-role
+rules:
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - certificatestores
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - certificatestores/status
+  verbs:
+  - get
diff --git a/config/samples/config_v1alpha1_certificatestore.yaml b/config/samples/config_v1alpha1_certificatestore.yaml
deleted file mode 100644
index 008df93..0000000
--- a/config/samples/config_v1alpha1_certificatestore.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-apiVersion: config.ratify.deislabs.io/v1alpha1
-kind: CertificateStore
-metadata:
-  name: certificatestore-sample
-spec:
-  # TODO(user): Add fields here
diff --git a/pkg/controllers/certificatestore_controller.go b/pkg/controllers/certificatestore_controller.go
new file mode 100644
index 0000000..af29526
--- /dev/null
+++ b/pkg/controllers/certificatestore_controller.go
@@ -0,0 +1,153 @@
+/*
+Copyright 2022.
+
+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"
+	"crypto/x509"
+	"encoding/json"
+	"encoding/pem"
+	"fmt"
+
+	configv1alpha1 "github.com/deislabs/ratify/api/v1alpha1"
+	"github.com/deislabs/ratify/pkg/certificateprovider/azurekeyvault"
+	"github.com/deislabs/ratify/pkg/certificateprovider/azurekeyvault/types"
+
+	"github.com/sirupsen/logrus"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// CertificateStoreReconciler reconciles a CertificateStore object
+type CertificateStoreReconciler struct {
+	client.Client
+	Scheme *runtime.Scheme
+}
+
+var (
+	// a map between CertificateStore name to array of x509 certificates
+	certificatesMap = map[string][]*x509.Certificate{}
+)
+
+//+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
+
+// Reconcile is part of the main kubernetes reconciliation loop which aims to
+// move the current state of the cluster closer to the desired state.
+// the CertificateStore object against the actual cluster state, and then
+// perform operations to make the cluster state reflect the state specified by
+// the user.
+//
+// For more details, check Reconcile and its Result here:
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile
+func (r *CertificateStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	logger := logrus.WithContext(ctx)
+
+	var resource = req.Name
+	var certStore configv1alpha1.CertificateStore
+
+	logger.Infof("reconciling certificate store '%v'", resource)
+
+	if err := r.Get(ctx, req.NamespacedName, &certStore); err != nil {
+		if apierrors.IsNotFound(err) {
+			logger.Infof("deletion detected, removing certificate store %v", req.Name)
+			delete(certificatesMap, resource)
+		} else {
+			logger.Error(err, "unable to fetch certificate store")
+		}
+
+		return ctrl.Result{}, client.IgnoreNotFound(err)
+	}
+
+	// get cert provider attributes
+	attributes, err := getCertStoreConfig(certStore.Spec)
+	if err != nil {
+		return ctrl.Result{}, err
+	}
+
+	// fetch certificates based on the provider
+	switch certStore.Spec.Provider {
+	case "azurekeyvault":
+		contents, err := azurekeyvault.GetCertificates(ctx, attributes)
+		if err != nil {
+			return ctrl.Result{}, fmt.Errorf("Error fetching certificates in store %v with azure key vault provider, error: %w", resource, err)
+		}
+
+		certificates, err := byteToCerts(contents)
+		if err != nil {
+			return ctrl.Result{}, fmt.Errorf("Failed to parse x509 certificate for certificate store %v, error: %w", resource, err)
+		}
+		certificatesMap[resource] = certificates
+		logger.Infof("%v certificates fetched for certificate store %v", len(certificates), resource)
+	default:
+
+		return ctrl.Result{}, fmt.Errorf("Unknown provider value %v defined in certificate store %v", certStore.Spec.Provider, resource)
+	}
+
+	// returning empty result and no error to indicate we’ve successfully reconciled this object
+	return ctrl.Result{}, nil
+}
+
+// returns the internal certificate map
+func GetCertificatesMap() map[string][]*x509.Certificate {
+	return certificatesMap
+}
+
+// converts array of cert content to x509 certificate
+func byteToCerts(certificates []types.Certificate) ([]*x509.Certificate, error) {
+	r := []*x509.Certificate{}
+	for _, c := range certificates {
+		block, _ := pem.Decode(c.Content)
+		if block == nil {
+			return nil, fmt.Errorf("Failed to decode certificate")
+		}
+
+		cert, err := x509.ParseCertificate(block.Bytes)
+		if err != nil {
+			return nil, err
+		}
+		logrus.Debugf("cert '%v', version '%v' added", c.CertificateName, c.Version)
+		r = append(r, cert)
+	}
+
+	return r, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *CertificateStoreReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&configv1alpha1.CertificateStore{}).
+		Complete(r)
+}
+
+func getCertStoreConfig(spec configv1alpha1.CertificateStoreSpec) (map[string]string, error) {
+	attributes := map[string]string{}
+
+	if string(spec.Parameters.Raw) == "" {
+		return nil, fmt.Errorf("Received empty parameters")
+	}
+
+	if err := json.Unmarshal(spec.Parameters.Raw, &attributes); err != nil {
+		logrus.Error(err, "unable to decode cert store parameters", "Parameters.Raw", spec.Parameters.Raw)
+		return attributes, err
+	}
+
+	return attributes, nil
+}
diff --git a/pkg/controllers/certificatestore_controller_test.go b/pkg/controllers/certificatestore_controller_test.go
new file mode 100644
index 0000000..9e63c09
--- /dev/null
+++ b/pkg/controllers/certificatestore_controller_test.go
@@ -0,0 +1,147 @@
+/*
+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 (
+	"testing"
+
+	configv1alpha1 "github.com/deislabs/ratify/api/v1alpha1"
+	"github.com/deislabs/ratify/pkg/certificateprovider/azurekeyvault/types"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+func TestByteToCerts_ByteArrayToCertificates(t *testing.T) {
+	certString1 := "-----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"
+	certString2 := "-----BEGIN CERTIFICATE-----\nMIIDsDCCApigAwIBAgIQFJMQeqR8TRuHqNu+x0MuEDANBgkqhkiG9w0BAQsFADBa\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+UqMDi1B7T2yzfvXcqQ75FpSRIxzK\n-----END CERTIFICATE-----\n"
+
+	c1 := []byte(certString1)
+	c2 := []byte(certString2)
+
+	certificates := []types.Certificate{}
+	certificates = append(certificates, types.Certificate{Content: c1})
+	certificates = append(certificates, types.Certificate{Content: c2})
+
+	r, err := byteToCerts(certificates)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	expectedLen := 2
+	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)
+	}
+
+	cert2 := r[1]
+	serialNumber2 := cert2.SerialNumber.String()
+
+	expectedserialNumber2 := "27348161789198234828835474579392769552"
+	if serialNumber2 != expectedserialNumber2 {
+		t.Fatalf("unexpected certificate, expected %+v, got %+v", expectedserialNumber2, serialNumber2)
+	}
+}
+
+func TestByteToCerts_FailedToDecode(t *testing.T) {
+	certString := "foo"
+
+	c1 := []byte(certString)
+
+	certificates := []types.Certificate{}
+	certificates = append(certificates, types.Certificate{Content: c1})
+
+	_, err := byteToCerts(certificates)
+	if err == nil {
+		t.Fatalf("bytesToCerts should return an error")
+	}
+
+	expectedError := "Failed to decode certificate"
+	if err.Error() != expectedError {
+		t.Fatalf("unexpected error, expected %+v, got %+v", expectedError, err.Error())
+	}
+}
+
+func TestByteToCerts_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)
+
+	certificates := []types.Certificate{}
+	certificates = append(certificates, types.Certificate{Content: c1})
+
+	_, err := byteToCerts(certificates)
+	if err == nil {
+		t.Fatalf("bytesToCerts should return an error")
+	}
+
+	expectedError := "x509: malformed issuer"
+	if err.Error() != expectedError {
+		t.Fatalf("unexpected error, expected %+v, got %+v", expectedError, err.Error())
+	}
+}
+
+func TestGetCertStoreConfig_ValidConfig(t *testing.T) {
+	var parametersString = "{\"certificates\":\"array:\\n  - |\\n    certificateName: wabbit-networks-io\\n    certificateVersion: 97a1545d893344079ce57699c8810590\\n\",\"clientID\":\"sampleClientID\",\"keyvaultName\":\"sampleKeyVault\",\"tenantID\":\"sampleTenantID\"}"
+	var certStoreParameters = []byte(parametersString)
+
+	spec := configv1alpha1.CertificateStoreSpec{
+		Provider: "azurekeyvault",
+		Parameters: runtime.RawExtension{
+			Raw: certStoreParameters,
+		},
+	}
+
+	result, err := getCertStoreConfig(spec)
+	if err != nil {
+		t.Fatalf("unexpected error %v", err)
+	}
+
+	if len(result) != 4 ||
+		result["clientID"] != "sampleClientID" ||
+		result["tenantID"] != "sampleTenantID" ||
+		result["keyvaultName"] != "sampleKeyVault" ||
+		result["certificates"] != "array:\n  - |\n    certificateName: wabbit-networks-io\n    certificateVersion: 97a1545d893344079ce57699c8810590\n" {
+		t.Fatalf("unexpected value")
+	}
+}
+
+func TestGetCertStoreConfig_EmptyStringError(t *testing.T) {
+	var parametersString = ""
+	var certStoreParameters = []byte(parametersString)
+
+	spec := configv1alpha1.CertificateStoreSpec{
+		Provider: "azurekeyvault",
+		Parameters: runtime.RawExtension{
+			Raw: certStoreParameters,
+		},
+	}
+
+	_, err := getCertStoreConfig(spec)
+	if err == nil {
+		t.Fatalf("Expected error")
+	}
+
+	expectedError := "Received empty parameters"
+	if err.Error() != expectedError {
+		t.Fatalf("Unexpected error, expected %+v, got %+v", expectedError, err.Error())
+	}
+}
diff --git a/pkg/verifier/notaryv2/notaryv2_test.go b/pkg/verifier/notaryv2/notaryv2_test.go
index 18a4d57..2061c27 100644
--- a/pkg/verifier/notaryv2/notaryv2_test.go
+++ b/pkg/verifier/notaryv2/notaryv2_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"crypto/x509"
 	"crypto/x509/pkix"
+	"encoding/pem"
 	"fmt"
 	paths "path/filepath"
 	"reflect"
@@ -18,6 +19,7 @@ import (
 	"github.com/deislabs/ratify/pkg/verifier"
 	sig "github.com/notaryproject/notation-core-go/signature"
 	"github.com/notaryproject/notation-go"
+	"github.com/notaryproject/notation-go/verifier/truststore"
 	"github.com/opencontainers/go-digest"
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
 )
@@ -223,8 +225,29 @@ func TestParseVerifierConfig(t *testing.T) {
 				VerificationCerts: []string{testPath, defaultCertDir},
 			},
 		},
+		{
+			name: "successfully parsed with specified cert stores",
+			configMap: map[string]interface{}{
+				"name":              test,
+				"verificationCerts": []string{testPath},
+				"verificationCertStores": map[string][]string{
+					"certstore1": {"akv1", "akv2"},
+					"certstore2": {"akv3", "akv4"},
+				},
+			},
+			expectErr: false,
+			expect: &NotaryV2VerifierConfig{
+				Name:              test,
+				VerificationCerts: []string{testPath, defaultCertDir},
+				VerificationCertStores: map[string][]string{
+					"certstore1": {"akv1", "akv2"},
+					"certstore2": {"akv3", "akv4"},
+				},
+			},
+		},
 	}
 
+	//TODO add new test for parseVerifierConfig
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			notaryConfig, err := parseVerifierConfig(tt.configMap)
@@ -358,3 +381,63 @@ func TestVerify(t *testing.T) {
 		})
 	}
 }
+
+func TestGetCertificates_EmptyCertMap(t *testing.T) {
+	certStore := map[string][]string{}
+	certStore["store1"] = []string{"kv1"}
+	certStore["store2"] = []string{"kv2"}
+	store := &trustStore{
+		certStores: certStore,
+	}
+
+	certificatesMap := map[string][]*x509.Certificate{}
+	_, err := store.getCertificatesInternal(context.Background(), truststore.TypeCA, "store1", certificatesMap)
+
+	if err == nil {
+		t.Fatalf("error expected if cert map is empty")
+	}
+}
+
+func TestGetCertificates_NamedStore(t *testing.T) {
+	certStore := map[string][]string{}
+	certStore["store1"] = []string{"kv1"}
+	certStore["store2"] = []string{"kv2"}
+
+	store := &trustStore{
+		certStores: certStore,
+	}
+
+	kv1Cert := getCert("-----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")
+	kv2Cert := getCert("-----BEGIN CERTIFICATE-----\nMIIDsDCCApigAwIBAgIQFJMQeqR8TRuHqNu+x0MuEDANBgkqhkiG9w0BAQsFADBa\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+UqMDi1B7T2yzfvXcqQ75FpSRIxzK\n-----END CERTIFICATE-----\n")
+
+	certificatesMap := map[string][]*x509.Certificate{}
+	certificatesMap["kv1"] = []*x509.Certificate{kv1Cert}
+	certificatesMap["kv2"] = []*x509.Certificate{kv2Cert}
+
+	// only the certificate in the specified namedStore should be returned
+	result, _ := store.getCertificatesInternal(context.Background(), truststore.TypeCA, "store1", certificatesMap)
+	expectedLen := 1
+
+	if len(result) != expectedLen {
+		t.Fatalf("unexpected count of certificate, expected %+v, got %+v", expectedLen, len(result))
+	}
+
+	if !reflect.DeepEqual(result[0], kv1Cert) {
+		t.Fatalf("unexpected certificate returned")
+	}
+}
+
+// convert string to a x509 certificate
+func getCert(certString string) *x509.Certificate {
+	block, _ := pem.Decode([]byte(certString))
+	if block == nil {
+		panic("failed to parse certificate PEM")
+	}
+
+	test, err := x509.ParseCertificate(block.Bytes)
+	if err != nil {
+		panic("failed to parse certificate: " + err.Error())
+	}
+
+	return test
+}