diff --git a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md
index 034766972361..66ee2ed5782c 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md
+++ b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md
@@ -3,6 +3,8 @@
## 2.12.0-beta.1 (Unreleased)
### Features Added
+- Added lazy loading for Key Vault certificate details in the JCA keystore. Certificate details are now loaded by alias when requested, avoiding unnecessary reads for unconfigured certificates. ([#49774](https://github.com/Azure/azure-sdk-for-java/pull/49774))
+- Added support for `azure.keyvault.jca.certificate-alias-filter-patterns` to filter Key Vault certificate aliases with include/exclude regex patterns. Include patterns are configured directly and exclude patterns are prefixed with `!`. If the property is not configured, alias filtering is disabled and all discovered Key Vault aliases remain eligible for lazy loading. ([#39487](https://github.com/Azure/azure-sdk-for-java/issues/39487))
### Breaking Changes
diff --git a/sdk/keyvault/azure-security-keyvault-jca/README.md b/sdk/keyvault/azure-security-keyvault-jca/README.md
index f31a7ca5e90a..fdcfcb0e528a 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/README.md
+++ b/sdk/keyvault/azure-security-keyvault-jca/README.md
@@ -141,6 +141,7 @@ The JCA library supports configuring the following options:
* `azure.keyvault.jca.refresh-certificates-when-have-un-trust-certificate`: Indicates whether to refresh certificates when have untrusted certificate.
* `azure.keyvault.jca.certificates-refresh-interval`: The refresh interval time.
* `azure.keyvault.jca.certificates-refresh-interval-in-ms`: The refresh interval time.
+* `azure.keyvault.jca.certificate-alias-filter-patterns`: Comma-separated Key Vault alias regex filters. Use include regexes directly and exclude regexes with a `!` prefix. Regex patterns use full-alias matching (`Pattern.matcher(alias).matches()`). An alias is loaded only if it matches at least one include regex (or if no include regex is configured) and matches no exclude regex. Example: `^prod-.*,!^prod-deprecated$,!.*-old$`. If this property is not configured, all discovered Key Vault aliases are eligible for lazy loading.
* `azure.keyvault.disable-challenge-resource-verification`: Indicates whether to disable verification that the authentication challenge resource matches the Key Vault or Managed HSM domain.
You can configure these properties using:
diff --git a/sdk/keyvault/azure-security-keyvault-jca/checkstyle-suppressions.xml b/sdk/keyvault/azure-security-keyvault-jca/checkstyle-suppressions.xml
index 1796f495a42b..790fdff01ec1 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/checkstyle-suppressions.xml
+++ b/sdk/keyvault/azure-security-keyvault-jca/checkstyle-suppressions.xml
@@ -13,6 +13,7 @@
+
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultKeyStore.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultKeyStore.java
index a87b1ef8ef78..1e8cf8dbcbbf 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultKeyStore.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultKeyStore.java
@@ -29,7 +29,9 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
import java.util.logging.Logger;
+import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.logging.Level.FINE;
@@ -56,6 +58,9 @@ public final class KeyVaultKeyStore extends KeyStoreSpi {
*/
private static final Logger LOGGER = Logger.getLogger(KeyVaultKeyStore.class.getName());
+ static final String CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY
+ = "azure.keyvault.jca.certificate-alias-filter-patterns";
+
/**
* Stores the Jre key store certificates.
*/
@@ -146,8 +151,9 @@ public KeyVaultKeyStore() {
customCertificates = SpecificPathCertificates.getSpecificPathCertificates(customPath);
LOGGER.log(FINE, String.format("Loaded custom certificates: %s.", customCertificates.getAliases()));
- keyVaultCertificates = new KeyVaultCertificates(refreshInterval, keyVaultUri, tenantId, clientId, clientSecret,
- managedIdentity, accessToken, disableChallengeResourceVerification);
+ keyVaultCertificates
+ = new KeyVaultCertificates(refreshInterval, keyVaultUri, tenantId, clientId, clientSecret, managedIdentity,
+ accessToken, disableChallengeResourceVerification, getKeyVaultCertificateAliasFilterPatterns());
LOGGER.log(FINE, String.format("Loaded Key Vault certificates: %s.", keyVaultCertificates.getAliases()));
classpathCertificates = new ClasspathCertificates();
@@ -168,6 +174,15 @@ Long getRefreshInterval() {
.orElse(0L);
}
+ Set getKeyVaultCertificateAliasFilterPatterns() {
+ return Optional.ofNullable(System.getProperty(CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY))
+ .map(value -> Stream.of(value.split(","))
+ .map(String::trim)
+ .filter(pattern -> !pattern.isEmpty())
+ .collect(Collectors.toSet()))
+ .orElse(Collections.emptySet());
+ }
+
/**
* get key vault key store by system property
*
@@ -254,16 +269,22 @@ public boolean engineEntryInstanceOf(String alias, Class extends KeyStore.Entr
*/
@Override
public Certificate engineGetCertificate(String alias) {
- Certificate certificate = allCertificates.stream()
- .map(AzureCertificates::getCertificates)
- .filter(a -> a.containsKey(alias))
- .findFirst()
- .map(certificates -> certificates.get(alias))
- .orElse(null);
+ Certificate certificate = null;
+ for (AzureCertificates certificatesSource : allCertificates) {
+ if (certificatesSource instanceof KeyVaultCertificates) {
+ certificate = ((KeyVaultCertificates) certificatesSource).getCertificate(alias);
+ } else {
+ certificate = certificatesSource.getCertificates().get(alias);
+ }
+
+ if (certificate != null) {
+ break;
+ }
+ }
if (refreshCertificatesWhenHaveUnTrustCertificate && certificate == null) {
keyVaultCertificates.refreshCertificates();
- certificate = keyVaultCertificates.getCertificates().get(alias);
+ certificate = keyVaultCertificates.getCertificate(alias);
}
return certificate;
@@ -307,16 +328,22 @@ public String engineGetCertificateAlias(Certificate cert) {
*/
@Override
public Certificate[] engineGetCertificateChain(String alias) {
- Certificate[] certificates = allCertificates.stream()
- .map(AzureCertificates::getCertificateChains)
- .filter(Objects::nonNull)
- .filter(a -> a.containsKey(alias))
- .findFirst()
- .map(m -> m.get(alias))
- .orElse(null);
+ Certificate[] certificates = null;
+ for (AzureCertificates certificatesSource : allCertificates) {
+ if (certificatesSource instanceof KeyVaultCertificates) {
+ certificates = ((KeyVaultCertificates) certificatesSource).getCertificateChain(alias);
+ } else {
+ certificates = certificatesSource.getCertificateChains().get(alias);
+ }
+
+ if (certificates != null) {
+ break;
+ }
+ }
+
if (refreshCertificatesWhenHaveUnTrustCertificate && certificates == null) {
keyVaultCertificates.refreshCertificates();
- return keyVaultCertificates.getCertificateChains().get(alias);
+ return keyVaultCertificates.getCertificateChain(alias);
}
return certificates;
}
@@ -358,12 +385,20 @@ public KeyStore.Entry engineGetEntry(String alias, KeyStore.ProtectionParameter
*/
@Override
public Key engineGetKey(String alias, char[] password) {
- return allCertificates.stream()
- .map(AzureCertificates::getCertificateKeys)
- .filter(a -> a.containsKey(alias))
- .findFirst()
- .map(certificateKeys -> certificateKeys.get(alias))
- .orElse(null);
+ Key key = null;
+ for (AzureCertificates certificatesSource : allCertificates) {
+ if (certificatesSource instanceof KeyVaultCertificates) {
+ key = ((KeyVaultCertificates) certificatesSource).getCertificateKey(alias);
+ } else {
+ key = certificatesSource.getCertificateKeys().get(alias);
+ }
+
+ if (key != null) {
+ break;
+ }
+ }
+
+ return key;
}
/**
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificates.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificates.java
index f4636abb2e0b..3313d8aa7e7f 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificates.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificates.java
@@ -11,20 +11,45 @@
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import static java.util.logging.Level.WARNING;
/**
* Store certificates loaded from KeyVault.
*/
public final class KeyVaultCertificates implements AzureCertificates {
+ private static final Logger LOGGER = Logger.getLogger(KeyVaultCertificates.class.getName());
+
/**
* Stores the list of aliases.
*/
private List aliases = new ArrayList<>();
+ /**
+ * Stores aliases whose certificate has already been loaded.
+ */
+ private final Set loadedCertificateAliases = new HashSet<>();
+
+ /**
+ * Stores aliases whose certificate chain has already been loaded.
+ */
+ private final Set loadedCertificateChainAliases = new HashSet<>();
+
+ /**
+ * Stores aliases whose private key has already been loaded.
+ */
+ private final Set loadedCertificateKeyAliases = new HashSet<>();
+
/**
* Stores the certificates by alias.
*/
@@ -49,17 +74,95 @@ public final class KeyVaultCertificates implements AzureCertificates {
private final long refreshInterval;
+ private final Set certificateFilterPatterns;
+
+ private final List includeAliasPatterns;
+
+ private final List excludeAliasPatterns;
+
public KeyVaultCertificates(long refreshInterval, String keyVaultUri, String tenantId, String clientId,
String clientSecret, String managedIdentity, String accessToken, boolean disableChallengeResourceVerification) {
+ this(refreshInterval, keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, accessToken,
+ disableChallengeResourceVerification, Collections.emptySet());
+ }
+
+ public KeyVaultCertificates(long refreshInterval, String keyVaultUri, String tenantId, String clientId,
+ String clientSecret, String managedIdentity, String accessToken, boolean disableChallengeResourceVerification,
+ Set certificateFilterPatterns) {
this.refreshInterval = refreshInterval;
+ this.certificateFilterPatterns
+ = new HashSet<>(Optional.ofNullable(certificateFilterPatterns).orElse(Collections.emptySet()));
+ this.includeAliasPatterns = getAliasPatterns(this.certificateFilterPatterns, false);
+ this.excludeAliasPatterns = getAliasPatterns(this.certificateFilterPatterns, true);
updateKeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, accessToken,
disableChallengeResourceVerification);
}
public KeyVaultCertificates(long refreshInterval, KeyVaultClient keyVaultClient) {
+ this(refreshInterval, keyVaultClient, Collections.emptySet());
+ }
+
+ public KeyVaultCertificates(long refreshInterval, KeyVaultClient keyVaultClient,
+ Set certificateFilterPatterns) {
this.refreshInterval = refreshInterval;
+ setKeyVaultClient(keyVaultClient);
+ this.certificateFilterPatterns
+ = new HashSet<>(Optional.ofNullable(certificateFilterPatterns).orElse(Collections.emptySet()));
+ this.includeAliasPatterns = getAliasPatterns(this.certificateFilterPatterns, false);
+ this.excludeAliasPatterns = getAliasPatterns(this.certificateFilterPatterns, true);
+ }
+
+ private List getAliasPatterns(Set filterPatterns, boolean excludePatterns) {
+ return Optional.ofNullable(filterPatterns)
+ .orElse(Collections.emptySet())
+ .stream()
+ .filter(Objects::nonNull)
+ .map(String::trim)
+ .filter(pattern -> !pattern.isEmpty())
+ .filter(pattern -> pattern.startsWith("!") == excludePatterns)
+ .map(pattern -> {
+ if (excludePatterns) {
+ return pattern.substring(1);
+ }
+ if (pattern.startsWith("+")) {
+ return pattern.substring(1);
+ }
+ return pattern;
+ })
+ .filter(pattern -> !pattern.isEmpty())
+ .map(this::compileRegexPattern)
+ .collect(Collectors.toList());
+ }
+
+ private Pattern compileRegexPattern(String regexPattern) {
+ try {
+ return Pattern.compile(regexPattern);
+ } catch (PatternSyntaxException exception) {
+ throw new IllegalArgumentException("Invalid certificate filter regex pattern: " + regexPattern, exception);
+ }
+ }
+
+ private boolean shouldIncludeAlias(String alias) {
+ if (alias == null) {
+ return false;
+ }
+
+ boolean included = includeAliasPatterns.isEmpty()
+ || includeAliasPatterns.stream().anyMatch(pattern -> pattern.matcher(alias).matches());
+ if (!included) {
+ return false;
+ }
+
+ return excludeAliasPatterns.stream().noneMatch(pattern -> pattern.matcher(alias).matches());
+ }
+
+ private synchronized KeyVaultClient getKeyVaultClient() {
+ return keyVaultClient;
+ }
+
+ private synchronized void setKeyVaultClient(KeyVaultClient keyVaultClient) {
this.keyVaultClient = keyVaultClient;
}
@@ -74,19 +177,32 @@ public KeyVaultCertificates(long refreshInterval, KeyVaultClient keyVaultClient)
* @param accessToken Access token.
* @param disableChallengeResourceVerification Indicates if the challenge resource verification should be disabled.
*/
- public void updateKeyVaultClient(String keyVaultUri, String tenantId, String clientId, String clientSecret,
- String managedIdentity, String accessToken, boolean disableChallengeResourceVerification) {
+ public synchronized void updateKeyVaultClient(String keyVaultUri, String tenantId, String clientId,
+ String clientSecret, String managedIdentity, String accessToken, boolean disableChallengeResourceVerification) {
if (keyVaultUri != null) {
- keyVaultClient = new KeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity,
- accessToken, disableChallengeResourceVerification);
+ setKeyVaultClient(new KeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity,
+ accessToken, disableChallengeResourceVerification));
} else {
- keyVaultClient = null;
+ setKeyVaultClient(null);
}
+
+ clearCachedState();
+ }
+
+ private synchronized void clearCachedState() {
+ aliases = new ArrayList<>();
+ loadedCertificateAliases.clear();
+ loadedCertificateChainAliases.clear();
+ loadedCertificateKeyAliases.clear();
+ certificateKeys.clear();
+ certificates.clear();
+ certificateChains.clear();
+ lastRefreshTime = null;
}
- boolean certificatesNeedRefresh() {
- if (keyVaultClient == null) {
+ synchronized boolean certificatesNeedRefresh() {
+ if (getKeyVaultClient() == null) {
return false;
}
if (lastRefreshTime == null) {
@@ -140,6 +256,45 @@ public Map getCertificateKeys() {
return certificateKeys;
}
+ /**
+ * Get key by alias.
+ *
+ * @param alias The alias.
+ * @return The key, or {@code null}.
+ */
+ public Key getCertificateKey(String alias) {
+ loadCertificateKeyIfNeeded(alias);
+ synchronized (this) {
+ return certificateKeys.get(alias);
+ }
+ }
+
+ /**
+ * Get certificate by alias.
+ *
+ * @param alias The alias.
+ * @return The certificate, or {@code null}.
+ */
+ public Certificate getCertificate(String alias) {
+ loadCertificateIfNeeded(alias);
+ synchronized (this) {
+ return certificates.get(alias);
+ }
+ }
+
+ /**
+ * Get certificate chain by alias.
+ *
+ * @param alias The alias.
+ * @return The certificate chain, or {@code null}.
+ */
+ public Certificate[] getCertificateChain(String alias) {
+ loadCertificateChainIfNeeded(alias);
+ synchronized (this) {
+ return certificateChains.get(alias);
+ }
+ }
+
private void refreshCertificatesIfNeeded() {
if (certificatesNeedRefresh()) { // Avoid acquiring the lock as much as possible.
synchronized (this) {
@@ -151,31 +306,133 @@ private void refreshCertificatesIfNeeded() {
}
/**
- * Refresh certificates. Including certificates, aliases, certificate keys, certificate chains.
+ * Refresh aliases and invalidate cached certificate details.
*/
public synchronized void refreshCertificates() {
- // When refreshing certificates, the update of the 3 variables should be an atomic operation.
- aliases = keyVaultClient.getAliases();
+ KeyVaultClient currentKeyVaultClient = getKeyVaultClient();
+ if (currentKeyVaultClient == null) {
+ clearCachedState();
+ return;
+ }
+
+ // Discover aliases from Key Vault and apply include/exclude regex filters.
+ aliases = Optional.ofNullable(currentKeyVaultClient.getAliases())
+ .orElse(Collections.emptyList())
+ .stream()
+ .filter(this::shouldIncludeAlias)
+ .sorted()
+ .collect(Collectors.toCollection(ArrayList::new));
+
+ loadedCertificateAliases.clear();
+ loadedCertificateChainAliases.clear();
+ loadedCertificateKeyAliases.clear();
certificateKeys.clear();
certificates.clear();
certificateChains.clear();
- Optional.ofNullable(aliases).orElse(Collections.emptyList()).forEach(alias -> {
- Key key = keyVaultClient.getKey(alias, null);
- if (!Objects.isNull(key)) {
- certificateKeys.put(alias, key);
+ lastRefreshTime = new Date();
+ }
+
+ private void loadCertificateIfNeeded(String alias) {
+ refreshCertificatesIfNeeded();
+ KeyVaultClient currentKeyVaultClient = getKeyVaultClient();
+
+ if (alias == null || currentKeyVaultClient == null) {
+ return;
+ }
+
+ synchronized (this) {
+ if (loadedCertificateAliases.contains(alias)
+ || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) {
+ return;
}
- Certificate certificate = keyVaultClient.getCertificate(alias);
- if (!Objects.isNull(certificate)) {
- certificates.put(alias, certificate);
+ }
+
+ try {
+ Certificate loadedCertificate = currentKeyVaultClient.getCertificate(alias);
+ synchronized (this) {
+ if (currentKeyVaultClient != keyVaultClient
+ || loadedCertificateAliases.contains(alias)
+ || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) {
+ return;
+ }
+
+ if (loadedCertificate != null) {
+ certificates.put(alias, loadedCertificate);
+ loadedCertificateAliases.add(alias);
+ }
}
- Certificate[] certificateChain = keyVaultClient.getCertificateChain(alias);
- if (!Objects.isNull(certificateChain)) {
- certificateChains.put(alias, certificateChain);
+ } catch (RuntimeException exception) {
+ LOGGER.log(WARNING, exception, () -> "Failed to load certificate for alias: " + alias);
+ }
+ }
+
+ private void loadCertificateChainIfNeeded(String alias) {
+ refreshCertificatesIfNeeded();
+ KeyVaultClient currentKeyVaultClient = getKeyVaultClient();
+
+ if (alias == null || currentKeyVaultClient == null) {
+ return;
+ }
+
+ synchronized (this) {
+ if (loadedCertificateChainAliases.contains(alias)
+ || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) {
+ return;
}
- });
+ }
- lastRefreshTime = new Date();
+ try {
+ Certificate[] loadedCertificateChain = currentKeyVaultClient.getCertificateChain(alias);
+ synchronized (this) {
+ if (currentKeyVaultClient != keyVaultClient
+ || loadedCertificateChainAliases.contains(alias)
+ || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) {
+ return;
+ }
+
+ if (loadedCertificateChain != null && loadedCertificateChain.length > 0) {
+ certificateChains.put(alias, loadedCertificateChain);
+ loadedCertificateChainAliases.add(alias);
+ }
+ }
+ } catch (RuntimeException exception) {
+ LOGGER.log(WARNING, exception, () -> "Failed to load certificate chain for alias: " + alias);
+ }
+ }
+
+ private void loadCertificateKeyIfNeeded(String alias) {
+ refreshCertificatesIfNeeded();
+ KeyVaultClient currentKeyVaultClient = getKeyVaultClient();
+
+ if (alias == null || currentKeyVaultClient == null) {
+ return;
+ }
+
+ synchronized (this) {
+ if (loadedCertificateKeyAliases.contains(alias)
+ || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) {
+ return;
+ }
+ }
+
+ try {
+ Key loadedKey = currentKeyVaultClient.getKey(alias, null);
+ synchronized (this) {
+ if (currentKeyVaultClient != keyVaultClient
+ || loadedCertificateKeyAliases.contains(alias)
+ || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) {
+ return;
+ }
+
+ if (loadedKey != null) {
+ certificateKeys.put(alias, loadedKey);
+ loadedCertificateKeyAliases.add(alias);
+ }
+ }
+ } catch (RuntimeException exception) {
+ LOGGER.log(WARNING, exception, () -> "Failed to load certificate key for alias: " + alias);
+ }
}
/**
@@ -186,8 +443,25 @@ public synchronized void refreshCertificates() {
* @return Certificate alias if it exists.
*/
public String refreshAndGetAliasByCertificate(Certificate certificate) {
+ if (certificate == null) {
+ return null;
+ }
+
refreshCertificates();
- return getCertificates().entrySet()
+
+ List aliasesSnapshot;
+ synchronized (this) {
+ aliasesSnapshot = new ArrayList<>(Optional.ofNullable(aliases).orElse(Collections.emptyList()));
+ }
+
+ aliasesSnapshot.forEach(this::loadCertificateIfNeeded);
+
+ Map certificatesSnapshot;
+ synchronized (this) {
+ certificatesSnapshot = new HashMap<>(certificates);
+ }
+
+ return certificatesSnapshot.entrySet()
.stream()
.filter(entry -> certificate.equals(entry.getValue()))
.findFirst()
@@ -202,10 +476,13 @@ public String refreshAndGetAliasByCertificate(Certificate certificate) {
* @param alias Deleted certificate.
*/
@Override
- public void deleteEntry(String alias) {
+ public synchronized void deleteEntry(String alias) {
if (aliases != null) {
aliases.remove(alias);
}
+ loadedCertificateAliases.remove(alias);
+ loadedCertificateChainAliases.remove(alias);
+ loadedCertificateKeyAliases.remove(alias);
certificates.remove(alias);
certificateChains.remove(alias);
certificateKeys.remove(alias);
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultKeyStoreUnitTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultKeyStoreUnitTest.java
index 9e290eaf45fc..0f3880199234 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultKeyStoreUnitTest.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultKeyStoreUnitTest.java
@@ -4,6 +4,8 @@
package com.azure.security.keyvault.jca;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.ResourceLock;
+import org.junit.jupiter.api.parallel.Resources;
import java.io.ByteArrayInputStream;
import java.security.ProviderException;
@@ -11,10 +13,13 @@
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;
+import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+@ResourceLock(Resources.SYSTEM_PROPERTIES)
public class KeyVaultKeyStoreUnitTest {
/**
@@ -91,4 +96,30 @@ public void testEngineSetCertificateEntry() {
assertNotNull(keystore.engineGetCertificate("setcert"));
}
+ @Test
+ public void testGetKeyVaultCertificateAliasFilterPatterns() {
+ String originalFilterPatterns = System.getProperty(KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY);
+ try {
+ System.clearProperty(KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY);
+ KeyVaultKeyStore keyVaultKeyStore = new KeyVaultKeyStore();
+ assertTrue(keyVaultKeyStore.getKeyVaultCertificateAliasFilterPatterns().isEmpty());
+
+ System.setProperty(KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY,
+ "^prod-.*,!^prod-deprecated-.*,,myalias");
+ keyVaultKeyStore = new KeyVaultKeyStore();
+ Set patterns = keyVaultKeyStore.getKeyVaultCertificateAliasFilterPatterns();
+
+ assertEquals(3, patterns.size());
+ assertTrue(patterns.contains("^prod-.*"));
+ assertTrue(patterns.contains("!^prod-deprecated-.*"));
+ assertTrue(patterns.contains("myalias"));
+ } finally {
+ if (originalFilterPatterns == null) {
+ System.clearProperty(KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY);
+ } else {
+ System.setProperty(KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY, originalFilterPatterns);
+ }
+ }
+ }
+
}
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificatesTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificatesTest.java
index 2791d1742e6a..1c8e3872f3fa 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificatesTest.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificatesTest.java
@@ -4,13 +4,20 @@
package com.azure.security.keyvault.jca.implementation.certificates;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.azure.security.keyvault.jca.implementation.KeyVaultClient;
import java.security.Key;
import java.security.cert.Certificate;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -23,6 +30,8 @@ public class KeyVaultCertificatesTest {
private final Certificate certificate = mock(Certificate.class);
+ private final Certificate[] certificateChain = new Certificate[] { certificate };
+
private KeyVaultCertificates keyVaultCertificates;
@BeforeEach
@@ -32,6 +41,7 @@ public void beforeEach() {
when(keyVaultClient.getAliases()).thenReturn(aliases);
when(keyVaultClient.getKey("myalias", null)).thenReturn(key);
when(keyVaultClient.getCertificate("myalias")).thenReturn(certificate);
+ when(keyVaultClient.getCertificateChain("myalias")).thenReturn(certificateChain);
keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient);
}
@@ -42,12 +52,17 @@ public void testGetAliases() {
@Test
public void testGetKey() {
- Assertions.assertTrue(keyVaultCertificates.getCertificateKeys().containsValue(key));
+ Assertions.assertEquals(key, keyVaultCertificates.getCertificateKey("myalias"));
}
@Test
public void testGetCertificate() {
- Assertions.assertTrue(keyVaultCertificates.getCertificates().containsValue(certificate));
+ Assertions.assertEquals(certificate, keyVaultCertificates.getCertificate("myalias"));
+ }
+
+ @Test
+ public void testGetCertificateChain() {
+ Assertions.assertArrayEquals(certificateChain, keyVaultCertificates.getCertificateChain("myalias"));
}
@Test
@@ -59,6 +74,11 @@ public void testRefreshAndGetAliasByCertificate() {
Assertions.assertNull(keyVaultCertificates.getCertificates().get("myalias"));
}
+ @Test
+ public void testRefreshAndGetAliasByCertificateWithNullCertificate() {
+ Assertions.assertNull(keyVaultCertificates.refreshAndGetAliasByCertificate(null));
+ }
+
@Test
public void testDeleteAlias() {
Assertions.assertTrue(keyVaultCertificates.getAliases().contains("myalias"));
@@ -66,4 +86,233 @@ public void testDeleteAlias() {
Assertions.assertFalse(keyVaultCertificates.getAliases().contains("myalias"));
}
+ @Test
+ public void testGetAliasesDoesNotLoadCertificateDetailsEagerly() {
+ keyVaultCertificates.getAliases();
+
+ verify(keyVaultClient, never()).getKey("myalias", null);
+ verify(keyVaultClient, never()).getCertificate("myalias");
+ verify(keyVaultClient, never()).getCertificateChain("myalias");
+ }
+
+ @Test
+ public void testLoadCertificateDetailsForRequestedAliasOnly() {
+ List aliases = new ArrayList<>();
+ aliases.add("myalias");
+ aliases.add("otheralias");
+
+ Key otherKey = mock(Key.class);
+ Certificate otherCertificate = mock(Certificate.class);
+
+ when(keyVaultClient.getAliases()).thenReturn(aliases);
+ when(keyVaultClient.getKey("otheralias", null)).thenReturn(otherKey);
+ when(keyVaultClient.getCertificate("otheralias")).thenReturn(otherCertificate);
+
+ keyVaultCertificates.getCertificate("myalias");
+
+ verify(keyVaultClient, times(1)).getCertificate("myalias");
+ verify(keyVaultClient, never()).getKey("myalias", null);
+ verify(keyVaultClient, never()).getCertificateChain("myalias");
+ verify(keyVaultClient, never()).getKey("otheralias", null);
+ verify(keyVaultClient, never()).getCertificate("otheralias");
+ verify(keyVaultClient, never()).getCertificateChain("otheralias");
+ }
+
+ @Test
+ public void testGetKeyLoadsOnlyKeyForRequestedAlias() {
+ keyVaultCertificates.getCertificateKey("myalias");
+
+ verify(keyVaultClient, times(1)).getKey("myalias", null);
+ verify(keyVaultClient, never()).getCertificate("myalias");
+ verify(keyVaultClient, never()).getCertificateChain("myalias");
+ }
+
+ @Test
+ public void testGetCertificateChainLoadsOnlyChainForRequestedAlias() {
+ keyVaultCertificates.getCertificateChain("myalias");
+
+ verify(keyVaultClient, times(1)).getCertificateChain("myalias");
+ verify(keyVaultClient, never()).getKey("myalias", null);
+ verify(keyVaultClient, never()).getCertificate("myalias");
+ }
+
+ @Test
+ public void testConfiguredAliasesFilter() {
+ List aliases = new ArrayList<>();
+ aliases.add("myalias");
+ aliases.add("otheralias");
+ when(keyVaultClient.getAliases()).thenReturn(aliases);
+
+ keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("myalias"));
+
+ List result = keyVaultCertificates.getAliases();
+ Assertions.assertEquals(1, result.size());
+ Assertions.assertTrue(result.contains("myalias"));
+ Assertions.assertFalse(result.contains("otheralias"));
+ }
+
+ @Test
+ public void testFilterPatternsIncludeRegex() {
+ List aliases = new ArrayList<>();
+ aliases.add("prod-cert");
+ aliases.add("dev-cert");
+ when(keyVaultClient.getAliases()).thenReturn(aliases);
+
+ keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("^prod-.*"));
+
+ Assertions.assertEquals(Collections.singletonList("prod-cert"), keyVaultCertificates.getAliases());
+ verify(keyVaultClient, times(1)).getAliases();
+ }
+
+ @Test
+ public void testFilterPatternsExcludeRegex() {
+ List aliases = new ArrayList<>();
+ aliases.add("prod-active");
+ aliases.add("prod-deprecated");
+ when(keyVaultClient.getAliases()).thenReturn(aliases);
+
+ Set filterPatterns = new HashSet<>(Arrays.asList("^prod-.*", "!^prod-deprecated$"));
+ keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, filterPatterns);
+
+ Assertions.assertEquals(Collections.singletonList("prod-active"), keyVaultCertificates.getAliases());
+ verify(keyVaultClient, times(1)).getAliases();
+ }
+
+ @Test
+ public void testConfiguredAliasesFilterAfterRefresh() {
+ keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("myalias"));
+
+ Assertions.assertEquals(Collections.singletonList("myalias"), keyVaultCertificates.getAliases());
+
+ keyVaultCertificates.refreshCertificates();
+
+ List refreshedAliases = keyVaultCertificates.getAliases();
+ Assertions.assertEquals(1, refreshedAliases.size());
+ Assertions.assertTrue(refreshedAliases.contains("myalias"));
+ Assertions.assertFalse(refreshedAliases.contains("otheralias"));
+ Assertions.assertFalse(refreshedAliases.contains("new"));
+ verify(keyVaultClient, times(2)).getAliases();
+ }
+
+ @Test
+ public void testConfiguredAliasesFilterUsesListApi() {
+ when(keyVaultClient.getAliases()).thenReturn(Arrays.asList("configured-alias", "other-alias"));
+
+ keyVaultCertificates
+ = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("configured-alias"));
+
+ Assertions.assertEquals(Collections.singletonList("configured-alias"), keyVaultCertificates.getAliases());
+ verify(keyVaultClient, times(1)).getAliases();
+ }
+
+ @Test
+ public void testConfiguredAliasesIgnoreNullEntries() {
+ Set configuredAliases = new HashSet<>(Arrays.asList("myalias", null));
+ keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, configuredAliases);
+
+ Assertions.assertEquals(Collections.singletonList("myalias"), keyVaultCertificates.getAliases());
+ verify(keyVaultClient, times(1)).getAliases();
+ }
+
+ @Test
+ public void testInvalidFilterPatternThrows() {
+ Set filterPatterns = new HashSet<>(Collections.singletonList("[invalid"));
+
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> new KeyVaultCertificates(60_000, keyVaultClient, filterPatterns));
+ }
+
+ @Test
+ public void testGetCertificateWithUnconfiguredAliasDoesNotFetchDetails() {
+ keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("myalias"));
+
+ Assertions.assertNull(keyVaultCertificates.getCertificate("otheralias"));
+
+ verify(keyVaultClient, never()).getKey("otheralias", null);
+ verify(keyVaultClient, never()).getCertificate("otheralias");
+ verify(keyVaultClient, never()).getCertificateChain("otheralias");
+ }
+
+ @Test
+ public void testAliasCertificateLoadFailureIsRetriedOnNextAccess() {
+ when(keyVaultClient.getCertificate("myalias")).thenThrow(new RuntimeException("transient error"))
+ .thenReturn(certificate);
+
+ Assertions.assertNull(keyVaultCertificates.getCertificate("myalias"));
+ Assertions.assertEquals(certificate, keyVaultCertificates.getCertificate("myalias"));
+ verify(keyVaultClient, times(2)).getCertificate("myalias");
+ verify(keyVaultClient, never()).getKey("myalias", null);
+ verify(keyVaultClient, never()).getCertificateChain("myalias");
+ }
+
+ @Test
+ public void testAliasCertificateNullLoadIsRetriedOnNextAccess() {
+ when(keyVaultClient.getCertificate("myalias")).thenReturn(null).thenReturn(certificate);
+
+ Assertions.assertNull(keyVaultCertificates.getCertificate("myalias"));
+ Assertions.assertEquals(certificate, keyVaultCertificates.getCertificate("myalias"));
+ verify(keyVaultClient, times(2)).getCertificate("myalias");
+ verify(keyVaultClient, never()).getKey("myalias", null);
+ verify(keyVaultClient, never()).getCertificateChain("myalias");
+ }
+
+ @Test
+ public void testAliasKeyLoadFailureIsRetriedOnNextAccess() {
+ when(keyVaultClient.getKey("myalias", null)).thenThrow(new RuntimeException("transient error")).thenReturn(key);
+
+ Assertions.assertNull(keyVaultCertificates.getCertificateKey("myalias"));
+ Assertions.assertEquals(key, keyVaultCertificates.getCertificateKey("myalias"));
+ verify(keyVaultClient, times(2)).getKey("myalias", null);
+ verify(keyVaultClient, never()).getCertificate("myalias");
+ verify(keyVaultClient, never()).getCertificateChain("myalias");
+ }
+
+ @Test
+ public void testAliasKeyNullLoadIsRetriedOnNextAccess() {
+ when(keyVaultClient.getKey("myalias", null)).thenReturn(null).thenReturn(key);
+
+ Assertions.assertNull(keyVaultCertificates.getCertificateKey("myalias"));
+ Assertions.assertEquals(key, keyVaultCertificates.getCertificateKey("myalias"));
+ verify(keyVaultClient, times(2)).getKey("myalias", null);
+ verify(keyVaultClient, never()).getCertificate("myalias");
+ verify(keyVaultClient, never()).getCertificateChain("myalias");
+ }
+
+ @Test
+ public void testAliasChainLoadFailureIsRetriedOnNextAccess() {
+ when(keyVaultClient.getCertificateChain("myalias")).thenThrow(new RuntimeException("transient error"))
+ .thenReturn(certificateChain);
+
+ Assertions.assertNull(keyVaultCertificates.getCertificateChain("myalias"));
+ Assertions.assertArrayEquals(certificateChain, keyVaultCertificates.getCertificateChain("myalias"));
+ verify(keyVaultClient, times(2)).getCertificateChain("myalias");
+ verify(keyVaultClient, never()).getCertificate("myalias");
+ verify(keyVaultClient, never()).getKey("myalias", null);
+ }
+
+ @Test
+ public void testAliasChainEmptyLoadIsRetriedOnNextAccess() {
+ when(keyVaultClient.getCertificateChain("myalias")).thenReturn(new Certificate[0]).thenReturn(certificateChain);
+
+ Assertions.assertNull(keyVaultCertificates.getCertificateChain("myalias"));
+ Assertions.assertArrayEquals(certificateChain, keyVaultCertificates.getCertificateChain("myalias"));
+ verify(keyVaultClient, times(2)).getCertificateChain("myalias");
+ verify(keyVaultClient, never()).getCertificate("myalias");
+ verify(keyVaultClient, never()).getKey("myalias", null);
+ }
+
+ @Test
+ public void testUpdateKeyVaultClientClearsCachedState() {
+ Assertions.assertTrue(keyVaultCertificates.getAliases().contains("myalias"));
+ Assertions.assertEquals(certificate, keyVaultCertificates.getCertificate("myalias"));
+
+ keyVaultCertificates.updateKeyVaultClient(null, null, null, null, null, null, false);
+
+ Assertions.assertTrue(keyVaultCertificates.getAliases().isEmpty());
+ Assertions.assertTrue(keyVaultCertificates.getCertificates().isEmpty());
+ Assertions.assertTrue(keyVaultCertificates.getCertificateChains().isEmpty());
+ Assertions.assertTrue(keyVaultCertificates.getCertificateKeys().isEmpty());
+ Assertions.assertNull(keyVaultCertificates.getCertificate("myalias"));
+ }
+
}