Repository: deislabs/ratify
Pull request #531 (2023-01-06 00:26:15)

feat: add ratify azure keyvault provider
# Description
Adding a azure keyvault library for fetching certificates. This provider is based on implementation from azure secret store csi provider at https://github.com/Azure/secrets-store-csi-driver-provider-azure/blob/master/pkg/provider/. Note, Ratify will only supports certificate objectType.

Related design doc at : https://hackmd.io/@susanshi/Skg7vvCDi

In the next PR, we will introduce  Ratify Cert store CRD to initiate requests to fetch certs in memory.

This PR is part1 out of 2 to fix issue #307

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] 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

- Tested in a k8 scenario , we were able to fetch the certificate store in azure keyvault with workload identity

# 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?


susanshi: Hi @aramase , this PR is ready, please review when you get a chance. thanks!
susanshi: @aramase thanks so much for a quick turnaround on reviewing this, i m working on the udpates and will be able to push an update tomorrow. Meanwhile , there are some items that i left comments to continue the discussion, let me know what you think. thanks!
susanshi: Hi @aramase, i believe I have responsed all the comments. Let me know if there anything else i have missed, would like to try to get this merged by end of the week to be on schedule for ratify RC1. ( will  work and publish CRD pr next week). Here is a sample test output for reference. thanks! 

```code
func TestGetCertWithVersion(t *testing.T) {
	attrib := map[string]string{}
	attrib["vaultUri"] = "https://notarycerts.vault.azure.net/"
	attrib["clientID"] = "1c7ac023-5bf6-4916-83f2-96dd203e35a3"
	attrib["cloudName"] = "AzurePublicCloud"
	attrib["tenantID"] = "72f988bf-86f1-41af-91ab-2d7cd011db47"
	attrib["certificates"] = "array:\n- |\n  certificateName: wabbit-networks-io  \n  certificateVersion: \"97a1545d893344079ce57699c8810590\"\n"
	files, err := GetCertificates(context.TODO(), attrib)
	expectedCert := "-----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"
	if err != nil {
		logrus.Infof("err %v", err)
	}
	assert.Equal(t, "wabbit-networks-io", string(files[0].CertificateName))
	assert.Equal(t, expectedCert, string(files[0].Content))
	assert.Equal(t, "97a1545d893344079ce57699c8810590", string(files[0].Version)) // should return the pinned version
}

```

Patch:
diff --git a/go.mod b/go.mod
index 3c35812..cc75d74 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
 	github.com/Azure/azure-sdk-for-go v67.3.0+incompatible
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.4
 	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0
-	github.com/AzureAD/microsoft-authentication-library-for-go v0.5.3
+	github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0
 	github.com/aws/aws-sdk-go-v2 v1.17.3
 	github.com/aws/aws-sdk-go-v2/config v1.18.8
 	github.com/aws/aws-sdk-go-v2/credentials v1.13.8
@@ -43,6 +43,13 @@ require (
 )
 
 require (
+	cloud.google.com/go/compute/metadata v0.2.1 // indirect
+	github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
+	github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
+	github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect
+	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
 	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 117dbc4..aea3657 100644
--- a/go.sum
+++ b/go.sum
@@ -93,6 +93,10 @@ github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSY
 github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
 github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw=
 github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU=
+github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk=
+github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=
+github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac=
+github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=
 github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=
 github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
 github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
@@ -101,6 +105,8 @@ github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft
 github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
 github.com/AzureAD/microsoft-authentication-library-for-go v0.5.3 h1:TsFCaaF5tR4XN8b4zLVl/J4qMb0nf80Q4CXcpXDNJDY=
 github.com/AzureAD/microsoft-authentication-library-for-go v0.5.3/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
+github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0 h1:VgSJlZH5u0k2qxSpqyghcFQKmvYckj46uymKK5XzkBM=
+github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0/go.mod h1:BDJ5qMFKx9DugEg3+uQSDCdbYPr5s9vBTrL9P8TpqOU=
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
diff --git a/pkg/certificateprovider/azurekeyvault/auth.go b/pkg/certificateprovider/azurekeyvault/auth.go
new file mode 100644
index 0000000..0cd98f9
--- /dev/null
+++ b/pkg/certificateprovider/azurekeyvault/auth.go
@@ -0,0 +1,118 @@
+/*
+Copyright The Ratify Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package azurekeyvault
+
+// This class is based on implementation from  azure secret store csi provider
+// Source: https://github.com/Azure/secrets-store-csi-driver-provider-azure/tree/release-1.4/pkg/auth
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/deislabs/ratify/pkg/utils"
+
+	"github.com/Azure/go-autorest/autorest"
+	"github.com/Azure/go-autorest/autorest/adal"
+)
+
+const (
+	// the format for expires_on in UTC with AM/PM
+	expiresOnDateFormatPM = "1/2/2006 15:04:05 PM +00:00"
+	// the format for expires_on in UTC without AM/PM
+	expiresOnDateFormat = "1/2/2006 15:04:05 +00:00"
+
+	tokenTypeBearer = "Bearer"
+	// For Azure AD Workload Identity, the audience recommended for use is
+	// "api://AzureADTokenExchange"
+	DefaultTokenAudience = "api://AzureADTokenExchange" //nolint
+)
+
+// authResult contains the subset of results from token acquisition operation in ConfidentialClientApplication
+// For details see https://aka.ms/msal-net-authenticationresult
+type authResult struct {
+	accessToken    string
+	expiresOn      time.Time
+	grantedScopes  []string
+	declinedScopes []string
+}
+
+func getAuthorizerForWorkloadIdentity(ctx context.Context, tenantID, clientID, resource string) (autorest.Authorizer, error) {
+	scope := resource
+	// .default needs to be added to the scope
+	if !strings.Contains(resource, ".default") {
+		scope = fmt.Sprintf("%s/.default", resource)
+	}
+
+	result, err := utils.GetAADAccessToken(ctx, tenantID, clientID, scope)
+	if err != nil {
+		return nil, fmt.Errorf("failed to acquire token: %w", err)
+	}
+
+	token := adal.Token{
+		AccessToken: result.AccessToken,
+		Resource:    resource,
+		Type:        tokenTypeBearer,
+	}
+	token.ExpiresOn, err = parseExpiresOn(result.ExpiresOn.UTC().Local().Format(expiresOnDateFormat))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse expires_on: %w", err)
+	}
+
+	return autorest.NewBearerAuthorizer(authResult{
+		accessToken:    result.AccessToken,
+		expiresOn:      result.ExpiresOn,
+		grantedScopes:  result.GrantedScopes,
+		declinedScopes: result.DeclinedScopes,
+	}), nil
+}
+
+// OAuthToken implements the OAuthTokenProvider interface.  It returns the current access token.
+func (ar authResult) OAuthToken() string {
+	return ar.accessToken
+}
+
+// Vendored from https://github.com/Azure/go-autorest/blob/79575dd7ba2e88e7ce7ab84e167ec6653dcb70c1/autorest/adal/token.go
+// converts expires_on to the number of seconds
+func parseExpiresOn(s interface{}) (json.Number, error) {
+	// the JSON unmarshaler treats JSON numbers unmarshaled into an interface{} as float64
+	asFloat64, ok := s.(float64)
+	if ok {
+		// this is the number of seconds as int case
+		return json.Number(strconv.FormatInt(int64(asFloat64), 10)), nil
+	}
+	asStr, ok := s.(string)
+	if !ok {
+		return "", fmt.Errorf("unexpected expires_on type %T", s)
+	}
+	// convert the expiration date to the number of seconds from the unix epoch
+	timeToDuration := func(t time.Time) json.Number {
+		return json.Number(strconv.FormatInt(t.UTC().Unix(), 10))
+	}
+	if _, err := json.Number(asStr).Int64(); err == nil {
+		// this is the number of seconds case, no conversion required
+		return json.Number(asStr), nil
+	} else if eo, err := time.Parse(expiresOnDateFormatPM, asStr); err == nil {
+		return timeToDuration(eo), nil
+	} else if eo, err := time.Parse(expiresOnDateFormat, asStr); err == nil {
+		return timeToDuration(eo), nil
+	} else {
+		// unknown format
+		return json.Number(""), err
+	}
+}
diff --git a/pkg/certificateprovider/azurekeyvault/provider.go b/pkg/certificateprovider/azurekeyvault/provider.go
new file mode 100644
index 0000000..6e7c6ce
--- /dev/null
+++ b/pkg/certificateprovider/azurekeyvault/provider.go
@@ -0,0 +1,204 @@
+/*
+Copyright The Ratify Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package azurekeyvault
+
+// This class is based on implementation from  azure secret store csi provider
+// Source: https://github.com/Azure/secrets-store-csi-driver-provider-azure/tree/release-1.4/pkg/provider
+import (
+	"context"
+	"encoding/pem"
+	"fmt"
+	"reflect"
+	"strings"
+
+	"github.com/deislabs/ratify/pkg/certificateprovider/azurekeyvault/types"
+	"github.com/deislabs/ratify/pkg/common"
+
+	kv "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
+	"github.com/Azure/go-autorest/autorest/azure"
+	"github.com/pkg/errors"
+	"gopkg.in/yaml.v2"
+)
+
+type keyvaultObject struct {
+	content string
+	version string
+}
+
+// returns an array of certificates based on certificate properties defined in attrib map
+func GetCertificates(ctx context.Context, attrib map[string]string) ([]types.Certificate, error) {
+	keyvaultUri := types.GetKeyVaultUri(attrib)
+	cloudName := types.GetCloudName(attrib)
+	tenantID := types.GetTenantID(attrib)
+	workloadIdentityClientID := types.GetClientID(attrib)
+
+	if keyvaultUri == "" {
+		return nil, fmt.Errorf("keyvaultUri is not set")
+	}
+	if tenantID == "" {
+		return nil, fmt.Errorf("tenantID is not set")
+	}
+	if workloadIdentityClientID == "" {
+		return nil, fmt.Errorf("clientID is not set")
+	}
+
+	azureCloudEnv, err := parseAzureEnvironment(cloudName)
+	if err != nil {
+		return nil, fmt.Errorf("cloudName %s is not valid, error: %w", cloudName, err)
+	}
+
+	// 1. cleaning up keyvault objects definition
+	certificatesStrings := types.GetCertificates(attrib)
+	if certificatesStrings == "" {
+		return nil, fmt.Errorf("certificates is not set")
+	}
+
+	common.LogDebug("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)
+
+	keyVaultCerts := []types.KeyVaultCertificate{}
+	for i, object := range objects.Array {
+		var keyVaultCert types.KeyVaultCertificate
+		if err = yaml.Unmarshal([]byte(object), &keyVaultCert); err != nil {
+			return nil, fmt.Errorf("unmarshal failed for keyVaultCerts at index %d, error: %w", i, err)
+		}
+		// remove whitespace from all fields in keyVaultCert
+		formatKeyVaultCertificate(&keyVaultCert)
+
+		keyVaultCerts = append(keyVaultCerts, keyVaultCert)
+	}
+
+	common.LogDebug("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)
+
+	kvClient, err := initializeKvClient(ctx, azureCloudEnv.KeyVaultEndpoint, tenantID, workloadIdentityClientID)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get keyvault client, error: %w", err)
+	}
+
+	certs := []types.Certificate{}
+	for _, keyVaultCert := range keyVaultCerts {
+		common.LogDebug("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)
+		if err != nil {
+			return nil, err
+		}
+
+		objectContent := []byte(result.content)
+
+		cert := types.Certificate{
+			CertificateName: keyVaultCert.CertificateName,
+			Content:         objectContent,
+			Version:         result.version,
+		}
+
+		certs = append(certs, cert)
+		common.LogDebug("added certificates %v to response", cert.CertificateName)
+	}
+	return certs, nil
+}
+
+// formatKeyVaultCertificate formats the fields in KeyVaultCertificate
+func formatKeyVaultCertificate(object *types.KeyVaultCertificate) {
+	if object == nil {
+		return
+	}
+	objectPtr := reflect.ValueOf(object)
+	objectValue := objectPtr.Elem()
+
+	for i := 0; i < objectValue.NumField(); i++ {
+		field := objectValue.Field(i)
+		if field.Type() != reflect.TypeOf("") {
+			continue
+		}
+		str := field.Interface().(string)
+		str = strings.TrimSpace(str)
+		field.SetString(str)
+	}
+}
+
+// parseAzureEnvironment returns azure environment by name
+func parseAzureEnvironment(cloudName string) (*azure.Environment, error) {
+	var env azure.Environment
+	var err error
+	if cloudName == "" {
+		env = azure.PublicCloud
+	} else {
+		env, err = azure.EnvironmentFromName(cloudName)
+	}
+	return &env, err
+}
+
+func initializeKvClient(ctx context.Context, keyVaultEndpoint, tenantID, clientId string) (*kv.BaseClient, error) {
+	kvClient := kv.New()
+	kvEndpoint := strings.TrimSuffix(keyVaultEndpoint, "/")
+
+	err := kvClient.AddToUserAgent("ratify")
+	if err != nil {
+		return nil, fmt.Errorf("failed to add user agent to keyvault client, error: %w", err)
+	}
+
+	kvClient.Authorizer, err = getAuthorizerForWorkloadIdentity(ctx, tenantID, clientId, kvEndpoint)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get authorizer for keyvault client, error: %w", err)
+	}
+	return &kvClient, nil
+}
+
+// getCertificate retrieves the certificate from the vault
+func getCertificate(ctx context.Context, kvClient *kv.BaseClient, vaultURL string, kvObject types.KeyVaultCertificate) (keyvaultObject, error) {
+	certbundle, err := kvClient.GetCertificate(ctx, vaultURL, kvObject.CertificateName, kvObject.CertificateVersion)
+	if err != nil {
+		return keyvaultObject{}, fmt.Errorf("failed to get certificate objectName:%s, objectVersion:%s, error: %w", kvObject.CertificateName, kvObject.CertificateVersion, err)
+	}
+	if certbundle.Cer == nil {
+		return keyvaultObject{}, errors.Errorf("certificate value is nil")
+	}
+	if certbundle.ID == nil {
+		return keyvaultObject{}, errors.Errorf("certificate id is nil")
+	}
+	version := getObjectVersion(*certbundle.ID)
+
+	certBlock := &pem.Block{
+		Type:  types.CertificateType,
+		Bytes: *certbundle.Cer,
+	}
+	var pemData []byte
+	pemData = append(pemData, pem.EncodeToMemory(certBlock)...)
+	return keyvaultObject{content: string(pemData), version: version}, nil
+}
+
+// getObjectVersion parses the id to retrieve the version
+// of object fetched
+// example id format - https://kindkv.vault.azure.net/secrets/actual/1f304204f3624873aab40231241243eb
+// TODO (aramase) follow up on https://github.com/Azure/azure-rest-api-specs/issues/10825 to provide
+// a native way to obtain the version
+func getObjectVersion(id string) string {
+	splitID := strings.Split(id, "/")
+	return splitID[len(splitID)-1]
+}
diff --git a/pkg/certificateprovider/azurekeyvault/types/parameters.go b/pkg/certificateprovider/azurekeyvault/types/parameters.go
new file mode 100644
index 0000000..b67c6f2
--- /dev/null
+++ b/pkg/certificateprovider/azurekeyvault/types/parameters.go
@@ -0,0 +1,63 @@
+/*
+Copyright The Ratify Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package types
+
+// This class is based on implementation from azure secret store csi provider
+// Source: https://github.com/Azure/secrets-store-csi-driver-provider-azure/tree/release-1.4/pkg/provider
+import (
+	"strings"
+
+	"gopkg.in/yaml.v3"
+)
+
+// GetKeyVaultUri returns the key vault name
+func GetKeyVaultUri(parameters map[string]string) string {
+	return strings.TrimSpace(parameters[KeyVaultUriParameter])
+}
+
+// GetCloudName returns the cloud name
+func GetCloudName(parameters map[string]string) string {
+	return strings.TrimSpace(parameters[CloudNameParameter])
+}
+
+// GetTenantID returns the tenant ID
+func GetTenantID(parameters map[string]string) string {
+	// ref: https://github.com/Azure/secrets-store-csi-driver-provider-azure/issues/857
+	tenantID := strings.TrimSpace(parameters["tenantID"])
+	if tenantID != "" {
+		return tenantID
+	}
+
+	return strings.TrimSpace(parameters[TenantIDParameter])
+}
+
+// GetClientID returns the client ID
+func GetClientID(parameters map[string]string) string {
+	return strings.TrimSpace(parameters[ClientIDParameter])
+}
+
+// GetCertificates returns the key vault objects
+func GetCertificates(parameters map[string]string) string {
+	return strings.TrimSpace(parameters[CertificatesParameter])
+}
+
+// GetCertificatesArray returns the key vault objects array
+func GetCertificatesArray(objects string) (StringArray, error) {
+	var a StringArray
+	err := yaml.Unmarshal([]byte(objects), &a)
+
+	return a, err
+}
diff --git a/pkg/certificateprovider/azurekeyvault/types/types.go b/pkg/certificateprovider/azurekeyvault/types/types.go
new file mode 100644
index 0000000..3344b73
--- /dev/null
+++ b/pkg/certificateprovider/azurekeyvault/types/types.go
@@ -0,0 +1,53 @@
+/*
+Copyright The Ratify Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package types
+
+// This class is based on implementation from  azure secret store csi provider
+// Source: https://github.com/Azure/secrets-store-csi-driver-provider-azure/tree/release-1.4/pkg/provider
+const (
+	// KeyVaultUriParameter is the name of the key vault URI parameter
+	KeyVaultUriParameter = "vaultURI"
+	// CloudNameParameter is the name of the cloud name parameter
+	CloudNameParameter = "cloudName"
+	// TenantIDParameter is the name of the tenant ID parameter
+	TenantIDParameter = "tenantID"
+	// ClientIDParameter is the name of the client ID parameter
+	// This clientID is used for workload identity
+	ClientIDParameter = "clientID"
+	// CertificatesParameter is the name of the objects parameter
+	CertificatesParameter = "certificates"
+	// Static string for certificate type
+	CertificateType = "CERTIFICATE"
+)
+
+// KeyVaultCertificate holds keyvault certificate related config
+type KeyVaultCertificate struct {
+	// the name of the Azure Key Vault certificate
+	CertificateName string `json:"certificateName" yaml:"certificateName"`
+	// the version of the Azure Key Vault certificate
+	CertificateVersion string `json:"certificateVersion" yaml:"certificateVersion"`
+}
+
+// Certificate holds content and metadata of a keyvault certificate file
+type Certificate struct {
+	Content         []byte
+	CertificateName string
+	Version         string
+}
+
+// StringArray holds a list of strings
+type StringArray struct {
+	Array []string `json:"array" yaml:"array"`
+}
diff --git a/pkg/common/logging.go b/pkg/common/logging.go
index 0af37db..e6d8948 100644
--- a/pkg/common/logging.go
+++ b/pkg/common/logging.go
@@ -43,3 +43,10 @@ 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/common/oras/authprovider/azure/azureworkloadidentity.go b/pkg/common/oras/authprovider/azure/azureworkloadidentity.go
index 40dffb3..93cd1fc 100644
--- a/pkg/common/oras/authprovider/azure/azureworkloadidentity.go
+++ b/pkg/common/oras/authprovider/azure/azureworkloadidentity.go
@@ -22,18 +22,20 @@ import (
 	"os"
 	"time"
 
-	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
 	provider "github.com/deislabs/ratify/pkg/common/oras/authprovider"
-	"github.com/pkg/errors"
-	"github.com/sirupsen/logrus"
+	"github.com/deislabs/ratify/pkg/utils"
 
 	"github.com/Azure/azure-sdk-for-go/services/preview/containerregistry/runtime/2019-08-15-preview/containerregistry"
+	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
+	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
 )
 
 type AzureWIProviderFactory struct{}
 type azureWIAuthProvider struct {
 	aadToken confidential.AuthResult
 	tenantID string
+	clientID string
 }
 
 type azureWIAuthProviderConf struct {
@@ -62,11 +64,18 @@ func (s *AzureWIProviderFactory) Create(authProviderConfig provider.AuthProvider
 	}
 
 	tenant := os.Getenv("AZURE_TENANT_ID")
+
 	if tenant == "" {
 		return nil, fmt.Errorf("azure tenant id environment variable is empty")
 	}
+
+	clientID := os.Getenv("AZURE_CLIENT_ID")
+	if clientID == "" {
+		return nil, fmt.Errorf("azure client id environment variable is empty")
+	}
+
 	// retrieve an AAD Access token
-	token, err := getAADAccessToken(context.Background(), tenant)
+	token, err := utils.GetAADAccessToken(context.Background(), tenant, clientID, AADResource)
 	if err != nil {
 		return nil, err
 	}
@@ -74,12 +83,13 @@ func (s *AzureWIProviderFactory) Create(authProviderConfig provider.AuthProvider
 	return &azureWIAuthProvider{
 		aadToken: token,
 		tenantID: tenant,
+		clientID: clientID,
 	}, nil
 }
 
 // Enabled checks for non empty tenant ID and AAD access token
 func (d *azureWIAuthProvider) Enabled(ctx context.Context) bool {
-	if d.tenantID == "" {
+	if d.tenantID == "" || d.clientID == "" {
 		return false
 	}
 
@@ -105,7 +115,7 @@ func (d *azureWIAuthProvider) Provide(ctx context.Context, artifact string) (pro
 
 	// need to refresh AAD token if it's expired
 	if time.Now().Add(time.Minute * 5).After(d.aadToken.ExpiresOn) {
-		newToken, err := getAADAccessToken(ctx, d.tenantID)
+		newToken, err := utils.GetAADAccessToken(ctx, d.tenantID, d.clientID, AADResource)
 		if err != nil {
 			return provider.AuthConfig{}, errors.Wrap(err, "could not refresh AAD token")
 		}
@@ -132,55 +142,3 @@ func (d *azureWIAuthProvider) Provide(ctx context.Context, artifact string) (pro
 
 	return authConfig, nil
 }
-
-// Source: https://github.com/Azure/azure-workload-identity/blob/d126293e3c7c669378b225ad1b1f29cf6af4e56d/examples/msal-go/token_credential.go#L25
-func getAADAccessToken(ctx context.Context, tenantID string) (confidential.AuthResult, error) {
-	// Azure AD Workload Identity webhook will inject the following env vars:
-	// 	AZURE_CLIENT_ID with the clientID set in the service account annotation
-	// 	AZURE_TENANT_ID with the tenantID set in the service account annotation. If not defined, then
-	// 	the tenantID provided via azure-wi-webhook-config for the webhook will be used.
-	// 	AZURE_FEDERATED_TOKEN_FILE is the service account token path
-	// 	AZURE_AUTHORITY_HOST is the AAD authority hostname
-	clientID := os.Getenv("AZURE_CLIENT_ID")
-	tokenFilePath := os.Getenv("AZURE_FEDERATED_TOKEN_FILE")
-	authority := os.Getenv("AZURE_AUTHORITY_HOST")
-	if clientID == "" || tokenFilePath == "" || authority == "" {
-		return confidential.AuthResult{}, fmt.Errorf("required environment variables not set, AZURE_CLIENT_ID: %s, AZURE_FEDERATED_TOKEN_FILE: %s, AZURE_AUTHORITY_HOST: %s", clientID, tokenFilePath, authority)
-	}
-
-	// read the service account token from the filesystem
-	signedAssertion, err := readJWTFromFS(tokenFilePath)
-	if err != nil {
-		return confidential.AuthResult{}, errors.Wrap(err, "failed to read service account token")
-	}
-	cred, err := confidential.NewCredFromAssertion(signedAssertion)
-	if err != nil {
-		return confidential.AuthResult{}, errors.Wrap(err, "failed to create confidential creds")
-	}
-
-	// create the confidential client to request an AAD token
-	confidentialClientApp, err := confidential.New(
-		clientID,
-		cred,
-		confidential.WithAuthority(fmt.Sprintf("%s%s/oauth2/token", authority, tenantID)))
-	if err != nil {
-		return confidential.AuthResult{}, errors.Wrap(err, "failed to create confidential client app")
-	}
-
-	result, err := confidentialClientApp.AcquireTokenByCredential(ctx, []string{AADResource})
-	if err != nil {
-		return confidential.AuthResult{}, errors.Wrap(err, "failed to acquire AAD token")
-	}
-
-	return result, nil
-}
-
-// readJWTFromFS reads the jwt from file system
-// Source: https://github.com/Azure/azure-workload-identity/blob/d126293e3c7c669378b225ad1b1f29cf6af4e56d/examples/msal-go/token_credential.go#L88
-func readJWTFromFS(tokenFilePath string) (string, error) {
-	token, err := os.ReadFile(tokenFilePath)
-	if err != nil {
-		return "", err
-	}
-	return string(token), nil
-}
diff --git a/pkg/utils/authenticationUtils.go b/pkg/utils/authenticationUtils.go
new file mode 100644
index 0000000..bbde6df
--- /dev/null
+++ b/pkg/utils/authenticationUtils.go
@@ -0,0 +1,69 @@
+/*
+Copyright The Ratify Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package utils
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
+)
+
+// Source: https://github.com/Azure/azure-workload-identity/blob/d126293e3c7c669378b225ad1b1f29cf6af4e56d/examples/msal-go/token_credential.go#L25
+func GetAADAccessToken(ctx context.Context, tenantID, clientID, scope string) (confidential.AuthResult, error) {
+	// Azure AD Workload Identity webhook will inject the following env vars:
+	// 	AZURE_FEDERATED_TOKEN_FILE is the service account token path
+	// 	AZURE_AUTHORITY_HOST is the AAD authority hostname
+
+	tokenFilePath := os.Getenv("AZURE_FEDERATED_TOKEN_FILE")
+	authority := os.Getenv("AZURE_AUTHORITY_HOST")
+
+	if tokenFilePath == "" || authority == "" {
+		return confidential.AuthResult{}, fmt.Errorf("required environment variables not set, AZURE_FEDERATED_TOKEN_FILE: %s, AZURE_AUTHORITY_HOST: %s", tokenFilePath, authority)
+	}
+
+	cred := confidential.NewCredFromAssertionCallback(func(context.Context, confidential.AssertionRequestOptions) (string, error) {
+		// read the service account token from the filesystem
+		return readJWTFromFS(tokenFilePath)
+	})
+
+	// create the confidential client to request an AAD token
+	confidentialClientApp, err := confidential.New(
+		clientID,
+		cred,
+		confidential.WithAuthority(fmt.Sprintf("%s%s/oauth2/token", authority, tenantID)))
+	if err != nil {
+		return confidential.AuthResult{}, fmt.Errorf("failed to create confidential client app: %w", err)
+	}
+
+	result, err := confidentialClientApp.AcquireTokenByCredential(ctx, []string{scope})
+	if err != nil {
+		return confidential.AuthResult{}, fmt.Errorf("failed to acquire AAD token: %w", err)
+	}
+
+	return result, nil
+}
+
+// readJWTFromFS reads the jwt from file system
+// Source: https://github.com/Azure/azure-workload-identity/blob/d126293e3c7c669378b225ad1b1f29cf6af4e56d/examples/msal-go/token_credential.go#L88
+func readJWTFromFS(tokenFilePath string) (string, error) {
+	token, err := os.ReadFile(tokenFilePath)
+	if err != nil {
+		return "", err
+	}
+	return string(token), nil
+}


Test patch:
diff --git a/config/samples/config_v1alpha1_certificatestore.yaml b/config/samples/config_v1alpha1_certificatestore.yaml
new file mode 100644
index 0000000..008df93
--- /dev/null
+++ b/config/samples/config_v1alpha1_certificatestore.yaml
@@ -0,0 +1,6 @@
+apiVersion: config.ratify.deislabs.io/v1alpha1
+kind: CertificateStore
+metadata:
+  name: certificatestore-sample
+spec:
+  # TODO(user): Add fields here
diff --git a/pkg/certificateprovider/azurekeyvault/auth_test.go b/pkg/certificateprovider/azurekeyvault/auth_test.go
new file mode 100644
index 0000000..a7d3f59
--- /dev/null
+++ b/pkg/certificateprovider/azurekeyvault/auth_test.go
@@ -0,0 +1,67 @@
+/*
+Copyright The Ratify Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package azurekeyvault
+
+import (
+	"fmt"
+	"testing"
+	"time"
+)
+
+// Vendored from https://github.com/Azure/go-autorest/blob/7dd32b67be4e6c9386b9ba7b1c44a51263f05270/autorest/adal/token_test.go
+func TestParseExpiresOn(t *testing.T) {
+	n := time.Now().UTC()
+	amPM := "AM"
+	if n.Hour() >= 12 {
+		amPM = "PM"
+	}
+	testcases := []struct {
+		Name   string
+		String string
+		Value  int64
+	}{
+		{
+			Name:   "integer",
+			String: "3600",
+			Value:  3600,
+		},
+		{
+			Name:   "timestamp with AM/PM",
+			String: fmt.Sprintf("%d/%d/%d %d:%02d:%02d %s +00:00", n.Month(), n.Day(), n.Year(), n.Hour(), n.Minute(), n.Second(), amPM),
+			Value:  n.Unix(),
+		},
+		{
+			Name:   "timestamp without AM/PM",
+			String: fmt.Sprintf("%02d/%02d/%02d %02d:%02d:%02d +00:00", n.Month(), n.Day(), n.Year(), n.Hour(), n.Minute(), n.Second()),
+			Value:  n.Unix(),
+		},
+	}
+	for _, tc := range testcases {
+		t.Run(tc.Name, func(subT *testing.T) {
+			jn, err := parseExpiresOn(tc.String)
+			if err != nil {
+				subT.Error(err)
+			}
+			i, err := jn.Int64()
+			if err != nil {
+				subT.Error(err)
+			}
+			if i != tc.Value {
+				subT.Logf("expected %d, got %d", tc.Value, i)
+				subT.Fail()
+			}
+		})
+	}
+}
diff --git a/pkg/certificateprovider/azurekeyvault/provider_test.go b/pkg/certificateprovider/azurekeyvault/provider_test.go
new file mode 100644
index 0000000..74e088c
--- /dev/null
+++ b/pkg/certificateprovider/azurekeyvault/provider_test.go
@@ -0,0 +1,216 @@
+/*
+Copyright The Ratify Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package azurekeyvault
+
+// This class is based on implementation from  azure secret store csi provider
+// Source: https://github.com/Azure/secrets-store-csi-driver-provider-azure/tree/release-1.4/pkg/provider
+import (
+	"context"
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/Azure/go-autorest/autorest/azure"
+	"github.com/deislabs/ratify/pkg/certificateprovider/azurekeyvault/types"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParseAzureEnvironment(t *testing.T) {
+	envNamesArray := []string{"AZURECHINACLOUD", "AZUREGERMANCLOUD", "AZUREPUBLICCLOUD", "AZUREUSGOVERNMENTCLOUD", ""}
+	for _, envName := range envNamesArray {
+		azureEnv, err := parseAzureEnvironment(envName)
+		if err != nil {
+			t.Fatalf("expected no error, got %v", err)
+		}
+		if strings.EqualFold(envName, "") && !strings.EqualFold(azureEnv.Name, "AZUREPUBLICCLOUD") {
+			t.Fatalf("string doesn't match, expected AZUREPUBLICCLOUD, got %s", azureEnv.Name)
+		} else if !strings.EqualFold(envName, "") && !strings.EqualFold(envName, azureEnv.Name) {
+			t.Fatalf("string doesn't match, expected %s, got %s", envName, azureEnv.Name)
+		}
+	}
+
+	wrongEnvName := "AZUREWRONGCLOUD"
+	_, err := parseAzureEnvironment(wrongEnvName)
+	if err == nil {
+		t.Fatalf("expected error for wrong azure environment name")
+	}
+}
+
+func TestFormatKeyVaultCertificate(t *testing.T) {
+	cases := []struct {
+		desc                   string
+		keyVaultObject         types.KeyVaultCertificate
+		expectedKeyVaultObject types.KeyVaultCertificate
+	}{
+		{
+			desc: "leading and trailing whitespace trimmed from all fields",
+			keyVaultObject: types.KeyVaultCertificate{
+				CertificateName:    "cert1     ",
+				CertificateVersion: "",
+			},
+			expectedKeyVaultObject: types.KeyVaultCertificate{
+				CertificateName:    "cert1",
+				CertificateVersion: "",
+			},
+		},
+		{
+			desc: "no data loss for already sanitized object",
+			keyVaultObject: types.KeyVaultCertificate{
+				CertificateName:    "cert1",
+				CertificateVersion: "version1",
+			},
+			expectedKeyVaultObject: types.KeyVaultCertificate{
+				CertificateName:    "cert1",
+				CertificateVersion: "version1",
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.desc, func(t *testing.T) {
+			formatKeyVaultCertificate(&tc.keyVaultObject)
+			if !reflect.DeepEqual(tc.keyVaultObject, tc.expectedKeyVaultObject) {
+				t.Fatalf("expected: %+v, but got: %+v", tc.expectedKeyVaultObject, tc.keyVaultObject)
+			}
+		})
+	}
+}
+
+func SkipTestInitializeKVClient(t *testing.T) {
+	testEnvs := []azure.Environment{
+		azure.PublicCloud,
+		azure.GermanCloud,
+		azure.ChinaCloud,
+		azure.USGovernmentCloud,
+	}
+
+	for i := range testEnvs {
+		kvBaseClient, err := initializeKvClient(context.TODO(), testEnvs[i].KeyVaultEndpoint, "", "")
+		assert.NoError(t, err)
+		assert.NotNil(t, kvBaseClient)
+		assert.NotNil(t, kvBaseClient.Authorizer)
+		assert.Contains(t, kvBaseClient.UserAgent, "ratify")
+	}
+}
+
+func TestGetCertificatesContent(t *testing.T) {
+	cases := []struct {
+		desc        string
+		parameters  map[string]string
+		secrets     map[string]string
+		expectedErr bool
+	}{
+		{
+			desc:        "keyvault uri not provided",
+			parameters:  map[string]string{},
+			expectedErr: true,
+		},
+		{
+			desc: "tenantID not provided",
+			parameters: map[string]string{
+				"vaultUri": "https://testkv.vault.azure.net/",
+			},
+			expectedErr: true,
+		},
+		{
+			desc: "invalid cloud name",
+			parameters: map[string]string{
+				"vaultUri":  "https://testkv.vault.azure.net/",
+				"tenantID":  "tid",
+				"cloudName": "AzureCloud",
+			},
+			expectedErr: true,
+		},
+		{
+			desc: "certificates array not set",
+			parameters: map[string]string{
+				"vaultUri":             "https://testkv.vault.azure.net/",
+				"tenantID":             "tid",
+				"useVMManagedIdentity": "true",
+			},
+			expectedErr: true,
+		},
+		{
+			desc: "certificates not configured as an array",
+			parameters: map[string]string{
+				"vaultUri":             "https://testkv.vault.azure.net/",
+				"tenantID":             "tid",
+				"useVMManagedIdentity": "true",
+				"certificates": `
+        - |
+          CertificateName: cert1
+          CertificateVersion: ""`,
+			},
+			expectedErr: true,
+		},
+		{
+			desc: "certificates array is empty",
+			parameters: map[string]string{
+				"vaultUri": "https://testkv.vault.azure.net/",
+				"tenantID": "tid",
+				"clientID": "clientid",
+				"certificates": `
+      array:`,
+			},
+			expectedErr: true,
+		},
+		{
+			desc: "invalid object format",
+			parameters: map[string]string{
+				"vaultUri": "https://testkv.vault.azure.net/",
+				"tenantID": "tid",
+				"clientID": "clientid",
+				"certificates": `
+      array:
+        - |
+          CertificateName: cert1
+          CertificateVersion: ""`,
+			},
+			expectedErr: true,
+		},
+		{
+			desc: "error fetching from keyvault",
+			parameters: map[string]string{
+				"vaultUri": "https://testkv.vault.azure.net/",
+				"tenantID": "tid",
+				"certificates": `
+      array:
+        - |
+          CertificateName: cert1
+          CertificateVersion: ""`,
+			},
+			expectedErr: true,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.desc, func(t *testing.T) {
+			_, err := GetCertificates(context.TODO(), tc.parameters)
+			if tc.expectedErr {
+				assert.NotNil(t, err)
+			} else {
+				assert.Nil(t, err)
+			}
+		})
+	}
+}
+
+func TestGetObjectVersion(t *testing.T) {
+	id := "https://kindkv.vault.azure.net/secrets/cert1/c55925c29c6743dcb9bb4bf091be03b0"
+	expectedVersion := "c55925c29c6743dcb9bb4bf091be03b0"
+	actual := getObjectVersion(id)
+	assert.Equal(t, expectedVersion, actual)
+}
diff --git a/pkg/certificateprovider/azurekeyvault/types/parameters_test.go b/pkg/certificateprovider/azurekeyvault/types/parameters_test.go
new file mode 100644
index 0000000..376e67c
--- /dev/null
+++ b/pkg/certificateprovider/azurekeyvault/types/parameters_test.go
@@ -0,0 +1,274 @@
+/*
+Copyright The Ratify Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package types
+
+// This class is based on implementation from azure secret store csi provider
+// Source: https://github.com/Azure/secrets-store-csi-driver-provider-azure/tree/release-1.4/pkg/provider
+import (
+	"reflect"
+	"testing"
+)
+
+func TestGetKeyVaultUri(t *testing.T) {
+	tests := []struct {
+		name       string
+		parameters map[string]string
+		expected   string
+	}{
+		{
+			name: "empty",
+			parameters: map[string]string{
+				KeyVaultUriParameter: "",
+			},
+			expected: "",
+		},
+		{
+			name: "not empty",
+			parameters: map[string]string{
+				KeyVaultUriParameter: "https://test.vault.azure.net/",
+			},
+			expected: "https://test.vault.azure.net/",
+		},
+		{
+			name: "trim spaces",
+			parameters: map[string]string{
+				KeyVaultUriParameter: " https://test.vault.azure.net/ ",
+			},
+			expected: "https://test.vault.azure.net/",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			actual := GetKeyVaultUri(test.parameters)
+			if actual != test.expected {
+				t.Errorf("GetKeyVaultUri() = %v, expected %v", actual, test.expected)
+			}
+		})
+	}
+}
+
+func TestGetCloudName(t *testing.T) {
+	tests := []struct {
+		name       string
+		parameters map[string]string
+		expected   string
+	}{
+		{
+			name: "empty",
+			parameters: map[string]string{
+				CloudNameParameter: "",
+			},
+			expected: "",
+		},
+		{
+			name: "not empty",
+			parameters: map[string]string{
+				CloudNameParameter: "test",
+			},
+			expected: "test",
+		},
+		{
+			name: "trim spaces",
+			parameters: map[string]string{
+				CloudNameParameter: " test ",
+			},
+			expected: "test",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			actual := GetCloudName(test.parameters)
+			if actual != test.expected {
+				t.Errorf("GetCloudName() = %v, expected %v", actual, test.expected)
+			}
+		})
+	}
+}
+
+func TestGetTenantID(t *testing.T) {
+	tests := []struct {
+		name       string
+		parameters map[string]string
+		expected   string
+	}{
+		{
+			name: "empty",
+			parameters: map[string]string{
+				TenantIDParameter: "",
+			},
+			expected: "",
+		},
+		{
+			name: "not empty",
+			parameters: map[string]string{
+				TenantIDParameter: "test",
+			},
+			expected: "test",
+		},
+		{
+			name: "trim spaces",
+			parameters: map[string]string{
+				TenantIDParameter: " test ",
+			},
+			expected: "test",
+		},
+		{
+			name: "new tenantID parameter",
+			parameters: map[string]string{
+				"tenantID": "test",
+			},
+			expected: "test",
+		},
+		{
+			name: "new tenantID parameter with spaces",
+			parameters: map[string]string{
+				"tenantID": " test ",
+			},
+			expected: "test",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			actual := GetTenantID(test.parameters)
+			if actual != test.expected {
+				t.Errorf("GetTenantID() = %v, expected %v", actual, test.expected)
+			}
+		})
+	}
+}
+
+func TestGetClientID(t *testing.T) {
+	tests := []struct {
+		name       string
+		parameters map[string]string
+		expected   string
+	}{
+		{
+			name: "empty",
+			parameters: map[string]string{
+				"clientID": "",
+			},
+			expected: "",
+		},
+		{
+			name: "not empty",
+			parameters: map[string]string{
+				"clientID": "test",
+			},
+			expected: "test",
+		},
+		{
+			name: "trim spaces",
+			parameters: map[string]string{
+				"clientID": " test ",
+			},
+			expected: "test",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			actual := GetClientID(test.parameters)
+			if actual != test.expected {
+				t.Errorf("GetClientID() = %v, expected %v", actual, test.expected)
+			}
+		})
+	}
+}
+
+func TestGetObjects(t *testing.T) {
+	tests := []struct {
+		name       string
+		parameters map[string]string
+		expected   string
+	}{
+		{
+			name: "empty",
+			parameters: map[string]string{
+				CertificatesParameter: "",
+			},
+			expected: "",
+		},
+		{
+			name: "not empty",
+			parameters: map[string]string{
+				CertificatesParameter: "test",
+			},
+			expected: "test",
+		},
+		{
+			name: "trim spaces",
+			parameters: map[string]string{
+				CertificatesParameter: " test ",
+			},
+			expected: "test",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			actual := GetCertificates(test.parameters)
+			if actual != test.expected {
+				t.Errorf("GetObjects() = %v, expected %v", actual, test.expected)
+			}
+		})
+	}
+}
+
+func TestGetObjectsArray(t *testing.T) {
+	tests := []struct {
+		name     string
+		objects  string
+		expected StringArray
+	}{
+		{
+			name:     "empty",
+			objects:  "",
+			expected: StringArray{},
+		},
+		{
+			name:    "valid yaml",
+			objects: "array:\n- |\n certificateName: secret1\n certificateVersion: \"\"\n- |\n certificateName: secret2\n certificateVersion: \"\"\n",
+			expected: StringArray{
+				Array: []string{
+					"certificateName: secret1\ncertificateVersion: \"\"\n",
+					"certificateName: secret2\ncertificateVersion: \"\"\n",
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			actual, err := GetCertificatesArray(test.objects)
+			if err != nil {
+				t.Errorf("GetCertificatesArray() error = %v", err)
+			}
+			if !reflect.DeepEqual(actual, test.expected) {
+				t.Errorf("GetCertificatesArray() = %v, expected %v", actual, test.expected)
+			}
+		})
+	}
+}
+
+func TestGetCertificatesArrayError(t *testing.T) {
+	objects := "invalid"
+	if _, err := GetCertificatesArray(objects); err == nil {
+		t.Errorf("GetCertificatesArray() error is nil, expected error")
+	}
+}
diff --git a/pkg/common/oras/authprovider/azure/azureworkloadidentity_test.go b/pkg/common/oras/authprovider/azure/azureworkloadidentity_test.go
index fb67dcd..3b2c98c 100644
--- a/pkg/common/oras/authprovider/azure/azureworkloadidentity_test.go
+++ b/pkg/common/oras/authprovider/azure/azureworkloadidentity_test.go
@@ -29,6 +29,7 @@ import (
 func TestAzureWIEnabled_ExpectedResults(t *testing.T) {
 	azAuthProvider := azureWIAuthProvider{
 		tenantID: "test_tenant",
+		clientID: "test_client",
 		aadToken: confidential.AuthResult{
 			AccessToken: "test_token",
 		},
@@ -45,6 +46,11 @@ func TestAzureWIEnabled_ExpectedResults(t *testing.T) {
 		t.Fatal("enabled should have returned false but returned true for empty tenantID")
 	}
 
+	azAuthProvider.clientID = ""
+	if azAuthProvider.Enabled(ctx) {
+		t.Fatal("enabled should have returned false but returned true for empty clientID")
+	}
+
 	azAuthProvider.aadToken.AccessToken = ""
 	if azAuthProvider.Enabled(ctx) {
 		t.Fatal("enabled should have returned false but returned true for empty AAD access token")
@@ -75,9 +81,17 @@ func TestAzureWIValidation_EnvironmentVariables_ExpectedResults(t *testing.T) {
 		t.Fatal("failed to set env variable AZURE_TENANT_ID")
 	}
 
+	err = os.Setenv("AZURE_CLIENT_ID", "client id")
+	if err != nil {
+		t.Fatal("failed to set env variable AZURE_CLIENT_ID")
+	}
+
+	defer os.Unsetenv("AZURE_CLIENT_ID")
+	defer os.Unsetenv("AZURE_TENANT_ID")
+
 	_, err = authprovider.CreateAuthProviderFromConfig(authProviderConfig)
 
-	expectedErr = fmt.Errorf("required environment variables not set, AZURE_CLIENT_ID: , AZURE_FEDERATED_TOKEN_FILE: , AZURE_AUTHORITY_HOST: ")
+	expectedErr = fmt.Errorf("required environment variables not set, AZURE_FEDERATED_TOKEN_FILE: , AZURE_AUTHORITY_HOST: ")
 	if err == nil || err.Error() != expectedErr.Error() {
 		t.Fatalf("create auth provider should have failed: expected err %s, but got err %s", expectedErr, err)
 	}