Repository: deislabs/ratify
Pull request #709 (2023-03-13 07:27:45)

fix: add cert validation logic to notation TrustStore
# Description

## What this PR does / why we need it:
1. Add a cert validation to trustStore implementation so that we keep only CA certificates and self-signed certificates in the trust store.
2. Move TrustStore implementation out of notaryv2.go.
3. Added more unit tests.
4. Add instructions to update certificateStore.

## 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 https://github.com/deislabs/ratify/issues/694

## Type of change

Please delete options that are not relevant.

- [x] 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

Added unit tests 

# 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?


codecov[bot]: ## [Codecov](https://codecov.io/gh/deislabs/ratify/pull/709?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
Patch coverage: **`72.50`**% and project coverage change: **`+0.24`** :tada:
> Comparison is base [(`9e3a3a0`)](https://codecov.io/gh/deislabs/ratify/commit/9e3a3a0b1388ccbeca713fd8c135798ec77bc7db?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) 49.34% compared to head [(`1b37c36`)](https://codecov.io/gh/deislabs/ratify/pull/709?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) 49.58%.

> :exclamation: Current head 1b37c36 differs from pull request most recent head 47f994f. Consider uploading reports for the commit 47f994f to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #709      +/-   ##
==========================================
+ Coverage   49.34%   49.58%   +0.24%     
==========================================
  Files          56       57       +1     
  Lines        3267     3283      +16     
==========================================
+ Hits         1612     1628      +16     
+ Misses       1481     1479       -2     
- Partials      174      176       +2     
```


| [Impacted Files](https://codecov.io/gh/deislabs/ratify/pull/709?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [pkg/verifier/notaryv2/notaryv2.go](https://codecov.io/gh/deislabs/ratify/pull/709?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL3ZlcmlmaWVyL25vdGFyeXYyL25vdGFyeXYyLmdv) | `86.04% <ø> (+6.95%)` | :arrow_up: |
| [pkg/verifier/notaryv2/truststore.go](https://codecov.io/gh/deislabs/ratify/pull/709?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cGtnL3ZlcmlmaWVyL25vdGFyeXYyL3RydXN0c3RvcmUuZ28=) | `72.50% <72.50%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/deislabs/ratify/pull/709?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).


Patch:
diff --git a/Makefile b/Makefile
index ad574d2..412634e 100644
--- a/Makefile
+++ b/Makefile
@@ -122,7 +122,7 @@ test-e2e:
 
 .PHONY: test-e2e-cli
 
-test-e2e-cli: e2e-dependencies e2e-create-local-registry e2e-notaryv2-setup e2e-cosign-setup e2e-licensechecker-setup e2e-sbom-setup e2e-schemavalidator-setup
+test-e2e-cli: e2e-dependencies e2e-create-local-registry e2e-notaryv2-setup e2e-notation-leaf-cert-setup e2e-cosign-setup e2e-licensechecker-setup e2e-sbom-setup e2e-schemavalidator-setup
 	IS_OCI_1_1=${IS_OCI_1_1} RATIFY_DIR=${INSTALL_DIR} TEST_REGISTRY=${TEST_REGISTRY} ${GITHUB_WORKSPACE}/bin/bats -t ${BATS_CLI_TESTS_FILE}
 
 .PHONY: generate-certs
@@ -240,6 +240,21 @@ e2e-notaryv2-setup:
 		.staging/notaryv2/notation sign --signature-manifest artifact -u ${TEST_REGISTRY_USERNAME} -p ${TEST_REGISTRY_PASSWORD} `docker image inspect ${TEST_REGISTRY}/notation:ociartifact | jq -r .[0].RepoDigests[0]`; \
 	fi
 
+e2e-notation-leaf-cert-setup:
+	mkdir -p .staging/notaryv2/leaf-test
+	mkdir -p ~/.config/notation/truststore/x509/ca/leaf-test
+	./scripts/generate-cert-chain.sh .staging/notaryv2/leaf-test
+	cp .staging/notaryv2/leaf-test/leaf.crt ~/.config/notation/truststore/x509/ca/leaf-test/leaf.crt
+	cp .staging/notaryv2/leaf-test/ca.crt ~/.config/notation/truststore/x509/ca/leaf-test/root.crt
+	cat .staging/notaryv2/leaf-test/ca.crt >> .staging/notaryv2/leaf-test/leaf.crt
+
+	jq '.keys += [{"name":"leaf-test","keyPath":".staging/notaryv2/leaf-test/leaf.key","certPath":".staging/notaryv2/leaf-test/leaf.crt"}]' ~/.config/notation/signingkeys.json > tmp && mv tmp ~/.config/notation/signingkeys.json
+
+	echo 'FROM alpine\nCMD ["echo", "notaryv2 leaf signed image"]' > .staging/notaryv2/Dockerfile
+	docker build --no-cache -t ${TEST_REGISTRY}/notation:leafSigned .staging/notaryv2
+	docker push ${TEST_REGISTRY}/notation:leafSigned
+	.staging/notaryv2/notation sign -u ${TEST_REGISTRY_USERNAME} -p ${TEST_REGISTRY_PASSWORD} --key "leaf-test" `docker image inspect ${TEST_REGISTRY}/notation:leafSigned | jq -r .[0].RepoDigests[0]`
+
 e2e-cosign-setup:
 	rm -rf .staging/cosign
 	mkdir -p .staging/cosign
@@ -390,7 +405,7 @@ e2e-inlinecert-setup:
 	.staging/notaryv2/notation cert generate-test "alternate-cert"
 	.staging/notaryv2/notation sign -u ${TEST_REGISTRY_USERNAME} -p ${TEST_REGISTRY_PASSWORD} --key "alternate-cert" `docker image inspect ${TEST_REGISTRY}/notation:signed-alternate | jq -r .[0].RepoDigests[0]`
 
-e2e-azure-setup: e2e-create-all-image e2e-notaryv2-setup e2e-cosign-setup e2e-licensechecker-setup e2e-sbom-setup e2e-schemavalidator-setup
+e2e-azure-setup: e2e-create-all-image e2e-notaryv2-setup e2e-notation-leaf-cert-setup e2e-cosign-setup e2e-licensechecker-setup e2e-sbom-setup e2e-schemavalidator-setup
 
 e2e-deploy-gatekeeper: e2e-helm-install
 	./.staging/helm/linux-amd64/helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
@@ -403,7 +418,7 @@ e2e-deploy-gatekeeper: e2e-helm-install
     --set mutatingWebhookTimeoutSeconds=2 \
     --set auditInterval=0
 
-e2e-deploy-ratify: e2e-notaryv2-setup e2e-cosign-setup e2e-licensechecker-setup e2e-sbom-setup e2e-schemavalidator-setup e2e-inlinecert-setup
+e2e-deploy-ratify: e2e-notaryv2-setup e2e-notation-leaf-cert-setup e2e-cosign-setup e2e-cosign-setup e2e-licensechecker-setup e2e-sbom-setup e2e-schemavalidator-setup e2e-inlinecert-setup
 	docker build --progress=plain --no-cache -f ./httpserver/Dockerfile -t localbuild:test .
 	kind load docker-image --name kind localbuild:test
 
diff --git a/docs/reference/crds/certificate-stores.md b/docs/reference/crds/certificate-stores.md
index 6489323..e718d16 100644
--- a/docs/reference/crds/certificate-stores.md
+++ b/docs/reference/crds/certificate-stores.md
@@ -6,7 +6,8 @@ View more CRD samples [here](../../../config/samples/). Each provider must speci
 provider: required, name of the certificate store provider
 parameters: required, parameters specific to this certificate store provider
 ```
- 
+
+# Certificate Store Provider
 ## AzureKeyVault Certificate Provider
 See notary integration example [here](../../developer/verifier.md#section-6-built-in-verifiers)
 ```yml
@@ -75,4 +76,20 @@ spec:
 
 | Name        | Required | Description | Default Value |
 | ----------- | -------- | ----------- | ------------- | 
-| value      | yes    |      public certificate content       |   ""            |
\ No newline at end of file
+| value      | yes    |      public certificate content       |   ""            |
+
+# Certificate Specification
+The main use case of certificate store is for notation verifier in Ratify, so users must follow the [TrustStore specification](https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#trust-store) defined by notation.
+
+In brief, users must provide CA certificates or self-signed signing certificates, which means leaf certificates are not allowed to be used. Whatever certificates are provided, Ratify would keep only CA certificates and self-signed certificates. Therefore, if only leaf certificates are provided, Ratify would fail the verification directly since there are no valid certificates.
+
+For `AzureKeyVault Certificate Provider`, users must use the self-signed certificates due to some limitations on the SDK API. If users want to configure a certificate chain from root to leaf, it's recommended to use the `Inline Certificate Provider` instead.
+
+# CRD Resource Create/Update 
+During the CRD creating/updating process, some matters require attention. The CRD operation could be successful even though some invalid values or typos are provided. Examples:
+
+1. Invalid certificate value is provided in inline certificate provider.
+2. Invalid vaultUri/certificateName or typos within them are provided in AKV certificate provider.
+
+However, those invalid values or typos would cause failures while parsing certificates and signature verification in Ratify.
+So it's recommended to check the CRD status once the CRD operation is done.
\ No newline at end of file
diff --git a/pkg/verifier/notaryv2/notaryv2.go b/pkg/verifier/notaryv2/notaryv2.go
index 41c1a7c..a15dbb1 100644
--- a/pkg/verifier/notaryv2/notaryv2.go
+++ b/pkg/verifier/notaryv2/notaryv2.go
@@ -17,7 +17,6 @@ package notaryv2
 
 import (
 	"context"
-	"crypto/x509"
 	"encoding/json"
 	"fmt"
 	paths "path/filepath"
@@ -26,12 +25,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"
 	"github.com/deislabs/ratify/pkg/verifier"
 	"github.com/deislabs/ratify/pkg/verifier/config"
 	"github.com/deislabs/ratify/pkg/verifier/factory"
@@ -41,7 +37,6 @@ import (
 	"github.com/notaryproject/notation-go"
 	notaryVerifier "github.com/notaryproject/notation-go/verifier"
 	"github.com/notaryproject/notation-go/verifier/trustpolicy"
-	"github.com/notaryproject/notation-go/verifier/truststore"
 	oci "github.com/opencontainers/image-spec/specs-go/v1"
 )
 
@@ -68,52 +63,12 @@ type notaryV2Verifier struct {
 	notationVerifier *notation.Verifier
 }
 
-type trustStore struct {
-	certPaths  []string
-	certStores map[string][]string
-}
-
 type notaryv2VerifierFactory struct{}
 
 func init() {
 	factory.Register(verifierName, &notaryv2VerifierFactory{})
 }
 
-// trustStore implements GetCertificates API of X509TrustStore interface: [https://pkg.go.dev/github.com/notaryproject/notation-go@v0.12.0-beta.1.0.20221117143817-2573c88a5f62/verifier/truststore#X509TrustStore]
-// 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)
-
-	// 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...)
-		}
-	}
-
-	return certs, nil
-}
-
 func (f *notaryv2VerifierFactory) Create(version string, verifierConfig config.VerifierConfig) (verifier.ReferenceVerifier, error) {
 	conf, err := parseVerifierConfig(verifierConfig)
 	if err != nil {
diff --git a/pkg/verifier/notaryv2/truststore.go b/pkg/verifier/notaryv2/truststore.go
new file mode 100644
index 0000000..308a7c1
--- /dev/null
+++ b/pkg/verifier/notaryv2/truststore.go
@@ -0,0 +1,91 @@
+/*
+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 notaryv2
+
+import (
+	"context"
+	"crypto/x509"
+	"errors"
+	"fmt"
+
+	"github.com/deislabs/ratify/pkg/controllers"
+	"github.com/deislabs/ratify/pkg/utils"
+	"github.com/notaryproject/notation-go/verifier/truststore"
+	"github.com/sirupsen/logrus"
+)
+
+type trustStore struct {
+	certPaths  []string
+	certStores map[string][]string
+}
+
+// trustStore implements GetCertificates API of X509TrustStore interface: [https://pkg.go.dev/github.com/notaryproject/notation-go@v1.0.0-rc.3/verifier/truststore#X509TrustStore]
+// Note: this api gets invoked when Ratify calls verify API, so the certificates
+// will be loaded for each signature verification.
+// And this API must follow the Notation Trust Store spec: https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#trust-store
+func (s trustStore) GetCertificates(ctx context.Context, storeType truststore.Type, namedStore string) ([]*x509.Certificate, error) {
+	certs, err := s.getCertificatesInternal(ctx, storeType, namedStore, controllers.GetCertificatesMap())
+	if err != nil {
+		return nil, err
+	}
+	return s.filterValidCerts(certs)
+}
+
+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)
+
+	// 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...)
+		}
+	}
+
+	return certs, nil
+}
+
+// filterValidCerts keeps CA certificates and self-signed certs.
+func (s trustStore) filterValidCerts(certs []*x509.Certificate) ([]*x509.Certificate, error) {
+	filteredCerts := make([]*x509.Certificate, 0)
+	for _, cert := range certs {
+		if !cert.IsCA {
+			// check if it's a self-signed certificate.
+			if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil {
+				continue
+			}
+		}
+		filteredCerts = append(filteredCerts, cert)
+	}
+	if len(filteredCerts) == 0 {
+		return nil, errors.New("valid certificates must be provided, only CA certificates or self-signed signing certificates are supported")
+	}
+	return filteredCerts, nil
+}
diff --git a/scripts/generate-cert-chain.sh b/scripts/generate-cert-chain.sh
new file mode 100755
index 0000000..73d0fef
--- /dev/null
+++ b/scripts/generate-cert-chain.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+##--------------------------------------------------------------------
+#
+# 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.
+#
+##--------------------------------------------------------------------
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+CERT_DIR=$1
+
+generate() {
+    # generate CA key and certificate
+    echo "Generating CA key and certificate for ratify..."
+    openssl genrsa -out ca.key 2048
+    openssl req -new -x509 -days 1 -key ca.key -subj "/O=Ratify/CN=Ratify Root CA" -out ca.crt -addext "keyUsage=critical,keyCertSign"
+
+    # generate leaf key and certificate
+    echo "Generating leaf key and certificate for ratify..."
+    openssl genrsa -out leaf.key 2048
+    openssl req -newkey rsa:2048 -nodes -keyout leaf.key -subj "/CN=ratify.default" -out leaf.csr
+    openssl x509 -req -extfile <(printf "basicConstraints=critical,CA:FALSE\nkeyUsage=critical,digitalSignature") -days 365 -in leaf.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out leaf.crt
+}
+
+rm -r ${CERT_DIR} || true
+mkdir -p ${CERT_DIR}
+pushd "${CERT_DIR}"
+generate
+popd


Test patch:
diff --git a/pkg/verifier/notaryv2/notaryv2_test.go b/pkg/verifier/notaryv2/notaryv2_test.go
index 2061c27..6a90a3c 100644
--- a/pkg/verifier/notaryv2/notaryv2_test.go
+++ b/pkg/verifier/notaryv2/notaryv2_test.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"crypto/x509"
 	"crypto/x509/pkix"
-	"encoding/pem"
 	"fmt"
 	paths "path/filepath"
 	"reflect"
@@ -19,7 +18,6 @@ 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"
 )
@@ -382,62 +380,11 @@ 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
+func TestGetNestedReferences(t *testing.T) {
+	verifier := &notaryV2Verifier{}
+	nestedReferences := verifier.GetNestedReferences()
 
-	if len(result) != expectedLen {
-		t.Fatalf("unexpected count of certificate, expected %+v, got %+v", expectedLen, len(result))
+	if len(nestedReferences) != 0 {
+		t.Fatalf("notation signature should not have nested references")
 	}
-
-	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
 }
diff --git a/pkg/verifier/notaryv2/truststore_test.go b/pkg/verifier/notaryv2/truststore_test.go
new file mode 100644
index 0000000..008c14b
--- /dev/null
+++ b/pkg/verifier/notaryv2/truststore_test.go
@@ -0,0 +1,146 @@
+package notaryv2
+
+import (
+	"context"
+	"crypto/x509"
+	"encoding/pem"
+	"os"
+	"reflect"
+	"testing"
+
+	"github.com/notaryproject/notation-go/verifier/truststore"
+)
+
+const (
+	certStr     = "-----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"
+	certStr2    = "-----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"
+	caCertStr   = "-----BEGIN CERTIFICATE-----\nMIIDNTCCAh2gAwIBAgIUbLzOMsPOGflj7TS34tzjs5vBWYIwDQYJKoZIhvcNAQEL\nBQAwKjEPMA0GA1UECgwGUmF0aWZ5MRcwFQYDVQQDDA5SYXRpZnkgUm9vdCBDQTAe\nFw0yMzAzMTAwMTEwMjlaFw0yNDAzMDkwMTEwMjlaMCoxDzANBgNVBAoMBlJhdGlm\neTEXMBUGA1UEAwwOUmF0aWZ5IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB\nDwAwggEKAoIBAQCeNlh95GnkHLBVSCoYmlPztNKw5jwmlZYWLgwZMOK0qKedlsZs\naxbb5YzQlIV/z8D+/DnsZ3hTmokset0hE6JDQ1lw5wjk1I8DijkjwiE3oVH5Kyv/\nPSUtbSP7LmNDG/2vtWBkyTltXf3SfqAazLXVd0IQqGTXie+2SJa9Q6UAZFWKB1t7\n0Js6rDyQULcUMSzvF39QBPHFcd9iuSZLPw9CGG7hHNTlaOQryukr6U9tjY4hLK7e\n1OdgyP17nXELIlL81ngoWufi3rLgePqfkg8GgwnWyFrDS2eZSofFqW0X80gDvEQ+\n5WGj4XklyGGHpwyR4qNMnv5hp7QMtDd62iyPAgMBAAGjUzBRMB0GA1UdDgQWBBQQ\nGxu39HR2ynlSR40ZySohaxKGMDAfBgNVHSMEGDAWgBQQGxu39HR2ynlSR40ZySoh\naxKGMDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB22EixNuBZ\nyeGtrtSpkVPer+nxKU+6upwXmLfTe3ZEbv1NqC2auUvD+EN/86+mJfeXhkdVHHoV\nteuXNVU0oJ4ocRUXkgA/jPUEnjXwYK661/N67mqr5wLHcKIt48yKNdMvfXO6tXSE\nx/xLQlX7v2JehEQtTv7axnYGoHOvKL2H0+lq5VN0rDnB5hb7LyRX9kQq6mszLzG5\nbypfWX1aReWViWU2d6hvVegDYFpRpPXE3o5FGWvrad91RXdSJLsjJGdYJMlUe3xR\n+guQHepJ0iCoreAr+0XkctcV0qlyOiIuikWMdrQiXXTIDCGTRNmD9XCTu5+2tZe+\nW2O8QLkvH3Fv\n-----END CERTIFICATE-----\n"
+	leafCertStr = "-----BEGIN CERTIFICATE-----\nMIIC7jCCAdagAwIBAgIURNiOON+GKbFS8yFxG6aMRoMg29cwDQYJKoZIhvcNAQEL\nBQAwKjEPMA0GA1UECgwGUmF0aWZ5MRcwFQYDVQQDDA5SYXRpZnkgUm9vdCBDQTAe\nFw0yMzAzMTAwMTEwMjlaFw0yNDAzMDkwMTEwMjlaMBkxFzAVBgNVBAMMDnJhdGlm\neS5kZWZhdWx0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwUwGuWJ5\nDwspcL+7K+0XlkQ0g+sbyvfY0j0NdUmzsTPQNxsdUsbgYeidLnp0ruHKuHLq6Y9t\nEHUPF+A4S6lIi5OPhEkVxd/A5kzSX23WocJGmlew+Z/usjQdtiQ4ylYyHoHfPNrf\nrocbY21XQ3x2IM3yIo1QqSHNdCsE0UxsFI3j9XC+saIqrkr+k1SsI2AhhGRjXTke\nPNpOaJ+CRwsGz7PbnsACLbiAdOUJUGRkOlIl/p7hU2IcZUYTTGcKOFXP8DtbUJ+K\nQcBQOsfZyg36jvkpzmw/yAK00Uuc0X+5CaKfDKDw4MXvJFpRvG+Vc0mb5RB1E8py\neA6eXtUrZ5J4hQIDAQABox0wGzAZBgNVHREEEjAQgg5yYXRpZnkuZGVmYXVsdDAN\nBgkqhkiG9w0BAQsFAAOCAQEAHbiuodTJCDpCUu8tNjbww5ebTRznKZGnFmKQs5zU\no8KyCfLhR9/9zetDADwtWCQUvykFuHjx8tj41hALXXXafzkYPeTsfDmEoVWIJMQ1\nHqjbzc6bbxQAY7cC5HqM67fXYjPs1v3Uv3GZhF2EjBMqymKC+lZ/RSfktzN0iADn\nlwG9DrDibD739jBF09b3LHtdV55blN2wyB54DwMl5x0a4+bFYVj7fZzjctG4pH7T\njnBS69oxetPaqcRY7SQljJKaesiqx3CtiwVUpGTBexDtw6OIj9cWiCFT0lS3TfCh\nunfSQvVgezqE7txrFbXDQCgbl1jGagfia2ol7+IbLUR6TQ==\n-----END CERTIFICATE-----\n"
+)
+
+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{}
+	if _, err := store.getCertificatesInternal(context.Background(), truststore.TypeCA, "store1", certificatesMap); 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(certStr)
+	kv2Cert := getCert(certStr2)
+
+	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 !kv1Cert.Equal(result[0]) {
+		t.Fatalf("unexpected certificate returned")
+	}
+}
+
+func TestGetCertificates_certPath(t *testing.T) {
+	// create a temporary certificate file
+	tmpFile, err := os.CreateTemp("", "*.pem")
+	if err != nil {
+		t.Fatalf("failed to create temporary file: %v", err)
+	}
+	if _, err := tmpFile.Write([]byte(certStr)); err != nil {
+		t.Fatalf("failed to write cert: %v", err)
+	}
+
+	trustStore := &trustStore{
+		certPaths: []string{tmpFile.Name()},
+	}
+	certs, err := trustStore.getCertificatesInternal(context.Background(), truststore.TypeCA, "", nil)
+	if err != nil {
+		t.Fatalf("failed to get certs: %v", err)
+	}
+
+	if len(certs) != 1 || !certs[0].Equal(getCert(certStr)) {
+		t.Fatalf("unexpected certificate returned")
+	}
+
+	// remove the temporary certificate file
+	if err := tmpFile.Close(); err != nil {
+		t.Fatalf("failed to close temporary file: %v", err)
+	}
+}
+
+func TestFilterValidCerts(t *testing.T) {
+	trustStore := trustStore{}
+	tests := []struct {
+		name      string
+		certs     []*x509.Certificate
+		expect    []*x509.Certificate
+		expectErr bool
+	}{
+		{
+			name:      "CA cert",
+			certs:     []*x509.Certificate{getCert(caCertStr)},
+			expect:    []*x509.Certificate{getCert(caCertStr)},
+			expectErr: false,
+		},
+		{
+			name:      "self-signed cert",
+			certs:     []*x509.Certificate{getCert(certStr)},
+			expect:    []*x509.Certificate{getCert(certStr)},
+			expectErr: false,
+		},
+		{
+			name:      "invalid cert",
+			certs:     []*x509.Certificate{getCert(leafCertStr)},
+			expect:    nil,
+			expectErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result, err := trustStore.filterValidCerts(tt.certs)
+			if (err != nil) != tt.expectErr {
+				t.Errorf("error = %v, expectErr = %v", err, tt.expectErr)
+			}
+			if !reflect.DeepEqual(result, tt.expect) {
+				t.Errorf("expect %+v, got %+v", tt.expect, result)
+			}
+		})
+	}
+}
+
+// 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
+}
diff --git a/test/bats/azure-test.bats b/test/bats/azure-test.bats
index f60f2de..6e7236b 100644
--- a/test/bats/azure-test.bats
+++ b/test/bats/azure-test.bats
@@ -30,6 +30,49 @@ SLEEP_TIME=1
     assert_success
 }
 
+@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 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'
+
+        # restore the original notary verifier for other tests
+        wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl apply -f ./test/bats/tests/config/config_v1beta1_verifier_notary_akv.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
+
+    # 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
+    assert_success
+    sed -i '9,$d' ./test/bats/tests/config/config_v1beta1_certstore_inline.yaml
+
+    # configure the notary verifier to use the inline certificate store
+    run kubectl apply -f ./test/bats/tests/config/config_v1beta1_verifier_notary.yaml
+    assert_success
+
+    # verify that the image can be run with a root cert
+    run kubectl run demo-leaf --namespace default --image=${TEST_REGISTRY}/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
+    assert_success
+    sed -i '9,$d' ./test/bats/tests/config/config_v1beta1_certstore_inline.yaml
+
+    # wait for the httpserver cache to be invalidated
+    sleep 15
+    # verify that the image cannot be run with a leaf cert
+    run kubectl run demo-leaf2 --namespace default --image=${TEST_REGISTRY}/notation:leafSigned
+    assert_failure
+}
+
 @test "notary test" {
     teardown() {
         echo "cleaning up"
diff --git a/test/bats/cli-test.bats b/test/bats/cli-test.bats
index d3976fd..4fc9db9 100644
--- a/test/bats/cli-test.bats
+++ b/test/bats/cli-test.bats
@@ -16,6 +16,14 @@ load helpers
     assert_cmd_verify_failure
 }
 
+@test "notation verifier leaf cert test" {
+    run bin/ratify verify -c $RATIFY_DIR/config_notation_root_cert.json -s $TEST_REGISTRY/notation:leafSigned
+    assert_cmd_verify_success
+
+    run bin/ratify verify -c $RATIFY_DIR/config_notation_leaf_cert.json -s $TEST_REGISTRY/notation:leafSigned
+    assert_cmd_verify_failure
+}
+
 @test "cosign verifier test" {
     run bin/ratify verify -c $RATIFY_DIR/config.json -s $TEST_REGISTRY/cosign:signed-key
     assert_cmd_verify_success
diff --git a/test/bats/test.bats b/test/bats/test.bats
index a765af8..62c384a 100644
--- a/test/bats/test.bats
+++ b/test/bats/test.bats
@@ -331,44 +331,14 @@ SLEEP_TIME=1
     assert_failure
 
     # add the alternate certificate as an inline certificate store
-    cat <<EOF | kubectl apply -f -
-apiVersion: config.ratify.deislabs.io/v1beta1
-kind: CertificateStore
-metadata:
-  name: certstore-inline
-spec:
-  provider: inline
-  parameters:
-    value: |
-$(cat ~/.config/notation/truststore/x509/ca/alternate-cert/alternate-cert.crt | sed 's/^/      /g')
-EOF
+    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
+    assert_success
+    sed -i '9,$d' ./test/bats/tests/config/config_v1beta1_certstore_inline.yaml
 
     # configure the notary verifier to use the inline certificate store
-    cat <<EOF | kubectl apply -f -
-apiVersion: config.ratify.deislabs.io/v1beta1
-kind: Verifier
-metadata:
-  name: verifier-notary
-spec:
-  name: notaryv2
-  artifactTypes: application/vnd.cncf.notary.signature
-  parameters:
-    verificationCertStores:
-      certs:
-        - certstore-inline
-    trustPolicyDoc:
-      version: "1.0"
-      trustPolicies:
-        - name: default
-          registryScopes:
-            - "*"
-          signatureVerification:
-            level: strict
-          trustStores:
-            - ca:certs
-          trustedIdentities:
-            - "*"
-EOF
+    run kubectl apply -f ./test/bats/tests/config/config_v1beta1_verifier_notary.yaml
+    assert_success
     sleep 10
 
     # verify that the image can now be run
@@ -398,3 +368,46 @@ EOF
     run kubectl run demo1 --namespace default --image=registry:5000/notation:unsigned
     assert_failure
 }
+
+@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 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'
+
+        # restore the original notary verifier for other tests
+        wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl apply -f ./config/samples/config_v1beta1_verifier_notary.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
+
+    # 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
+    assert_success
+    sed -i '9,$d' ./test/bats/tests/config/config_v1beta1_certstore_inline.yaml
+
+    # configure the notary verifier to use the inline certificate store
+    run kubectl apply -f ./test/bats/tests/config/config_v1beta1_verifier_notary.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
+    assert_success
+    sed -i '9,$d' ./test/bats/tests/config/config_v1beta1_certstore_inline.yaml
+
+    # wait for the httpserver cache to be invalidated
+    sleep 15
+    # verify that the image cannot be run with a leaf cert
+    run kubectl run demo-leaf2 --namespace default --image=registry:5000/notation:leafSigned
+    assert_failure
+}
diff --git a/test/bats/tests/config/config_notation_leaf_cert.json b/test/bats/tests/config/config_notation_leaf_cert.json
new file mode 100644
index 0000000..4133bd3
--- /dev/null
+++ b/test/bats/tests/config/config_notation_leaf_cert.json
@@ -0,0 +1,50 @@
+{
+    "store": {
+        "version": "1.0.0",
+        "plugins": [
+            {
+                "name": "oras",
+                "cosignEnabled": true,
+                "useHttp": true
+            }
+        ]
+    },
+    "policy": {
+        "version": "1.0.0",
+        "plugin": {
+            "name": "configPolicy"
+        }
+    },
+    "verifier": {
+        "version": "1.0.0",
+        "plugins": [
+            {
+                "name": "notaryv2",
+                "artifactTypes": "application/vnd.cncf.notary.signature",
+                "verificationCerts": [
+                    "~/.config/notation/truststore/x509/ca/leaf-test/leaf.crt"
+                ],
+                "trustPolicyDoc": {
+                    "version": "1.0",
+                    "trustPolicies": [
+                        {
+                            "name": "default",
+                            "registryScopes": [
+                                "*"
+                            ],
+                            "signatureVerification": {
+                                "level": "strict"
+                            },
+                            "trustStores": [
+                                "ca:certs"
+                            ],
+                            "trustedIdentities": [
+                                "*"
+                            ]
+                        }
+                    ]
+                }
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/test/bats/tests/config/config_notation_root_cert.json b/test/bats/tests/config/config_notation_root_cert.json
new file mode 100644
index 0000000..ac8120a
--- /dev/null
+++ b/test/bats/tests/config/config_notation_root_cert.json
@@ -0,0 +1,50 @@
+{
+    "store": {
+        "version": "1.0.0",
+        "plugins": [
+            {
+                "name": "oras",
+                "cosignEnabled": true,
+                "useHttp": true
+            }
+        ]
+    },
+    "policy": {
+        "version": "1.0.0",
+        "plugin": {
+            "name": "configPolicy"
+        }
+    },
+    "verifier": {
+        "version": "1.0.0",
+        "plugins": [
+            {
+                "name": "notaryv2",
+                "artifactTypes": "application/vnd.cncf.notary.signature",
+                "verificationCerts": [
+                    "~/.config/notation/truststore/x509/ca/leaf-test/root.crt"
+                ],
+                "trustPolicyDoc": {
+                    "version": "1.0",
+                    "trustPolicies": [
+                        {
+                            "name": "default",
+                            "registryScopes": [
+                                "*"
+                            ],
+                            "signatureVerification": {
+                                "level": "strict"
+                            },
+                            "trustStores": [
+                                "ca:certs"
+                            ],
+                            "trustedIdentities": [
+                                "*"
+                            ]
+                        }
+                    ]
+                }
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/test/bats/tests/config/config_v1beta1_certstore_inline.yaml b/test/bats/tests/config/config_v1beta1_certstore_inline.yaml
new file mode 100644
index 0000000..880cbb1
--- /dev/null
+++ b/test/bats/tests/config/config_v1beta1_certstore_inline.yaml
@@ -0,0 +1,8 @@
+apiVersion: config.ratify.deislabs.io/v1beta1
+kind: CertificateStore
+metadata:
+  name: certstore-inline
+spec:
+  provider: inline
+  parameters:
+    value: |
diff --git a/test/bats/tests/config/config_v1beta1_verifier_notary.yaml b/test/bats/tests/config/config_v1beta1_verifier_notary.yaml
new file mode 100644
index 0000000..1d09513
--- /dev/null
+++ b/test/bats/tests/config/config_v1beta1_verifier_notary.yaml
@@ -0,0 +1,23 @@
+apiVersion: config.ratify.deislabs.io/v1beta1
+kind: Verifier
+metadata:
+  name: verifier-notary
+spec:
+  name: notaryv2
+  artifactTypes: application/vnd.cncf.notary.signature
+  parameters:
+    verificationCertStores:
+      certs:
+        - certstore-inline
+    trustPolicyDoc:
+      version: "1.0"
+      trustPolicies:
+        - name: default
+          registryScopes:
+            - "*"
+          signatureVerification:
+            level: strict
+          trustStores:
+            - ca:certs
+          trustedIdentities:
+            - "*"
diff --git a/test/bats/tests/config/config_v1beta1_verifier_notary_akv.yaml b/test/bats/tests/config/config_v1beta1_verifier_notary_akv.yaml
new file mode 100644
index 0000000..d103a10
--- /dev/null
+++ b/test/bats/tests/config/config_v1beta1_verifier_notary_akv.yaml
@@ -0,0 +1,23 @@
+apiVersion: config.ratify.deislabs.io/v1beta1
+kind: Verifier
+metadata:
+  name: verifier-notary
+spec:
+  name: notaryv2
+  artifactTypes: application/vnd.cncf.notary.signature
+  parameters:
+    verificationCertStores:
+      certs:
+        - certstore-akv
+    trustPolicyDoc:
+      version: "1.0"
+      trustPolicies:
+        - name: default
+          registryScopes:
+            - "*"
+          signatureVerification:
+            level: strict
+          trustStores:
+            - ca:certs
+          trustedIdentities:
+            - "*"