From 6c8ec909f1812b5b120cd0f94e1cd47774ec62dc Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 09:40:16 +0800 Subject: [PATCH 01/36] Add configured certificate filtering and lazy loading in KeyVault JCA --- .../azure-security-keyvault-jca/CHANGELOG.md | 2 + .../azure-security-keyvault-jca/README.md | 1 + .../keyvault/jca/KeyVaultKeyStore.java | 69 +++++++--- .../certificates/KeyVaultCertificates.java | 120 +++++++++++++++--- .../jca/KeyVaultKeyStoreUnitTest.java | 49 +++++++ .../KeyVaultCertificatesTest.java | 107 +++++++++++++++- 6 files changed, 312 insertions(+), 36 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md index 034766972361..34ab7148d6cb 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. +- Added support for `azure.keyvault.jca.certificates` to specify a comma-separated list of Key Vault certificate aliases to load. When set, only these aliases are considered from Key Vault. ([#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..fd8861757c80 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.certificates`: Comma-separated certificate aliases to load from Key Vault. When set, only these aliases are considered from Key Vault. * `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/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..e79fb1db01bc 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,8 +29,10 @@ 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.Stream; +import java.util.stream.Collectors; import static java.util.logging.Level.FINE; import static java.util.logging.Level.WARNING; @@ -56,6 +58,8 @@ public final class KeyVaultKeyStore extends KeyStoreSpi { */ private static final Logger LOGGER = Logger.getLogger(KeyVaultKeyStore.class.getName()); + static final String CONFIGURED_CERTIFICATES_PROPERTY = "azure.keyvault.jca.certificates"; + /** * Stores the Jre key store certificates. */ @@ -146,8 +150,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, getConfiguredKeyVaultCertificateAliases()); LOGGER.log(FINE, String.format("Loaded Key Vault certificates: %s.", keyVaultCertificates.getAliases())); classpathCertificates = new ClasspathCertificates(); @@ -168,6 +173,15 @@ Long getRefreshInterval() { .orElse(0L); } + Set getConfiguredKeyVaultCertificateAliases() { + return Optional.ofNullable(System.getProperty(CONFIGURED_CERTIFICATES_PROPERTY)) + .map(value -> Stream.of(value.split(",")) + .map(String::trim) + .filter(alias -> !alias.isEmpty()) + .collect(Collectors.toSet())) + .orElse(Collections.emptySet()); + } + /** * get key vault key store by system property * @@ -254,16 +268,22 @@ public boolean engineEntryInstanceOf(String alias, Class a.containsKey(alias)) - .findFirst() - .map(certificates -> certificates.get(alias)) - .orElse(null); + Certificate certificate + = Arrays.asList(jreCertificates, wellKnowCertificates, customCertificates, classpathCertificates) + .stream() + .map(AzureCertificates::getCertificates) + .filter(a -> a.containsKey(alias)) + .findFirst() + .map(certificates -> certificates.get(alias)) + .orElse(null); + + if (certificate == null) { + certificate = keyVaultCertificates.getCertificate(alias); + } if (refreshCertificatesWhenHaveUnTrustCertificate && certificate == null) { keyVaultCertificates.refreshCertificates(); - certificate = keyVaultCertificates.getCertificates().get(alias); + certificate = keyVaultCertificates.getCertificate(alias); } return certificate; @@ -307,16 +327,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 + = Arrays.asList(jreCertificates, wellKnowCertificates, customCertificates, classpathCertificates) + .stream() + .map(AzureCertificates::getCertificateChains) + .filter(Objects::nonNull) + .filter(a -> a.containsKey(alias)) + .findFirst() + .map(m -> m.get(alias)) + .orElse(null); + if (certificates == null) { + certificates = keyVaultCertificates.getCertificateChain(alias); + } + if (refreshCertificatesWhenHaveUnTrustCertificate && certificates == null) { keyVaultCertificates.refreshCertificates(); - return keyVaultCertificates.getCertificateChains().get(alias); + return keyVaultCertificates.getCertificateChain(alias); } return certificates; } @@ -358,12 +384,19 @@ public KeyStore.Entry engineGetEntry(String alias, KeyStore.ProtectionParameter */ @Override public Key engineGetKey(String alias, char[] password) { - return allCertificates.stream() + Key key = Arrays.asList(jreCertificates, wellKnowCertificates, customCertificates, classpathCertificates) + .stream() .map(AzureCertificates::getCertificateKeys) .filter(a -> a.containsKey(alias)) .findFirst() .map(certificateKeys -> certificateKeys.get(alias)) .orElse(null); + + if (key == null) { + key = keyVaultCertificates.getCertificateKey(alias); + } + + 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..ca4c84508e08 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,33 @@ 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 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 details have already been loaded. + */ + private final Set loadedAliases = new HashSet<>(); + /** * Stores the certificates by alias. */ @@ -49,18 +62,36 @@ public final class KeyVaultCertificates implements AzureCertificates { private final long refreshInterval; + private final Set configuredCertificateAliases; + 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 configuredCertificateAliases) { this.refreshInterval = refreshInterval; + this.configuredCertificateAliases + = new HashSet<>(Optional.ofNullable(configuredCertificateAliases).orElse(Collections.emptySet())); 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 configuredCertificateAliases) { this.refreshInterval = refreshInterval; this.keyVaultClient = keyVaultClient; + this.configuredCertificateAliases + = new HashSet<>(Optional.ofNullable(configuredCertificateAliases).orElse(Collections.emptySet())); } /** @@ -140,6 +171,39 @@ public Map getCertificateKeys() { return certificateKeys; } + /** + * Get key by alias. + * + * @param alias The alias. + * @return The key, or {@code null}. + */ + public Key getCertificateKey(String alias) { + loadCertificateDetailsIfNeeded(alias); + return certificateKeys.get(alias); + } + + /** + * Get certificate by alias. + * + * @param alias The alias. + * @return The certificate, or {@code null}. + */ + public Certificate getCertificate(String alias) { + loadCertificateDetailsIfNeeded(alias); + 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) { + loadCertificateDetailsIfNeeded(alias); + return certificateChains.get(alias); + } + private void refreshCertificatesIfNeeded() { if (certificatesNeedRefresh()) { // Avoid acquiring the lock as much as possible. synchronized (this) { @@ -154,28 +218,48 @@ private void refreshCertificatesIfNeeded() { * Refresh certificates. Including certificates, aliases, certificate keys, certificate chains. */ public synchronized void refreshCertificates() { - // When refreshing certificates, the update of the 3 variables should be an atomic operation. - aliases = keyVaultClient.getAliases(); + // Refresh alias list only. Certificate details are loaded lazily per alias when requested. + List refreshedAliases + = Optional.ofNullable(keyVaultClient.getAliases()).orElse(Collections.emptyList()); + if (configuredCertificateAliases.isEmpty()) { + aliases = refreshedAliases; + } else { + aliases + = refreshedAliases.stream().filter(configuredCertificateAliases::contains).collect(Collectors.toList()); + } + + loadedAliases.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); - } - Certificate certificate = keyVaultClient.getCertificate(alias); - if (!Objects.isNull(certificate)) { - certificates.put(alias, certificate); - } - Certificate[] certificateChain = keyVaultClient.getCertificateChain(alias); - if (!Objects.isNull(certificateChain)) { - certificateChains.put(alias, certificateChain); + lastRefreshTime = new Date(); + } + + private void loadCertificateDetailsIfNeeded(String alias) { + refreshCertificatesIfNeeded(); + + if (alias == null || keyVaultClient == null) { + return; + } + + synchronized (this) { + if (loadedAliases.contains(alias) + || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + return; } - }); - lastRefreshTime = new Date(); + try { + certificateKeys.put(alias, keyVaultClient.getKey(alias, null)); + certificates.put(alias, keyVaultClient.getCertificate(alias)); + certificateChains.put(alias, keyVaultClient.getCertificateChain(alias)); + } catch (RuntimeException exception) { + LOGGER.log(WARNING, String.format("Failed to load certificate details for alias: %s", alias), + exception); + } finally { + loadedAliases.add(alias); + } + } } /** @@ -187,6 +271,9 @@ public synchronized void refreshCertificates() { */ public String refreshAndGetAliasByCertificate(Certificate certificate) { refreshCertificates(); + + Optional.ofNullable(aliases).orElse(Collections.emptyList()).forEach(this::loadCertificateDetailsIfNeeded); + return getCertificates().entrySet() .stream() .filter(entry -> certificate.equals(entry.getValue())) @@ -206,6 +293,7 @@ public void deleteEntry(String alias) { if (aliases != null) { aliases.remove(alias); } + loadedAliases.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..a0a4308360e5 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 @@ -3,18 +3,26 @@ package com.azure.security.keyvault.jca; +import com.azure.security.keyvault.jca.implementation.certificates.KeyVaultCertificates; + 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.lang.reflect.Field; import java.security.ProviderException; import java.security.cert.CertificateException; 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 +99,45 @@ public void testEngineSetCertificateEntry() { assertNotNull(keystore.engineGetCertificate("setcert")); } + @Test + public void testGetConfiguredKeyVaultCertificateAliases() { + System.clearProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY); + KeyVaultKeyStore keyVaultKeyStore = new KeyVaultKeyStore(); + assertTrue(keyVaultKeyStore.getConfiguredKeyVaultCertificateAliases().isEmpty()); + + System.setProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY, "cert1, cert2 ,, cert3 "); + keyVaultKeyStore = new KeyVaultKeyStore(); + Set aliases = keyVaultKeyStore.getConfiguredKeyVaultCertificateAliases(); + + assertEquals(3, aliases.size()); + assertTrue(aliases.contains("cert1")); + assertTrue(aliases.contains("cert2")); + assertTrue(aliases.contains("cert3")); + + System.clearProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY); + } + + @Test + public void testConfiguredAliasesWiredToKeyVaultCertificates() throws Exception { + try { + System.setProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY, "certA, certB"); + KeyVaultKeyStore keyVaultKeyStore = new KeyVaultKeyStore(); + + Field keyVaultCertificatesField = KeyVaultKeyStore.class.getDeclaredField("keyVaultCertificates"); + keyVaultCertificatesField.setAccessible(true); + KeyVaultCertificates keyVaultCertificates + = (KeyVaultCertificates) keyVaultCertificatesField.get(keyVaultKeyStore); + + Field configuredAliasesField = KeyVaultCertificates.class.getDeclaredField("configuredCertificateAliases"); + configuredAliasesField.setAccessible(true); + Set configuredAliases = (Set) configuredAliasesField.get(keyVaultCertificates); + + assertEquals(2, configuredAliases.size()); + assertTrue(configuredAliases.contains("certA")); + assertTrue(configuredAliases.contains("certB")); + } finally { + System.clearProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY); + } + } + } 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..1d8da57fda41 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,12 +4,17 @@ 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.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -23,6 +28,8 @@ public class KeyVaultCertificatesTest { private final Certificate certificate = mock(Certificate.class); + private final Certificate[] certificateChain = new Certificate[] { certificate }; + private KeyVaultCertificates keyVaultCertificates; @BeforeEach @@ -32,6 +39,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 +50,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 @@ -66,4 +79,94 @@ 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)).getKey("myalias", null); + verify(keyVaultClient, times(1)).getCertificate("myalias"); + verify(keyVaultClient, times(1)).getCertificateChain("myalias"); + verify(keyVaultClient, never()).getKey("otheralias", null); + verify(keyVaultClient, never()).getCertificate("otheralias"); + verify(keyVaultClient, never()).getCertificateChain("otheralias"); + } + + @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 testConfiguredAliasesFilterAfterRefresh() { + when(keyVaultClient.getAliases()).thenReturn(Arrays.asList("myalias", "otheralias")); + when(keyVaultClient.getAliases()).thenReturn(Arrays.asList("otheralias", "myalias", "new")); + + 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")); + } + + @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 testAliasLoadFailureIsNotRetriedUntilRefresh() { + when(keyVaultClient.getKey("myalias", null)).thenThrow(new RuntimeException("transient error")).thenReturn(key); + + Assertions.assertNull(keyVaultCertificates.getCertificate("myalias")); + Assertions.assertNull(keyVaultCertificates.getCertificate("myalias")); + verify(keyVaultClient, times(1)).getKey("myalias", null); + + keyVaultCertificates.refreshCertificates(); + + Assertions.assertEquals(certificate, keyVaultCertificates.getCertificate("myalias")); + verify(keyVaultClient, times(2)).getKey("myalias", null); + } + } From 86621f2aaac103669b9490f2573dca2e6c4d44d6 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 09:45:21 +0800 Subject: [PATCH 02/36] Add PR link to keyvault-jca changelog entry --- sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md index 34ab7148d6cb..292c2e175a31 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md +++ b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md @@ -3,7 +3,7 @@ ## 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. +- 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.certificates` to specify a comma-separated list of Key Vault certificate aliases to load. When set, only these aliases are considered from Key Vault. ([#39487](https://github.com/Azure/azure-sdk-for-java/issues/39487)) ### Breaking Changes From f4371a4c5cc48ef3cdf2574097f3e0b60dc98245 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 09:52:05 +0800 Subject: [PATCH 03/36] Address PR review comments for alias refresh and thread safety --- .../certificates/KeyVaultCertificates.java | 12 +++++++++++- .../certificates/KeyVaultCertificatesTest.java | 10 ++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) 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 ca4c84508e08..a6039fc99495 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 @@ -218,6 +218,16 @@ private void refreshCertificatesIfNeeded() { * Refresh certificates. Including certificates, aliases, certificate keys, certificate chains. */ public synchronized void refreshCertificates() { + if (keyVaultClient == null) { + aliases = new ArrayList<>(); + loadedAliases.clear(); + certificateKeys.clear(); + certificates.clear(); + certificateChains.clear(); + lastRefreshTime = new Date(); + return; + } + // Refresh alias list only. Certificate details are loaded lazily per alias when requested. List refreshedAliases = Optional.ofNullable(keyVaultClient.getAliases()).orElse(Collections.emptyList()); @@ -289,7 +299,7 @@ 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); } 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 1d8da57fda41..03ba3f700542 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 @@ -16,6 +16,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -128,8 +129,13 @@ public void testConfiguredAliasesFilter() { @Test public void testConfiguredAliasesFilterAfterRefresh() { - when(keyVaultClient.getAliases()).thenReturn(Arrays.asList("myalias", "otheralias")); - when(keyVaultClient.getAliases()).thenReturn(Arrays.asList("otheralias", "myalias", "new")); + AtomicInteger invocationCount = new AtomicInteger(0); + when(keyVaultClient.getAliases()).thenAnswer(invocation -> { + if (invocationCount.getAndIncrement() == 0) { + return Arrays.asList("myalias", "otheralias"); + } + return Arrays.asList("otheralias", "myalias", "new"); + }); keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("myalias")); From 5507d46c37bd4622b4a91ab97701de3ce3fb4da0 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 10:00:58 +0800 Subject: [PATCH 04/36] Address additional PR review feedback on thread safety and refresh behavior --- .../certificates/KeyVaultCertificates.java | 27 ++++++++++++------- .../jca/KeyVaultKeyStoreUnitTest.java | 26 +++++++++--------- .../KeyVaultCertificatesTest.java | 7 +---- 3 files changed, 33 insertions(+), 27 deletions(-) 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 a6039fc99495..4ae2c6d04b3d 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 @@ -179,7 +179,9 @@ public Map getCertificateKeys() { */ public Key getCertificateKey(String alias) { loadCertificateDetailsIfNeeded(alias); - return certificateKeys.get(alias); + synchronized (this) { + return certificateKeys.get(alias); + } } /** @@ -190,7 +192,9 @@ public Key getCertificateKey(String alias) { */ public Certificate getCertificate(String alias) { loadCertificateDetailsIfNeeded(alias); - return certificates.get(alias); + synchronized (this) { + return certificates.get(alias); + } } /** @@ -201,7 +205,9 @@ public Certificate getCertificate(String alias) { */ public Certificate[] getCertificateChain(String alias) { loadCertificateDetailsIfNeeded(alias); - return certificateChains.get(alias); + synchronized (this) { + return certificateChains.get(alias); + } } private void refreshCertificatesIfNeeded() { @@ -224,7 +230,7 @@ public synchronized void refreshCertificates() { certificateKeys.clear(); certificates.clear(); certificateChains.clear(); - lastRefreshTime = new Date(); + lastRefreshTime = null; return; } @@ -260,14 +266,17 @@ private void loadCertificateDetailsIfNeeded(String alias) { } try { - certificateKeys.put(alias, keyVaultClient.getKey(alias, null)); - certificates.put(alias, keyVaultClient.getCertificate(alias)); - certificateChains.put(alias, keyVaultClient.getCertificateChain(alias)); + Key loadedKey = keyVaultClient.getKey(alias, null); + Certificate loadedCertificate = keyVaultClient.getCertificate(alias); + Certificate[] loadedCertificateChain = keyVaultClient.getCertificateChain(alias); + + certificateKeys.put(alias, loadedKey); + certificates.put(alias, loadedCertificate); + certificateChains.put(alias, loadedCertificateChain); + loadedAliases.add(alias); } catch (RuntimeException exception) { LOGGER.log(WARNING, String.format("Failed to load certificate details for alias: %s", alias), exception); - } finally { - loadedAliases.add(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 a0a4308360e5..bf0d780165af 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 @@ -101,20 +101,22 @@ public void testEngineSetCertificateEntry() { @Test public void testGetConfiguredKeyVaultCertificateAliases() { - System.clearProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY); - KeyVaultKeyStore keyVaultKeyStore = new KeyVaultKeyStore(); - assertTrue(keyVaultKeyStore.getConfiguredKeyVaultCertificateAliases().isEmpty()); - - System.setProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY, "cert1, cert2 ,, cert3 "); - keyVaultKeyStore = new KeyVaultKeyStore(); - Set aliases = keyVaultKeyStore.getConfiguredKeyVaultCertificateAliases(); + try { + System.clearProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY); + KeyVaultKeyStore keyVaultKeyStore = new KeyVaultKeyStore(); + assertTrue(keyVaultKeyStore.getConfiguredKeyVaultCertificateAliases().isEmpty()); - assertEquals(3, aliases.size()); - assertTrue(aliases.contains("cert1")); - assertTrue(aliases.contains("cert2")); - assertTrue(aliases.contains("cert3")); + System.setProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY, "cert1, cert2 ,, cert3 "); + keyVaultKeyStore = new KeyVaultKeyStore(); + Set aliases = keyVaultKeyStore.getConfiguredKeyVaultCertificateAliases(); - System.clearProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY); + assertEquals(3, aliases.size()); + assertTrue(aliases.contains("cert1")); + assertTrue(aliases.contains("cert2")); + assertTrue(aliases.contains("cert3")); + } finally { + System.clearProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY); + } } @Test 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 03ba3f700542..d4b350aa09c1 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 @@ -162,15 +162,10 @@ public void testGetCertificateWithUnconfiguredAliasDoesNotFetchDetails() { } @Test - public void testAliasLoadFailureIsNotRetriedUntilRefresh() { + public void testAliasLoadFailureIsRetriedOnNextAccess() { when(keyVaultClient.getKey("myalias", null)).thenThrow(new RuntimeException("transient error")).thenReturn(key); Assertions.assertNull(keyVaultCertificates.getCertificate("myalias")); - Assertions.assertNull(keyVaultCertificates.getCertificate("myalias")); - verify(keyVaultClient, times(1)).getKey("myalias", null); - - keyVaultCertificates.refreshCertificates(); - Assertions.assertEquals(certificate, keyVaultCertificates.getCertificate("myalias")); verify(keyVaultClient, times(2)).getKey("myalias", null); } From cc494aa6e067b188d8228fbcf138dffd9e65a78c Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 10:11:57 +0800 Subject: [PATCH 05/36] Make alias lookup use snapshots for thread-safe iteration --- .../certificates/KeyVaultCertificates.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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 4ae2c6d04b3d..2c812c485e90 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 @@ -291,9 +291,19 @@ private void loadCertificateDetailsIfNeeded(String alias) { public String refreshAndGetAliasByCertificate(Certificate certificate) { refreshCertificates(); - Optional.ofNullable(aliases).orElse(Collections.emptyList()).forEach(this::loadCertificateDetailsIfNeeded); + List aliasesSnapshot; + synchronized (this) { + aliasesSnapshot = new ArrayList<>(Optional.ofNullable(aliases).orElse(Collections.emptyList())); + } + + aliasesSnapshot.forEach(this::loadCertificateDetailsIfNeeded); + + Map certificatesSnapshot; + synchronized (this) { + certificatesSnapshot = new HashMap<>(certificates); + } - return getCertificates().entrySet() + return certificatesSnapshot.entrySet() .stream() .filter(entry -> certificate.equals(entry.getValue())) .findFirst() From b3cd9d5ef57ef740878d70b98214b38c691eef38 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 10:18:08 +0800 Subject: [PATCH 06/36] Ensure refreshed aliases list remains mutable --- .../implementation/certificates/KeyVaultCertificates.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 2c812c485e90..2af3587c8319 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 @@ -238,10 +238,11 @@ public synchronized void refreshCertificates() { List refreshedAliases = Optional.ofNullable(keyVaultClient.getAliases()).orElse(Collections.emptyList()); if (configuredCertificateAliases.isEmpty()) { - aliases = refreshedAliases; + aliases = new ArrayList<>(refreshedAliases); } else { - aliases - = refreshedAliases.stream().filter(configuredCertificateAliases::contains).collect(Collectors.toList()); + aliases = refreshedAliases.stream() + .filter(configuredCertificateAliases::contains) + .collect(Collectors.toCollection(ArrayList::new)); } loadedAliases.clear(); From 7875700bfea8ad4977571f1f6a608cae386d2f9f Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 10:24:59 +0800 Subject: [PATCH 07/36] Restore key vault precedence over classpath in keystore lookups --- .../keyvault/jca/KeyVaultKeyStore.java | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) 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 e79fb1db01bc..5ee37bcc78ab 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 @@ -268,19 +268,22 @@ public boolean engineEntryInstanceOf(String alias, Class a.containsKey(alias)) - .findFirst() - .map(certificates -> certificates.get(alias)) - .orElse(null); + Certificate certificate = Arrays.asList(jreCertificates, wellKnowCertificates, customCertificates) + .stream() + .map(AzureCertificates::getCertificates) + .filter(a -> a.containsKey(alias)) + .findFirst() + .map(certificates -> certificates.get(alias)) + .orElse(null); if (certificate == null) { certificate = keyVaultCertificates.getCertificate(alias); } + if (certificate == null) { + certificate = classpathCertificates.getCertificates().get(alias); + } + if (refreshCertificatesWhenHaveUnTrustCertificate && certificate == null) { keyVaultCertificates.refreshCertificates(); certificate = keyVaultCertificates.getCertificate(alias); @@ -327,19 +330,22 @@ public String engineGetCertificateAlias(Certificate cert) { */ @Override public Certificate[] engineGetCertificateChain(String alias) { - Certificate[] certificates - = Arrays.asList(jreCertificates, wellKnowCertificates, customCertificates, classpathCertificates) - .stream() - .map(AzureCertificates::getCertificateChains) - .filter(Objects::nonNull) - .filter(a -> a.containsKey(alias)) - .findFirst() - .map(m -> m.get(alias)) - .orElse(null); + Certificate[] certificates = Arrays.asList(jreCertificates, wellKnowCertificates, customCertificates) + .stream() + .map(AzureCertificates::getCertificateChains) + .filter(Objects::nonNull) + .filter(a -> a.containsKey(alias)) + .findFirst() + .map(m -> m.get(alias)) + .orElse(null); if (certificates == null) { certificates = keyVaultCertificates.getCertificateChain(alias); } + if (certificates == null) { + certificates = classpathCertificates.getCertificateChains().get(alias); + } + if (refreshCertificatesWhenHaveUnTrustCertificate && certificates == null) { keyVaultCertificates.refreshCertificates(); return keyVaultCertificates.getCertificateChain(alias); @@ -384,7 +390,7 @@ public KeyStore.Entry engineGetEntry(String alias, KeyStore.ProtectionParameter */ @Override public Key engineGetKey(String alias, char[] password) { - Key key = Arrays.asList(jreCertificates, wellKnowCertificates, customCertificates, classpathCertificates) + Key key = Arrays.asList(jreCertificates, wellKnowCertificates, customCertificates) .stream() .map(AzureCertificates::getCertificateKeys) .filter(a -> a.containsKey(alias)) @@ -396,6 +402,10 @@ public Key engineGetKey(String alias, char[] password) { key = keyVaultCertificates.getCertificateKey(alias); } + if (key == null) { + key = classpathCertificates.getCertificateKeys().get(alias); + } + return key; } From a3430593b4454a7535c2fa7638fa252938261e0c Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 10:34:36 +0800 Subject: [PATCH 08/36] Refine lazy key vault loads and remove brittle reflection test --- .../certificates/KeyVaultCertificates.java | 101 +++++++++++++----- .../jca/KeyVaultKeyStoreUnitTest.java | 26 ----- .../KeyVaultCertificatesTest.java | 40 ++++++- 3 files changed, 112 insertions(+), 55 deletions(-) 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 2af3587c8319..ad2b56f99f77 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 @@ -34,9 +34,19 @@ public final class KeyVaultCertificates implements AzureCertificates { private List aliases = new ArrayList<>(); /** - * Stores aliases whose certificate details have already been loaded. + * Stores aliases whose certificate has already been loaded. */ - private final Set loadedAliases = new HashSet<>(); + 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. @@ -178,7 +188,7 @@ public Map getCertificateKeys() { * @return The key, or {@code null}. */ public Key getCertificateKey(String alias) { - loadCertificateDetailsIfNeeded(alias); + loadCertificateKeyIfNeeded(alias); synchronized (this) { return certificateKeys.get(alias); } @@ -191,7 +201,7 @@ public Key getCertificateKey(String alias) { * @return The certificate, or {@code null}. */ public Certificate getCertificate(String alias) { - loadCertificateDetailsIfNeeded(alias); + loadCertificateIfNeeded(alias); synchronized (this) { return certificates.get(alias); } @@ -204,7 +214,7 @@ public Certificate getCertificate(String alias) { * @return The certificate chain, or {@code null}. */ public Certificate[] getCertificateChain(String alias) { - loadCertificateDetailsIfNeeded(alias); + loadCertificateChainIfNeeded(alias); synchronized (this) { return certificateChains.get(alias); } @@ -226,7 +236,9 @@ private void refreshCertificatesIfNeeded() { public synchronized void refreshCertificates() { if (keyVaultClient == null) { aliases = new ArrayList<>(); - loadedAliases.clear(); + loadedCertificateAliases.clear(); + loadedCertificateChainAliases.clear(); + loadedCertificateKeyAliases.clear(); certificateKeys.clear(); certificates.clear(); certificateChains.clear(); @@ -234,18 +246,17 @@ public synchronized void refreshCertificates() { return; } - // Refresh alias list only. Certificate details are loaded lazily per alias when requested. - List refreshedAliases - = Optional.ofNullable(keyVaultClient.getAliases()).orElse(Collections.emptyList()); if (configuredCertificateAliases.isEmpty()) { - aliases = new ArrayList<>(refreshedAliases); + // If aliases are not configured, discover aliases from Key Vault. + aliases = new ArrayList<>(Optional.ofNullable(keyVaultClient.getAliases()).orElse(Collections.emptyList())); } else { - aliases = refreshedAliases.stream() - .filter(configuredCertificateAliases::contains) - .collect(Collectors.toCollection(ArrayList::new)); + // If aliases are configured, avoid the list API and only use the configured aliases. + aliases = configuredCertificateAliases.stream().sorted().collect(Collectors.toCollection(ArrayList::new)); } - loadedAliases.clear(); + loadedCertificateAliases.clear(); + loadedCertificateChainAliases.clear(); + loadedCertificateKeyAliases.clear(); certificateKeys.clear(); certificates.clear(); certificateChains.clear(); @@ -253,7 +264,7 @@ public synchronized void refreshCertificates() { lastRefreshTime = new Date(); } - private void loadCertificateDetailsIfNeeded(String alias) { + private void loadCertificateIfNeeded(String alias) { refreshCertificatesIfNeeded(); if (alias == null || keyVaultClient == null) { @@ -261,23 +272,63 @@ private void loadCertificateDetailsIfNeeded(String alias) { } synchronized (this) { - if (loadedAliases.contains(alias) + if (loadedCertificateAliases.contains(alias) || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { return; } try { - Key loadedKey = keyVaultClient.getKey(alias, null); Certificate loadedCertificate = keyVaultClient.getCertificate(alias); + certificates.put(alias, loadedCertificate); + loadedCertificateAliases.add(alias); + } catch (RuntimeException exception) { + LOGGER.log(WARNING, String.format("Failed to load certificate for alias: %s", alias), exception); + } + } + } + + private void loadCertificateChainIfNeeded(String alias) { + refreshCertificatesIfNeeded(); + + if (alias == null || keyVaultClient == null) { + return; + } + + synchronized (this) { + if (loadedCertificateChainAliases.contains(alias) + || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + return; + } + + try { Certificate[] loadedCertificateChain = keyVaultClient.getCertificateChain(alias); + certificateChains.put(alias, loadedCertificateChain); + loadedCertificateChainAliases.add(alias); + } catch (RuntimeException exception) { + LOGGER.log(WARNING, String.format("Failed to load certificate chain for alias: %s", alias), exception); + } + } + } + + private void loadCertificateKeyIfNeeded(String alias) { + refreshCertificatesIfNeeded(); + + if (alias == null || keyVaultClient == null) { + return; + } + synchronized (this) { + if (loadedCertificateKeyAliases.contains(alias) + || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + return; + } + + try { + Key loadedKey = keyVaultClient.getKey(alias, null); certificateKeys.put(alias, loadedKey); - certificates.put(alias, loadedCertificate); - certificateChains.put(alias, loadedCertificateChain); - loadedAliases.add(alias); + loadedCertificateKeyAliases.add(alias); } catch (RuntimeException exception) { - LOGGER.log(WARNING, String.format("Failed to load certificate details for alias: %s", alias), - exception); + LOGGER.log(WARNING, String.format("Failed to load certificate key for alias: %s", alias), exception); } } } @@ -297,7 +348,7 @@ public String refreshAndGetAliasByCertificate(Certificate certificate) { aliasesSnapshot = new ArrayList<>(Optional.ofNullable(aliases).orElse(Collections.emptyList())); } - aliasesSnapshot.forEach(this::loadCertificateDetailsIfNeeded); + aliasesSnapshot.forEach(this::loadCertificateIfNeeded); Map certificatesSnapshot; synchronized (this) { @@ -323,7 +374,9 @@ public synchronized void deleteEntry(String alias) { if (aliases != null) { aliases.remove(alias); } - loadedAliases.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 bf0d780165af..fbd6c6681ab7 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 @@ -3,14 +3,11 @@ package com.azure.security.keyvault.jca; -import com.azure.security.keyvault.jca.implementation.certificates.KeyVaultCertificates; - 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.lang.reflect.Field; import java.security.ProviderException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -119,27 +116,4 @@ public void testGetConfiguredKeyVaultCertificateAliases() { } } - @Test - public void testConfiguredAliasesWiredToKeyVaultCertificates() throws Exception { - try { - System.setProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY, "certA, certB"); - KeyVaultKeyStore keyVaultKeyStore = new KeyVaultKeyStore(); - - Field keyVaultCertificatesField = KeyVaultKeyStore.class.getDeclaredField("keyVaultCertificates"); - keyVaultCertificatesField.setAccessible(true); - KeyVaultCertificates keyVaultCertificates - = (KeyVaultCertificates) keyVaultCertificatesField.get(keyVaultKeyStore); - - Field configuredAliasesField = KeyVaultCertificates.class.getDeclaredField("configuredCertificateAliases"); - configuredAliasesField.setAccessible(true); - Set configuredAliases = (Set) configuredAliasesField.get(keyVaultCertificates); - - assertEquals(2, configuredAliases.size()); - assertTrue(configuredAliases.contains("certA")); - assertTrue(configuredAliases.contains("certB")); - } finally { - System.clearProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY); - } - } - } 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 d4b350aa09c1..461d3422f17b 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 @@ -104,14 +104,32 @@ public void testLoadCertificateDetailsForRequestedAliasOnly() { keyVaultCertificates.getCertificate("myalias"); - verify(keyVaultClient, times(1)).getKey("myalias", null); verify(keyVaultClient, times(1)).getCertificate("myalias"); - verify(keyVaultClient, times(1)).getCertificateChain("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<>(); @@ -150,6 +168,15 @@ public void testConfiguredAliasesFilterAfterRefresh() { Assertions.assertFalse(refreshedAliases.contains("new")); } + @Test + public void testConfiguredAliasesDoNotCallListApi() { + keyVaultCertificates + = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("configured-alias")); + + Assertions.assertEquals(Collections.singletonList("configured-alias"), keyVaultCertificates.getAliases()); + verify(keyVaultClient, never()).getAliases(); + } + @Test public void testGetCertificateWithUnconfiguredAliasDoesNotFetchDetails() { keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("myalias")); @@ -162,12 +189,15 @@ public void testGetCertificateWithUnconfiguredAliasDoesNotFetchDetails() { } @Test - public void testAliasLoadFailureIsRetriedOnNextAccess() { - when(keyVaultClient.getKey("myalias", null)).thenThrow(new RuntimeException("transient error")).thenReturn(key); + 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)).getKey("myalias", null); + verify(keyVaultClient, times(2)).getCertificate("myalias"); + verify(keyVaultClient, never()).getKey("myalias", null); + verify(keyVaultClient, never()).getCertificateChain("myalias"); } } From a6fbf69be4825b3630d35cb2ae1ae015f1859634 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 10:42:25 +0800 Subject: [PATCH 09/36] Fix import order and remove unused import --- .../java/com/azure/security/keyvault/jca/KeyVaultKeyStore.java | 2 +- .../jca/implementation/certificates/KeyVaultCertificates.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) 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 5ee37bcc78ab..d313258e3c18 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 @@ -31,8 +31,8 @@ import java.util.Optional; import java.util.Set; import java.util.logging.Logger; -import java.util.stream.Stream; import java.util.stream.Collectors; +import java.util.stream.Stream; import static java.util.logging.Level.FINE; import static java.util.logging.Level.WARNING; 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 ad2b56f99f77..01fe2b71128e 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 @@ -14,7 +14,6 @@ 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; From 9d74face8c03d7c27fb10c7c911d5abc65d58282 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 10:50:22 +0800 Subject: [PATCH 10/36] Strengthen test isolation and list API assertions --- .../keyvault/jca/KeyVaultKeyStoreUnitTest.java | 7 ++++++- .../certificates/KeyVaultCertificatesTest.java | 11 +---------- 2 files changed, 7 insertions(+), 11 deletions(-) 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 fbd6c6681ab7..72d4fb2c1e99 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 @@ -98,6 +98,7 @@ public void testEngineSetCertificateEntry() { @Test public void testGetConfiguredKeyVaultCertificateAliases() { + String originalConfiguredCertificates = System.getProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY); try { System.clearProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY); KeyVaultKeyStore keyVaultKeyStore = new KeyVaultKeyStore(); @@ -112,7 +113,11 @@ public void testGetConfiguredKeyVaultCertificateAliases() { assertTrue(aliases.contains("cert2")); assertTrue(aliases.contains("cert3")); } finally { - System.clearProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY); + if (originalConfiguredCertificates == null) { + System.clearProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY); + } else { + System.setProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY, originalConfiguredCertificates); + } } } 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 461d3422f17b..2d9c0051eedd 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 @@ -13,10 +13,8 @@ import java.security.Key; import java.security.cert.Certificate; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -147,14 +145,6 @@ public void testConfiguredAliasesFilter() { @Test public void testConfiguredAliasesFilterAfterRefresh() { - AtomicInteger invocationCount = new AtomicInteger(0); - when(keyVaultClient.getAliases()).thenAnswer(invocation -> { - if (invocationCount.getAndIncrement() == 0) { - return Arrays.asList("myalias", "otheralias"); - } - return Arrays.asList("otheralias", "myalias", "new"); - }); - keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("myalias")); Assertions.assertEquals(Collections.singletonList("myalias"), keyVaultCertificates.getAliases()); @@ -166,6 +156,7 @@ public void testConfiguredAliasesFilterAfterRefresh() { Assertions.assertTrue(refreshedAliases.contains("myalias")); Assertions.assertFalse(refreshedAliases.contains("otheralias")); Assertions.assertFalse(refreshedAliases.contains("new")); + verify(keyVaultClient, never()).getAliases(); } @Test From 162a828726fa827c8892061430facccc58467d4c Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 13:22:40 +0800 Subject: [PATCH 11/36] Retry certificate load when Key Vault returns null --- .../certificates/KeyVaultCertificates.java | 6 ++++-- .../certificates/KeyVaultCertificatesTest.java | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) 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 01fe2b71128e..9432690bfdfa 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 @@ -278,8 +278,10 @@ private void loadCertificateIfNeeded(String alias) { try { Certificate loadedCertificate = keyVaultClient.getCertificate(alias); - certificates.put(alias, loadedCertificate); - loadedCertificateAliases.add(alias); + if (loadedCertificate != null) { + certificates.put(alias, loadedCertificate); + loadedCertificateAliases.add(alias); + } } catch (RuntimeException exception) { LOGGER.log(WARNING, String.format("Failed to load certificate for alias: %s", alias), exception); } 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 2d9c0051eedd..0a77fab5047b 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 @@ -191,4 +191,15 @@ public void testAliasCertificateLoadFailureIsRetriedOnNextAccess() { 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"); + } + } From d588c4daed994c9dfb1db0bd9220651c07af0e7a Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 13:55:27 +0800 Subject: [PATCH 12/36] Suppress GoodLoggingCheck and fix synchronization consistency --- .../checkstyle-suppressions.xml | 1 + .../certificates/KeyVaultCertificates.java | 41 ++++++++++++------- 2 files changed, 28 insertions(+), 14 deletions(-) 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/implementation/certificates/KeyVaultCertificates.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificates.java index 9432690bfdfa..c5ec7f570fa9 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 @@ -98,11 +98,19 @@ public KeyVaultCertificates(long refreshInterval, KeyVaultClient keyVaultClient) public KeyVaultCertificates(long refreshInterval, KeyVaultClient keyVaultClient, Set configuredCertificateAliases) { this.refreshInterval = refreshInterval; - this.keyVaultClient = keyVaultClient; + setKeyVaultClient(keyVaultClient); this.configuredCertificateAliases = new HashSet<>(Optional.ofNullable(configuredCertificateAliases).orElse(Collections.emptySet())); } + private synchronized KeyVaultClient getKeyVaultClient() { + return keyVaultClient; + } + + private synchronized void setKeyVaultClient(KeyVaultClient keyVaultClient) { + this.keyVaultClient = keyVaultClient; + } + /** * Update KeyVaultClient. * @@ -118,15 +126,15 @@ public void updateKeyVaultClient(String keyVaultUri, String tenantId, String cli 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); } } - boolean certificatesNeedRefresh() { - if (keyVaultClient == null) { + synchronized boolean certificatesNeedRefresh() { + if (getKeyVaultClient() == null) { return false; } if (lastRefreshTime == null) { @@ -233,7 +241,8 @@ private void refreshCertificatesIfNeeded() { * Refresh certificates. Including certificates, aliases, certificate keys, certificate chains. */ public synchronized void refreshCertificates() { - if (keyVaultClient == null) { + KeyVaultClient currentKeyVaultClient = getKeyVaultClient(); + if (currentKeyVaultClient == null) { aliases = new ArrayList<>(); loadedCertificateAliases.clear(); loadedCertificateChainAliases.clear(); @@ -247,7 +256,8 @@ public synchronized void refreshCertificates() { if (configuredCertificateAliases.isEmpty()) { // If aliases are not configured, discover aliases from Key Vault. - aliases = new ArrayList<>(Optional.ofNullable(keyVaultClient.getAliases()).orElse(Collections.emptyList())); + aliases = new ArrayList<>( + Optional.ofNullable(currentKeyVaultClient.getAliases()).orElse(Collections.emptyList())); } else { // If aliases are configured, avoid the list API and only use the configured aliases. aliases = configuredCertificateAliases.stream().sorted().collect(Collectors.toCollection(ArrayList::new)); @@ -265,8 +275,9 @@ public synchronized void refreshCertificates() { private void loadCertificateIfNeeded(String alias) { refreshCertificatesIfNeeded(); + KeyVaultClient currentKeyVaultClient = getKeyVaultClient(); - if (alias == null || keyVaultClient == null) { + if (alias == null || currentKeyVaultClient == null) { return; } @@ -277,7 +288,7 @@ private void loadCertificateIfNeeded(String alias) { } try { - Certificate loadedCertificate = keyVaultClient.getCertificate(alias); + Certificate loadedCertificate = currentKeyVaultClient.getCertificate(alias); if (loadedCertificate != null) { certificates.put(alias, loadedCertificate); loadedCertificateAliases.add(alias); @@ -290,8 +301,9 @@ private void loadCertificateIfNeeded(String alias) { private void loadCertificateChainIfNeeded(String alias) { refreshCertificatesIfNeeded(); + KeyVaultClient currentKeyVaultClient = getKeyVaultClient(); - if (alias == null || keyVaultClient == null) { + if (alias == null || currentKeyVaultClient == null) { return; } @@ -302,7 +314,7 @@ private void loadCertificateChainIfNeeded(String alias) { } try { - Certificate[] loadedCertificateChain = keyVaultClient.getCertificateChain(alias); + Certificate[] loadedCertificateChain = currentKeyVaultClient.getCertificateChain(alias); certificateChains.put(alias, loadedCertificateChain); loadedCertificateChainAliases.add(alias); } catch (RuntimeException exception) { @@ -313,8 +325,9 @@ private void loadCertificateChainIfNeeded(String alias) { private void loadCertificateKeyIfNeeded(String alias) { refreshCertificatesIfNeeded(); + KeyVaultClient currentKeyVaultClient = getKeyVaultClient(); - if (alias == null || keyVaultClient == null) { + if (alias == null || currentKeyVaultClient == null) { return; } @@ -325,7 +338,7 @@ private void loadCertificateKeyIfNeeded(String alias) { } try { - Key loadedKey = keyVaultClient.getKey(alias, null); + Key loadedKey = currentKeyVaultClient.getKey(alias, null); certificateKeys.put(alias, loadedKey); loadedCertificateKeyAliases.add(alias); } catch (RuntimeException exception) { From b4aae2844f1210238804f11521d126332c8a2462 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 13:57:23 +0800 Subject: [PATCH 13/36] Retry key and chain loads after transient failures --- .../certificates/KeyVaultCertificates.java | 12 +++-- .../KeyVaultCertificatesTest.java | 45 +++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) 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 c5ec7f570fa9..e0c9094aef1c 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 @@ -315,8 +315,10 @@ private void loadCertificateChainIfNeeded(String alias) { try { Certificate[] loadedCertificateChain = currentKeyVaultClient.getCertificateChain(alias); - certificateChains.put(alias, loadedCertificateChain); - loadedCertificateChainAliases.add(alias); + if (loadedCertificateChain != null && loadedCertificateChain.length > 0) { + certificateChains.put(alias, loadedCertificateChain); + loadedCertificateChainAliases.add(alias); + } } catch (RuntimeException exception) { LOGGER.log(WARNING, String.format("Failed to load certificate chain for alias: %s", alias), exception); } @@ -339,8 +341,10 @@ private void loadCertificateKeyIfNeeded(String alias) { try { Key loadedKey = currentKeyVaultClient.getKey(alias, null); - certificateKeys.put(alias, loadedKey); - loadedCertificateKeyAliases.add(alias); + if (loadedKey != null) { + certificateKeys.put(alias, loadedKey); + loadedCertificateKeyAliases.add(alias); + } } catch (RuntimeException exception) { LOGGER.log(WARNING, String.format("Failed to load certificate key for alias: %s", alias), exception); } 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 0a77fab5047b..384def710158 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 @@ -202,4 +202,49 @@ public void testAliasCertificateNullLoadIsRetriedOnNextAccess() { 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); + } + } From c553796984e24fd9488b11b1e640621657fa0fab Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 14:06:51 +0800 Subject: [PATCH 14/36] Use lazy warning logs and remove extra suppression --- .../azure-security-keyvault-jca/checkstyle-suppressions.xml | 1 - .../implementation/certificates/KeyVaultCertificates.java | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-jca/checkstyle-suppressions.xml b/sdk/keyvault/azure-security-keyvault-jca/checkstyle-suppressions.xml index 790fdff01ec1..1796f495a42b 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/checkstyle-suppressions.xml +++ b/sdk/keyvault/azure-security-keyvault-jca/checkstyle-suppressions.xml @@ -13,7 +13,6 @@ - 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 e0c9094aef1c..3f886f3e501c 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 @@ -294,7 +294,7 @@ private void loadCertificateIfNeeded(String alias) { loadedCertificateAliases.add(alias); } } catch (RuntimeException exception) { - LOGGER.log(WARNING, String.format("Failed to load certificate for alias: %s", alias), exception); + LOGGER.log(WARNING, exception, () -> "Failed to load certificate for alias: " + alias); } } } @@ -320,7 +320,7 @@ private void loadCertificateChainIfNeeded(String alias) { loadedCertificateChainAliases.add(alias); } } catch (RuntimeException exception) { - LOGGER.log(WARNING, String.format("Failed to load certificate chain for alias: %s", alias), exception); + LOGGER.log(WARNING, exception, () -> "Failed to load certificate chain for alias: " + alias); } } } @@ -346,7 +346,7 @@ private void loadCertificateKeyIfNeeded(String alias) { loadedCertificateKeyAliases.add(alias); } } catch (RuntimeException exception) { - LOGGER.log(WARNING, String.format("Failed to load certificate key for alias: %s", alias), exception); + LOGGER.log(WARNING, exception, () -> "Failed to load certificate key for alias: " + alias); } } } From c73eb95ecc11457881c37b4f48c41edb666cf587 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 14:15:23 +0800 Subject: [PATCH 15/36] Reset key vault certificate cache when client changes --- .../certificates/KeyVaultCertificates.java | 13 +++++++++++++ .../certificates/KeyVaultCertificatesTest.java | 14 ++++++++++++++ 2 files changed, 27 insertions(+) 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 3f886f3e501c..5b2123c6d33b 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 @@ -131,6 +131,19 @@ public void updateKeyVaultClient(String keyVaultUri, String tenantId, String cli } else { setKeyVaultClient(null); } + + clearCachedState(); + } + + private synchronized void clearCachedState() { + aliases = new ArrayList<>(); + loadedCertificateAliases.clear(); + loadedCertificateChainAliases.clear(); + loadedCertificateKeyAliases.clear(); + certificateKeys.clear(); + certificates.clear(); + certificateChains.clear(); + lastRefreshTime = null; } synchronized boolean certificatesNeedRefresh() { 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 384def710158..d18364825206 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 @@ -247,4 +247,18 @@ public void testAliasChainEmptyLoadIsRetriedOnNextAccess() { 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")); + } + } From 72887a0ed0839f3310c66c4891ebe900b9e9acbc Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 14:21:51 +0800 Subject: [PATCH 16/36] Make key vault client update atomic and reuse cache reset --- .../certificates/KeyVaultCertificates.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) 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 5b2123c6d33b..b29b4c6acad9 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 @@ -122,8 +122,8 @@ private synchronized void setKeyVaultClient(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) { setKeyVaultClient(new KeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, @@ -256,14 +256,7 @@ private void refreshCertificatesIfNeeded() { public synchronized void refreshCertificates() { KeyVaultClient currentKeyVaultClient = getKeyVaultClient(); if (currentKeyVaultClient == null) { - aliases = new ArrayList<>(); - loadedCertificateAliases.clear(); - loadedCertificateChainAliases.clear(); - loadedCertificateKeyAliases.clear(); - certificateKeys.clear(); - certificates.clear(); - certificateChains.clear(); - lastRefreshTime = null; + clearCachedState(); return; } From ce99b3eb996581c10da775ffffcccd6bdf995742 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 14:31:46 +0800 Subject: [PATCH 17/36] Ignore null configured certificate aliases --- .../certificates/KeyVaultCertificates.java | 6 +++++- .../certificates/KeyVaultCertificatesTest.java | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) 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 b29b4c6acad9..4b0f50cb9765 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 @@ -14,6 +14,7 @@ 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; @@ -266,7 +267,10 @@ public synchronized void refreshCertificates() { Optional.ofNullable(currentKeyVaultClient.getAliases()).orElse(Collections.emptyList())); } else { // If aliases are configured, avoid the list API and only use the configured aliases. - aliases = configuredCertificateAliases.stream().sorted().collect(Collectors.toCollection(ArrayList::new)); + aliases = configuredCertificateAliases.stream() + .filter(Objects::nonNull) + .sorted() + .collect(Collectors.toCollection(ArrayList::new)); } loadedCertificateAliases.clear(); 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 d18364825206..1d29447f01fd 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 @@ -13,8 +13,11 @@ 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; @@ -168,6 +171,15 @@ public void testConfiguredAliasesDoNotCallListApi() { verify(keyVaultClient, never()).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, never()).getAliases(); + } + @Test public void testGetCertificateWithUnconfiguredAliasDoesNotFetchDetails() { keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("myalias")); From 7ec6baf70a2ddce8783727ad0ed771982f732e1f Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 14:47:45 +0800 Subject: [PATCH 18/36] Use allCertificates precedence in KeyVaultKeyStore lookups --- .../keyvault/jca/KeyVaultKeyStore.java | 69 ++++++++----------- 1 file changed, 30 insertions(+), 39 deletions(-) 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 d313258e3c18..3f098cf6e9c2 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 @@ -268,20 +268,17 @@ public boolean engineEntryInstanceOf(String alias, Class a.containsKey(alias)) - .findFirst() - .map(certificates -> certificates.get(alias)) - .orElse(null); - - if (certificate == null) { - certificate = keyVaultCertificates.getCertificate(alias); - } + 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) { - certificate = classpathCertificates.getCertificates().get(alias); + if (certificate != null) { + break; + } } if (refreshCertificatesWhenHaveUnTrustCertificate && certificate == null) { @@ -330,20 +327,17 @@ public String engineGetCertificateAlias(Certificate cert) { */ @Override public Certificate[] engineGetCertificateChain(String alias) { - Certificate[] certificates = Arrays.asList(jreCertificates, wellKnowCertificates, customCertificates) - .stream() - .map(AzureCertificates::getCertificateChains) - .filter(Objects::nonNull) - .filter(a -> a.containsKey(alias)) - .findFirst() - .map(m -> m.get(alias)) - .orElse(null); - if (certificates == null) { - certificates = keyVaultCertificates.getCertificateChain(alias); - } + 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) { - certificates = classpathCertificates.getCertificateChains().get(alias); + if (certificates != null) { + break; + } } if (refreshCertificatesWhenHaveUnTrustCertificate && certificates == null) { @@ -390,20 +384,17 @@ public KeyStore.Entry engineGetEntry(String alias, KeyStore.ProtectionParameter */ @Override public Key engineGetKey(String alias, char[] password) { - Key key = Arrays.asList(jreCertificates, wellKnowCertificates, customCertificates) - .stream() - .map(AzureCertificates::getCertificateKeys) - .filter(a -> a.containsKey(alias)) - .findFirst() - .map(certificateKeys -> certificateKeys.get(alias)) - .orElse(null); - - if (key == null) { - key = keyVaultCertificates.getCertificateKey(alias); - } + 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) { - key = classpathCertificates.getCertificateKeys().get(alias); + if (key != null) { + break; + } } return key; From f34921f733806e0d83a7784726dd785d1807afa3 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 16:03:49 +0800 Subject: [PATCH 19/36] Add GoodLoggingCheck suppression for KeyVaultCertificates --- .../azure-security-keyvault-jca/checkstyle-suppressions.xml | 1 + 1 file changed, 1 insertion(+) 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 @@ + From 9506d26d7f65dd15053df071c25ab0c8116429a4 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Fri, 10 Jul 2026 16:15:58 +0800 Subject: [PATCH 20/36] Guard null certificate alias lookup and clarify refresh docs --- .../implementation/certificates/KeyVaultCertificates.java | 6 +++++- .../certificates/KeyVaultCertificatesTest.java | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) 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 4b0f50cb9765..3e29b444157f 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 @@ -252,7 +252,7 @@ private void refreshCertificatesIfNeeded() { } /** - * Refresh certificates. Including certificates, aliases, certificate keys, certificate chains. + * Refresh aliases and invalidate cached certificate details. */ public synchronized void refreshCertificates() { KeyVaultClient currentKeyVaultClient = getKeyVaultClient(); @@ -369,6 +369,10 @@ private void loadCertificateKeyIfNeeded(String alias) { * @return Certificate alias if it exists. */ public String refreshAndGetAliasByCertificate(Certificate certificate) { + if (certificate == null) { + return null; + } + refreshCertificates(); List aliasesSnapshot; 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 1d29447f01fd..71a5416c1e02 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 @@ -74,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")); From 35885e3568380c1cfa75baaecea9681402112713 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 13 Jul 2026 09:10:51 +0800 Subject: [PATCH 21/36] Reduce lock contention in lazy certificate loading --- .../certificates/KeyVaultCertificates.java | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) 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 3e29b444157f..7ad6e463268a 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 @@ -296,16 +296,23 @@ private void loadCertificateIfNeeded(String alias) { || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { return; } + } + + try { + Certificate loadedCertificate = currentKeyVaultClient.getCertificate(alias); + synchronized (this) { + if (loadedCertificateAliases.contains(alias) + || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + return; + } - try { - Certificate loadedCertificate = currentKeyVaultClient.getCertificate(alias); if (loadedCertificate != null) { certificates.put(alias, loadedCertificate); loadedCertificateAliases.add(alias); } - } catch (RuntimeException exception) { - LOGGER.log(WARNING, exception, () -> "Failed to load certificate for alias: " + alias); } + } catch (RuntimeException exception) { + LOGGER.log(WARNING, exception, () -> "Failed to load certificate for alias: " + alias); } } @@ -322,16 +329,23 @@ private void loadCertificateChainIfNeeded(String alias) { || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { return; } + } + + try { + Certificate[] loadedCertificateChain = currentKeyVaultClient.getCertificateChain(alias); + synchronized (this) { + if (loadedCertificateChainAliases.contains(alias) + || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + return; + } - try { - Certificate[] loadedCertificateChain = currentKeyVaultClient.getCertificateChain(alias); 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); } + } catch (RuntimeException exception) { + LOGGER.log(WARNING, exception, () -> "Failed to load certificate chain for alias: " + alias); } } @@ -348,16 +362,23 @@ private void loadCertificateKeyIfNeeded(String alias) { || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { return; } + } + + try { + Key loadedKey = currentKeyVaultClient.getKey(alias, null); + synchronized (this) { + if (loadedCertificateKeyAliases.contains(alias) + || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + return; + } - try { - Key loadedKey = currentKeyVaultClient.getKey(alias, null); 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); } + } catch (RuntimeException exception) { + LOGGER.log(WARNING, exception, () -> "Failed to load certificate key for alias: " + alias); } } From e67607c4a14b77b85537cdb8392997f2d3e08437 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 13 Jul 2026 09:20:15 +0800 Subject: [PATCH 22/36] Add certificates-filter-patterns property for alias regex filtering --- .../azure-security-keyvault-jca/CHANGELOG.md | 2 +- .../azure-security-keyvault-jca/README.md | 2 +- .../keyvault/jca/KeyVaultKeyStore.java | 13 ++- .../certificates/KeyVaultCertificates.java | 86 +++++++++++++++---- .../jca/KeyVaultKeyStoreUnitTest.java | 27 +++--- .../KeyVaultCertificatesTest.java | 45 +++++++++- 6 files changed, 131 insertions(+), 44 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md index 292c2e175a31..5c164f6e1b51 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md +++ b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md @@ -4,7 +4,7 @@ ### 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.certificates` to specify a comma-separated list of Key Vault certificate aliases to load. When set, only these aliases are considered from Key Vault. ([#39487](https://github.com/Azure/azure-sdk-for-java/issues/39487)) +- Added support for `azure.keyvault.jca.certificates-filter-patterns` to filter Key Vault certificate aliases with include/exclude regex patterns. Include patterns are configured directly (or with `+` prefix) and exclude patterns are prefixed with `!`. ([#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 fd8861757c80..4cd68339f5da 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/README.md +++ b/sdk/keyvault/azure-security-keyvault-jca/README.md @@ -141,7 +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.certificates`: Comma-separated certificate aliases to load from Key Vault. When set, only these aliases are considered from Key Vault. +* `azure.keyvault.jca.certificates-filter-patterns`: Comma-separated Key Vault alias regex filters. Use include regexes directly (or with a `+` prefix) and exclude regexes with a `!` prefix. 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$`. * `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/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 3f098cf6e9c2..d58c4c7c8a0d 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 @@ -58,7 +58,7 @@ public final class KeyVaultKeyStore extends KeyStoreSpi { */ private static final Logger LOGGER = Logger.getLogger(KeyVaultKeyStore.class.getName()); - static final String CONFIGURED_CERTIFICATES_PROPERTY = "azure.keyvault.jca.certificates"; + static final String CERTIFICATES_FILTER_PATTERNS_PROPERTY = "azure.keyvault.jca.certificates-filter-patterns"; /** * Stores the Jre key store certificates. @@ -150,9 +150,8 @@ 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, getConfiguredKeyVaultCertificateAliases()); + keyVaultCertificates = new KeyVaultCertificates(refreshInterval, keyVaultUri, tenantId, clientId, clientSecret, + managedIdentity, accessToken, disableChallengeResourceVerification, getKeyVaultCertificateFilterPatterns()); LOGGER.log(FINE, String.format("Loaded Key Vault certificates: %s.", keyVaultCertificates.getAliases())); classpathCertificates = new ClasspathCertificates(); @@ -173,11 +172,11 @@ Long getRefreshInterval() { .orElse(0L); } - Set getConfiguredKeyVaultCertificateAliases() { - return Optional.ofNullable(System.getProperty(CONFIGURED_CERTIFICATES_PROPERTY)) + Set getKeyVaultCertificateFilterPatterns() { + return Optional.ofNullable(System.getProperty(CERTIFICATES_FILTER_PATTERNS_PROPERTY)) .map(value -> Stream.of(value.split(",")) .map(String::trim) - .filter(alias -> !alias.isEmpty()) + .filter(pattern -> !pattern.isEmpty()) .collect(Collectors.toSet())) .orElse(Collections.emptySet()); } 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 7ad6e463268a..a8ab7a0bba00 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 @@ -19,6 +19,8 @@ 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; @@ -72,7 +74,11 @@ public final class KeyVaultCertificates implements AzureCertificates { private final long refreshInterval; - private final Set configuredCertificateAliases; + 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) { @@ -82,11 +88,13 @@ public KeyVaultCertificates(long refreshInterval, String keyVaultUri, String ten public KeyVaultCertificates(long refreshInterval, String keyVaultUri, String tenantId, String clientId, String clientSecret, String managedIdentity, String accessToken, boolean disableChallengeResourceVerification, - Set configuredCertificateAliases) { + Set certificateFilterPatterns) { this.refreshInterval = refreshInterval; - this.configuredCertificateAliases - = new HashSet<>(Optional.ofNullable(configuredCertificateAliases).orElse(Collections.emptySet())); + 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); @@ -97,11 +105,57 @@ public KeyVaultCertificates(long refreshInterval, KeyVaultClient keyVaultClient) } public KeyVaultCertificates(long refreshInterval, KeyVaultClient keyVaultClient, - Set configuredCertificateAliases) { + Set certificateFilterPatterns) { this.refreshInterval = refreshInterval; setKeyVaultClient(keyVaultClient); - this.configuredCertificateAliases - = new HashSet<>(Optional.ofNullable(configuredCertificateAliases).orElse(Collections.emptySet())); + 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() { @@ -261,17 +315,13 @@ public synchronized void refreshCertificates() { return; } - if (configuredCertificateAliases.isEmpty()) { - // If aliases are not configured, discover aliases from Key Vault. - aliases = new ArrayList<>( - Optional.ofNullable(currentKeyVaultClient.getAliases()).orElse(Collections.emptyList())); - } else { - // If aliases are configured, avoid the list API and only use the configured aliases. - aliases = configuredCertificateAliases.stream() - .filter(Objects::nonNull) - .sorted() - .collect(Collectors.toCollection(ArrayList::new)); - } + // 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(); 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 72d4fb2c1e99..3ca15942c6c0 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 @@ -97,26 +97,27 @@ public void testEngineSetCertificateEntry() { } @Test - public void testGetConfiguredKeyVaultCertificateAliases() { - String originalConfiguredCertificates = System.getProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY); + public void testGetKeyVaultCertificateFilterPatterns() { + String originalFilterPatterns = System.getProperty(KeyVaultKeyStore.CERTIFICATES_FILTER_PATTERNS_PROPERTY); try { - System.clearProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY); + System.clearProperty(KeyVaultKeyStore.CERTIFICATES_FILTER_PATTERNS_PROPERTY); KeyVaultKeyStore keyVaultKeyStore = new KeyVaultKeyStore(); - assertTrue(keyVaultKeyStore.getConfiguredKeyVaultCertificateAliases().isEmpty()); + assertTrue(keyVaultKeyStore.getKeyVaultCertificateFilterPatterns().isEmpty()); - System.setProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY, "cert1, cert2 ,, cert3 "); + System.setProperty(KeyVaultKeyStore.CERTIFICATES_FILTER_PATTERNS_PROPERTY, + "^prod-.*,!^prod-deprecated-.*,,myalias"); keyVaultKeyStore = new KeyVaultKeyStore(); - Set aliases = keyVaultKeyStore.getConfiguredKeyVaultCertificateAliases(); + Set patterns = keyVaultKeyStore.getKeyVaultCertificateFilterPatterns(); - assertEquals(3, aliases.size()); - assertTrue(aliases.contains("cert1")); - assertTrue(aliases.contains("cert2")); - assertTrue(aliases.contains("cert3")); + assertEquals(3, patterns.size()); + assertTrue(patterns.contains("^prod-.*")); + assertTrue(patterns.contains("!^prod-deprecated-.*")); + assertTrue(patterns.contains("myalias")); } finally { - if (originalConfiguredCertificates == null) { - System.clearProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY); + if (originalFilterPatterns == null) { + System.clearProperty(KeyVaultKeyStore.CERTIFICATES_FILTER_PATTERNS_PROPERTY); } else { - System.setProperty(KeyVaultKeyStore.CONFIGURED_CERTIFICATES_PROPERTY, originalConfiguredCertificates); + System.setProperty(KeyVaultKeyStore.CERTIFICATES_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 71a5416c1e02..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 @@ -151,6 +151,33 @@ public void testConfiguredAliasesFilter() { 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")); @@ -164,16 +191,18 @@ public void testConfiguredAliasesFilterAfterRefresh() { Assertions.assertTrue(refreshedAliases.contains("myalias")); Assertions.assertFalse(refreshedAliases.contains("otheralias")); Assertions.assertFalse(refreshedAliases.contains("new")); - verify(keyVaultClient, never()).getAliases(); + verify(keyVaultClient, times(2)).getAliases(); } @Test - public void testConfiguredAliasesDoNotCallListApi() { + 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, never()).getAliases(); + verify(keyVaultClient, times(1)).getAliases(); } @Test @@ -182,7 +211,15 @@ public void testConfiguredAliasesIgnoreNullEntries() { keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, configuredAliases); Assertions.assertEquals(Collections.singletonList("myalias"), keyVaultCertificates.getAliases()); - verify(keyVaultClient, never()).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 From 8cddbf07adc78b84bcf037e53f908e542f9eac4b Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 13 Jul 2026 09:22:57 +0800 Subject: [PATCH 23/36] Simplify certificates filter pattern README wording --- sdk/keyvault/azure-security-keyvault-jca/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/keyvault/azure-security-keyvault-jca/README.md b/sdk/keyvault/azure-security-keyvault-jca/README.md index 4cd68339f5da..bd05c619f914 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/README.md +++ b/sdk/keyvault/azure-security-keyvault-jca/README.md @@ -141,7 +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.certificates-filter-patterns`: Comma-separated Key Vault alias regex filters. Use include regexes directly (or with a `+` prefix) and exclude regexes with a `!` prefix. 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$`. +* `azure.keyvault.jca.certificates-filter-patterns`: Comma-separated Key Vault alias regex filters. Use include regexes directly and exclude regexes with a `!` prefix. 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$`. * `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: From f1ce384efcb3c9ec7fe97b25d3e32083b2707325 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 13 Jul 2026 09:25:27 +0800 Subject: [PATCH 24/36] Align changelog filter pattern wording with README --- sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md index 5c164f6e1b51..8adfec9cc294 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md +++ b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md @@ -4,7 +4,7 @@ ### 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.certificates-filter-patterns` to filter Key Vault certificate aliases with include/exclude regex patterns. Include patterns are configured directly (or with `+` prefix) and exclude patterns are prefixed with `!`. ([#39487](https://github.com/Azure/azure-sdk-for-java/issues/39487)) +- Added support for `azure.keyvault.jca.certificates-filter-patterns` to filter Key Vault certificate aliases with include/exclude regex patterns. Include patterns are configured directly and exclude patterns are prefixed with `!`. ([#39487](https://github.com/Azure/azure-sdk-for-java/issues/39487)) ### Breaking Changes From 470b184c11099c9ea6a82aa890287f876c539b77 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 13 Jul 2026 09:27:23 +0800 Subject: [PATCH 25/36] Rename certificate alias filter property --- .../azure-security-keyvault-jca/CHANGELOG.md | 2 +- .../azure-security-keyvault-jca/README.md | 2 +- .../security/keyvault/jca/KeyVaultKeyStore.java | 12 +++++++----- .../keyvault/jca/KeyVaultKeyStoreUnitTest.java | 16 ++++++++-------- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md index 8adfec9cc294..117cc651ca91 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md +++ b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md @@ -4,7 +4,7 @@ ### 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.certificates-filter-patterns` to filter Key Vault certificate aliases with include/exclude regex patterns. Include patterns are configured directly and exclude patterns are prefixed with `!`. ([#39487](https://github.com/Azure/azure-sdk-for-java/issues/39487)) +- 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 `!`. ([#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 bd05c619f914..4a0b8be539ff 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/README.md +++ b/sdk/keyvault/azure-security-keyvault-jca/README.md @@ -141,7 +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.certificates-filter-patterns`: Comma-separated Key Vault alias regex filters. Use include regexes directly and exclude regexes with a `!` prefix. 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$`. +* `azure.keyvault.jca.certificate-alias-filter-patterns`: Comma-separated Key Vault alias regex filters. Use include regexes directly and exclude regexes with a `!` prefix. 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$`. * `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/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 d58c4c7c8a0d..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 @@ -58,7 +58,8 @@ public final class KeyVaultKeyStore extends KeyStoreSpi { */ private static final Logger LOGGER = Logger.getLogger(KeyVaultKeyStore.class.getName()); - static final String CERTIFICATES_FILTER_PATTERNS_PROPERTY = "azure.keyvault.jca.certificates-filter-patterns"; + static final String CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY + = "azure.keyvault.jca.certificate-alias-filter-patterns"; /** * Stores the Jre key store certificates. @@ -150,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, getKeyVaultCertificateFilterPatterns()); + 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(); @@ -172,8 +174,8 @@ Long getRefreshInterval() { .orElse(0L); } - Set getKeyVaultCertificateFilterPatterns() { - return Optional.ofNullable(System.getProperty(CERTIFICATES_FILTER_PATTERNS_PROPERTY)) + 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()) 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 3ca15942c6c0..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 @@ -97,17 +97,17 @@ public void testEngineSetCertificateEntry() { } @Test - public void testGetKeyVaultCertificateFilterPatterns() { - String originalFilterPatterns = System.getProperty(KeyVaultKeyStore.CERTIFICATES_FILTER_PATTERNS_PROPERTY); + public void testGetKeyVaultCertificateAliasFilterPatterns() { + String originalFilterPatterns = System.getProperty(KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY); try { - System.clearProperty(KeyVaultKeyStore.CERTIFICATES_FILTER_PATTERNS_PROPERTY); + System.clearProperty(KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY); KeyVaultKeyStore keyVaultKeyStore = new KeyVaultKeyStore(); - assertTrue(keyVaultKeyStore.getKeyVaultCertificateFilterPatterns().isEmpty()); + assertTrue(keyVaultKeyStore.getKeyVaultCertificateAliasFilterPatterns().isEmpty()); - System.setProperty(KeyVaultKeyStore.CERTIFICATES_FILTER_PATTERNS_PROPERTY, + System.setProperty(KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY, "^prod-.*,!^prod-deprecated-.*,,myalias"); keyVaultKeyStore = new KeyVaultKeyStore(); - Set patterns = keyVaultKeyStore.getKeyVaultCertificateFilterPatterns(); + Set patterns = keyVaultKeyStore.getKeyVaultCertificateAliasFilterPatterns(); assertEquals(3, patterns.size()); assertTrue(patterns.contains("^prod-.*")); @@ -115,9 +115,9 @@ public void testGetKeyVaultCertificateFilterPatterns() { assertTrue(patterns.contains("myalias")); } finally { if (originalFilterPatterns == null) { - System.clearProperty(KeyVaultKeyStore.CERTIFICATES_FILTER_PATTERNS_PROPERTY); + System.clearProperty(KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY); } else { - System.setProperty(KeyVaultKeyStore.CERTIFICATES_FILTER_PATTERNS_PROPERTY, originalFilterPatterns); + System.setProperty(KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY, originalFilterPatterns); } } } From 524655e3b586f260273cd03d57ce2f3b5ff5ff77 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 13 Jul 2026 09:31:17 +0800 Subject: [PATCH 26/36] Document default alias filter behavior --- sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md | 2 +- sdk/keyvault/azure-security-keyvault-jca/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md index 117cc651ca91..66ee2ed5782c 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md +++ b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md @@ -4,7 +4,7 @@ ### 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 `!`. ([#39487](https://github.com/Azure/azure-sdk-for-java/issues/39487)) +- 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 4a0b8be539ff..43a1127c7c6e 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/README.md +++ b/sdk/keyvault/azure-security-keyvault-jca/README.md @@ -141,7 +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. 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$`. +* `azure.keyvault.jca.certificate-alias-filter-patterns`: Comma-separated Key Vault alias regex filters. Use include regexes directly and exclude regexes with a `!` prefix. 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. If this property is not configured, all discovered Key Vault aliases are eligible for lazy loading. Example: `^prod-.*,!^prod-deprecated$,!.*-old$`. * `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: From 76621266019277a1ca542fd1e2ffb929654ccfae Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 13 Jul 2026 09:32:00 +0800 Subject: [PATCH 27/36] Reorder default filter behavior sentence in README --- sdk/keyvault/azure-security-keyvault-jca/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/keyvault/azure-security-keyvault-jca/README.md b/sdk/keyvault/azure-security-keyvault-jca/README.md index 43a1127c7c6e..0897cad0b99f 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/README.md +++ b/sdk/keyvault/azure-security-keyvault-jca/README.md @@ -141,7 +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. 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. If this property is not configured, all discovered Key Vault aliases are eligible for lazy loading. Example: `^prod-.*,!^prod-deprecated$,!.*-old$`. +* `azure.keyvault.jca.certificate-alias-filter-patterns`: Comma-separated Key Vault alias regex filters. Use include regexes directly and exclude regexes with a `!` prefix. 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: From 60907c29728d1392e445742479326a8b7842253c Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 13 Jul 2026 09:43:57 +0800 Subject: [PATCH 28/36] Fix stale-client publish races in lazy loaders --- sdk/keyvault/azure-security-keyvault-jca/README.md | 2 +- .../certificates/KeyVaultCertificates.java | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-jca/README.md b/sdk/keyvault/azure-security-keyvault-jca/README.md index 0897cad0b99f..fdcfcb0e528a 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/README.md +++ b/sdk/keyvault/azure-security-keyvault-jca/README.md @@ -141,7 +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. 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.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/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 a8ab7a0bba00..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 @@ -351,7 +351,8 @@ private void loadCertificateIfNeeded(String alias) { try { Certificate loadedCertificate = currentKeyVaultClient.getCertificate(alias); synchronized (this) { - if (loadedCertificateAliases.contains(alias) + if (currentKeyVaultClient != keyVaultClient + || loadedCertificateAliases.contains(alias) || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { return; } @@ -384,7 +385,8 @@ private void loadCertificateChainIfNeeded(String alias) { try { Certificate[] loadedCertificateChain = currentKeyVaultClient.getCertificateChain(alias); synchronized (this) { - if (loadedCertificateChainAliases.contains(alias) + if (currentKeyVaultClient != keyVaultClient + || loadedCertificateChainAliases.contains(alias) || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { return; } @@ -417,7 +419,8 @@ private void loadCertificateKeyIfNeeded(String alias) { try { Key loadedKey = currentKeyVaultClient.getKey(alias, null); synchronized (this) { - if (loadedCertificateKeyAliases.contains(alias) + if (currentKeyVaultClient != keyVaultClient + || loadedCertificateKeyAliases.contains(alias) || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { return; } From d75b0e2918121d20e7309288ac55b32b16234ac5 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 13 Jul 2026 14:31:59 +0800 Subject: [PATCH 29/36] Regenerate linting suppressions for keyvault-jca --- .../azure-security-keyvault-jca/checkstyle-suppressions.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/keyvault/azure-security-keyvault-jca/checkstyle-suppressions.xml b/sdk/keyvault/azure-security-keyvault-jca/checkstyle-suppressions.xml index 790fdff01ec1..12bdda26dccf 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/checkstyle-suppressions.xml +++ b/sdk/keyvault/azure-security-keyvault-jca/checkstyle-suppressions.xml @@ -20,6 +20,7 @@ + From b3120a87fd76942089cc1332dee1c41b580b6197 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 13 Jul 2026 14:34:56 +0800 Subject: [PATCH 30/36] Address review feedback on alias logging and filter parsing --- .../java/com/azure/security/keyvault/jca/KeyVaultKeyStore.java | 2 +- .../jca/implementation/certificates/KeyVaultCertificates.java | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) 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 1e8cf8dbcbbf..6725d7e03320 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 @@ -154,7 +154,7 @@ public KeyVaultKeyStore() { keyVaultCertificates = new KeyVaultCertificates(refreshInterval, keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, accessToken, disableChallengeResourceVerification, getKeyVaultCertificateAliasFilterPatterns()); - LOGGER.log(FINE, String.format("Loaded Key Vault certificates: %s.", keyVaultCertificates.getAliases())); + LOGGER.log(FINE, () -> String.format("Loaded Key Vault certificates: %s.", keyVaultCertificates.getAliases())); classpathCertificates = new ClasspathCertificates(); LOGGER.log(FINE, String.format("Loaded classpath certificates: %s.", classpathCertificates.getAliases())); 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 3313d8aa7e7f..987a375f90c4 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 @@ -126,9 +126,6 @@ private List getAliasPatterns(Set filterPatterns, boolean exclu if (excludePatterns) { return pattern.substring(1); } - if (pattern.startsWith("+")) { - return pattern.substring(1); - } return pattern; }) .filter(pattern -> !pattern.isEmpty()) From 91a495cd5f4df5fe909c896b7041caac326c7a41 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 13 Jul 2026 15:19:37 +0800 Subject: [PATCH 31/36] Address review feedback on filter pattern normalization --- .../certificates/KeyVaultCertificates.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) 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 987a375f90c4..34c143e98eee 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 @@ -29,6 +29,8 @@ */ public final class KeyVaultCertificates implements AzureCertificates { private static final Logger LOGGER = Logger.getLogger(KeyVaultCertificates.class.getName()); + private static final String CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY + = "azure.keyvault.jca.certificate-alias-filter-patterns"; /** * Stores the list of aliases. @@ -91,8 +93,7 @@ public KeyVaultCertificates(long refreshInterval, String keyVaultUri, String ten Set certificateFilterPatterns) { this.refreshInterval = refreshInterval; - this.certificateFilterPatterns - = new HashSet<>(Optional.ofNullable(certificateFilterPatterns).orElse(Collections.emptySet())); + this.certificateFilterPatterns = normalizeFilterPatterns(certificateFilterPatterns); this.includeAliasPatterns = getAliasPatterns(this.certificateFilterPatterns, false); this.excludeAliasPatterns = getAliasPatterns(this.certificateFilterPatterns, true); @@ -108,12 +109,21 @@ public KeyVaultCertificates(long refreshInterval, KeyVaultClient keyVaultClient, Set certificateFilterPatterns) { this.refreshInterval = refreshInterval; setKeyVaultClient(keyVaultClient); - this.certificateFilterPatterns - = new HashSet<>(Optional.ofNullable(certificateFilterPatterns).orElse(Collections.emptySet())); + this.certificateFilterPatterns = normalizeFilterPatterns(certificateFilterPatterns); this.includeAliasPatterns = getAliasPatterns(this.certificateFilterPatterns, false); this.excludeAliasPatterns = getAliasPatterns(this.certificateFilterPatterns, true); } + private Set normalizeFilterPatterns(Set filterPatterns) { + return Optional.ofNullable(filterPatterns) + .orElse(Collections.emptySet()) + .stream() + .filter(Objects::nonNull) + .map(String::trim) + .filter(pattern -> !pattern.isEmpty()) + .collect(Collectors.toCollection(HashSet::new)); + } + private List getAliasPatterns(Set filterPatterns, boolean excludePatterns) { return Optional.ofNullable(filterPatterns) .orElse(Collections.emptySet()) @@ -137,7 +147,8 @@ private Pattern compileRegexPattern(String regexPattern) { try { return Pattern.compile(regexPattern); } catch (PatternSyntaxException exception) { - throw new IllegalArgumentException("Invalid certificate filter regex pattern: " + regexPattern, exception); + throw new IllegalArgumentException("Invalid regex in system property '" + + CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY + "': " + regexPattern, exception); } } From a407a55556aef2326eaf6394b28daf7964a18118 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 13 Jul 2026 15:28:56 +0800 Subject: [PATCH 32/36] Refine alias filter error message and state usage --- .../certificates/KeyVaultCertificates.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) 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 34c143e98eee..33267845395e 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 @@ -76,8 +76,6 @@ public final class KeyVaultCertificates implements AzureCertificates { private final long refreshInterval; - private final Set certificateFilterPatterns; - private final List includeAliasPatterns; private final List excludeAliasPatterns; @@ -93,9 +91,9 @@ public KeyVaultCertificates(long refreshInterval, String keyVaultUri, String ten Set certificateFilterPatterns) { this.refreshInterval = refreshInterval; - this.certificateFilterPatterns = normalizeFilterPatterns(certificateFilterPatterns); - this.includeAliasPatterns = getAliasPatterns(this.certificateFilterPatterns, false); - this.excludeAliasPatterns = getAliasPatterns(this.certificateFilterPatterns, true); + Set normalizedFilterPatterns = normalizeFilterPatterns(certificateFilterPatterns); + this.includeAliasPatterns = getAliasPatterns(normalizedFilterPatterns, false); + this.excludeAliasPatterns = getAliasPatterns(normalizedFilterPatterns, true); updateKeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, accessToken, disableChallengeResourceVerification); @@ -109,9 +107,9 @@ public KeyVaultCertificates(long refreshInterval, KeyVaultClient keyVaultClient, Set certificateFilterPatterns) { this.refreshInterval = refreshInterval; setKeyVaultClient(keyVaultClient); - this.certificateFilterPatterns = normalizeFilterPatterns(certificateFilterPatterns); - this.includeAliasPatterns = getAliasPatterns(this.certificateFilterPatterns, false); - this.excludeAliasPatterns = getAliasPatterns(this.certificateFilterPatterns, true); + Set normalizedFilterPatterns = normalizeFilterPatterns(certificateFilterPatterns); + this.includeAliasPatterns = getAliasPatterns(normalizedFilterPatterns, false); + this.excludeAliasPatterns = getAliasPatterns(normalizedFilterPatterns, true); } private Set normalizeFilterPatterns(Set filterPatterns) { @@ -147,8 +145,9 @@ private Pattern compileRegexPattern(String regexPattern) { try { return Pattern.compile(regexPattern); } catch (PatternSyntaxException exception) { - throw new IllegalArgumentException("Invalid regex in system property '" - + CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY + "': " + regexPattern, exception); + throw new IllegalArgumentException("Invalid certificate alias filter regex pattern: " + regexPattern + + ". If configured via system property, check '" + CERTIFICATE_ALIAS_FILTER_PATTERNS_PROPERTY + "'.", + exception); } } From 31c94e25240b9b6461669408eea30fb7041004de Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 13 Jul 2026 16:01:03 +0800 Subject: [PATCH 33/36] Harden certificate lookup null safety and key map exposure --- .../security/keyvault/jca/KeyVaultKeyStore.java | 2 +- .../certificates/KeyVaultCertificates.java | 4 +++- .../certificates/KeyVaultCertificatesTest.java | 13 +++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) 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 6725d7e03320..74a1c20bc3fe 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 @@ -304,7 +304,7 @@ public String engineGetCertificateAlias(Certificate cert) { List aliasList = getAllAliases(); for (String candidateAlias : aliasList) { Certificate certificate = engineGetCertificate(candidateAlias); - if (certificate.equals(cert)) { + if (certificate != null && certificate.equals(cert)) { alias = candidateAlias; break; } 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 33267845395e..04a12a950031 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 @@ -260,7 +260,9 @@ public Map getCertificateChains() { @Override public Map getCertificateKeys() { refreshCertificatesIfNeeded(); - return certificateKeys; + synchronized (this) { + return new HashMap<>(certificateKeys); + } } /** 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 1c8e3872f3fa..603d2588c407 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 @@ -17,6 +17,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -55,6 +56,18 @@ public void testGetKey() { Assertions.assertEquals(key, keyVaultCertificates.getCertificateKey("myalias")); } + @Test + public void testGetCertificateKeysReturnsSnapshot() { + Assertions.assertEquals(key, keyVaultCertificates.getCertificateKey("myalias")); + + Map keySnapshot = keyVaultCertificates.getCertificateKeys(); + Assertions.assertEquals(key, keySnapshot.get("myalias")); + + keySnapshot.clear(); + + Assertions.assertEquals(key, keyVaultCertificates.getCertificateKeys().get("myalias")); + } + @Test public void testGetCertificate() { Assertions.assertEquals(certificate, keyVaultCertificates.getCertificate("myalias")); From fb8476255669ee38d9d7100d24c39b2a149d6002 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 13 Jul 2026 16:18:06 +0800 Subject: [PATCH 34/36] Defensively copy exposed certificate collections --- .../keyvault/jca/KeyVaultKeyStore.java | 5 +- .../certificates/KeyVaultCertificates.java | 22 +++++++-- .../KeyVaultCertificatesTest.java | 46 +++++++++++++++++++ 3 files changed, 66 insertions(+), 7 deletions(-) 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 74a1c20bc3fe..c8130ad3d713 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 @@ -343,9 +343,10 @@ public Certificate[] engineGetCertificateChain(String alias) { if (refreshCertificatesWhenHaveUnTrustCertificate && certificates == null) { keyVaultCertificates.refreshCertificates(); - return keyVaultCertificates.getCertificateChain(alias); + Certificate[] refreshedChain = keyVaultCertificates.getCertificateChain(alias); + return refreshedChain == null ? null : refreshedChain.clone(); } - return certificates; + return certificates == null ? null : certificates.clone(); } /** 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 04a12a950031..c66a996a0abc 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 @@ -227,8 +227,9 @@ synchronized boolean certificatesNeedRefresh() { @Override public List getAliases() { refreshCertificatesIfNeeded(); - - return aliases; + synchronized (this) { + return new ArrayList<>(aliases); + } } /** @@ -239,7 +240,9 @@ public List getAliases() { @Override public Map getCertificates() { refreshCertificatesIfNeeded(); - return certificates; + synchronized (this) { + return new HashMap<>(certificates); + } } /** @@ -249,7 +252,9 @@ public Map getCertificates() { @Override public Map getCertificateChains() { refreshCertificatesIfNeeded(); - return certificateChains; + synchronized (this) { + return copyCertificateChains(certificateChains); + } } /** @@ -300,10 +305,17 @@ public Certificate getCertificate(String alias) { public Certificate[] getCertificateChain(String alias) { loadCertificateChainIfNeeded(alias); synchronized (this) { - return certificateChains.get(alias); + Certificate[] chain = certificateChains.get(alias); + return chain == null ? null : chain.clone(); } } + private Map copyCertificateChains(Map source) { + Map copiedChains = new HashMap<>(); + source.forEach((alias, chain) -> copiedChains.put(alias, chain == null ? null : chain.clone())); + return copiedChains; + } + private void refreshCertificatesIfNeeded() { if (certificatesNeedRefresh()) { // Avoid acquiring the lock as much as possible. synchronized (this) { 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 603d2588c407..8dae6b534788 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 @@ -51,6 +51,16 @@ public void testGetAliases() { Assertions.assertTrue(keyVaultCertificates.getAliases().contains("myalias")); } + @Test + public void testGetAliasesReturnsSnapshot() { + List aliasSnapshot = keyVaultCertificates.getAliases(); + Assertions.assertTrue(aliasSnapshot.contains("myalias")); + + aliasSnapshot.clear(); + + Assertions.assertTrue(keyVaultCertificates.getAliases().contains("myalias")); + } + @Test public void testGetKey() { Assertions.assertEquals(key, keyVaultCertificates.getCertificateKey("myalias")); @@ -73,11 +83,47 @@ public void testGetCertificate() { Assertions.assertEquals(certificate, keyVaultCertificates.getCertificate("myalias")); } + @Test + public void testGetCertificatesReturnsSnapshot() { + Assertions.assertEquals(certificate, keyVaultCertificates.getCertificate("myalias")); + + Map certificateSnapshot = keyVaultCertificates.getCertificates(); + Assertions.assertEquals(certificate, certificateSnapshot.get("myalias")); + + certificateSnapshot.clear(); + + Assertions.assertEquals(certificate, keyVaultCertificates.getCertificates().get("myalias")); + } + @Test public void testGetCertificateChain() { Assertions.assertArrayEquals(certificateChain, keyVaultCertificates.getCertificateChain("myalias")); } + @Test + public void testGetCertificateChainReturnsClone() { + Certificate[] firstRead = keyVaultCertificates.getCertificateChain("myalias"); + Assertions.assertNotNull(firstRead); + + firstRead[0] = null; + + Certificate[] secondRead = keyVaultCertificates.getCertificateChain("myalias"); + Assertions.assertNotNull(secondRead); + Assertions.assertEquals(certificate, secondRead[0]); + } + + @Test + public void testGetCertificateChainsReturnsSnapshot() { + Assertions.assertArrayEquals(certificateChain, keyVaultCertificates.getCertificateChain("myalias")); + + Map chainSnapshot = keyVaultCertificates.getCertificateChains(); + Assertions.assertArrayEquals(certificateChain, chainSnapshot.get("myalias")); + + chainSnapshot.clear(); + + Assertions.assertArrayEquals(certificateChain, keyVaultCertificates.getCertificateChains().get("myalias")); + } + @Test public void testRefreshAndGetAliasByCertificate() { Assertions.assertEquals(keyVaultCertificates.refreshAndGetAliasByCertificate(certificate), "myalias"); From e4508413074f49aa6a86fa12c6dd1adc4928d47c Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 13 Jul 2026 16:29:20 +0800 Subject: [PATCH 35/36] Refine KeyVault certificate refresh locking --- .../certificates/KeyVaultCertificates.java | 55 ++++++++++--------- 1 file changed, 30 insertions(+), 25 deletions(-) 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 c66a996a0abc..c59237ad930e 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 @@ -318,40 +318,48 @@ private Map copyCertificateChains(Map refreshedAliases = 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(); + synchronized (this) { + if (currentKeyVaultClient != keyVaultClient || (!forceRefresh && !certificatesNeedRefresh())) { + return; + } + + // Discover aliases from Key Vault and apply include/exclude regex filters. + aliases = refreshedAliases; - lastRefreshTime = new Date(); + loadedCertificateAliases.clear(); + loadedCertificateChainAliases.clear(); + loadedCertificateKeyAliases.clear(); + certificateKeys.clear(); + certificates.clear(); + certificateChains.clear(); + + lastRefreshTime = new Date(); + } } private void loadCertificateIfNeeded(String alias) { @@ -363,8 +371,7 @@ private void loadCertificateIfNeeded(String alias) { } synchronized (this) { - if (loadedCertificateAliases.contains(alias) - || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + if (loadedCertificateAliases.contains(alias) || !aliases.contains(alias)) { return; } } @@ -374,7 +381,7 @@ private void loadCertificateIfNeeded(String alias) { synchronized (this) { if (currentKeyVaultClient != keyVaultClient || loadedCertificateAliases.contains(alias) - || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + || !aliases.contains(alias)) { return; } @@ -397,8 +404,7 @@ private void loadCertificateChainIfNeeded(String alias) { } synchronized (this) { - if (loadedCertificateChainAliases.contains(alias) - || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + if (loadedCertificateChainAliases.contains(alias) || !aliases.contains(alias)) { return; } } @@ -408,7 +414,7 @@ private void loadCertificateChainIfNeeded(String alias) { synchronized (this) { if (currentKeyVaultClient != keyVaultClient || loadedCertificateChainAliases.contains(alias) - || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + || !aliases.contains(alias)) { return; } @@ -431,8 +437,7 @@ private void loadCertificateKeyIfNeeded(String alias) { } synchronized (this) { - if (loadedCertificateKeyAliases.contains(alias) - || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + if (loadedCertificateKeyAliases.contains(alias) || !aliases.contains(alias)) { return; } } @@ -442,7 +447,7 @@ private void loadCertificateKeyIfNeeded(String alias) { synchronized (this) { if (currentKeyVaultClient != keyVaultClient || loadedCertificateKeyAliases.contains(alias) - || !Optional.ofNullable(aliases).orElse(Collections.emptyList()).contains(alias)) { + || !aliases.contains(alias)) { return; } @@ -472,7 +477,7 @@ public String refreshAndGetAliasByCertificate(Certificate certificate) { List aliasesSnapshot; synchronized (this) { - aliasesSnapshot = new ArrayList<>(Optional.ofNullable(aliases).orElse(Collections.emptyList())); + aliasesSnapshot = new ArrayList<>(aliases); } aliasesSnapshot.forEach(this::loadCertificateIfNeeded); From 7c232840a933b67dcb8189e3f162bd3d8d3490da Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Mon, 13 Jul 2026 16:44:35 +0800 Subject: [PATCH 36/36] Avoid duplicate certificate chain cloning --- .../com/azure/security/keyvault/jca/KeyVaultKeyStore.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 c8130ad3d713..5c009b5f0b3c 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 @@ -333,7 +333,8 @@ public Certificate[] engineGetCertificateChain(String alias) { if (certificatesSource instanceof KeyVaultCertificates) { certificates = ((KeyVaultCertificates) certificatesSource).getCertificateChain(alias); } else { - certificates = certificatesSource.getCertificateChains().get(alias); + Certificate[] certificateChain = certificatesSource.getCertificateChains().get(alias); + certificates = certificateChain == null ? null : certificateChain.clone(); } if (certificates != null) { @@ -343,10 +344,9 @@ public Certificate[] engineGetCertificateChain(String alias) { if (refreshCertificatesWhenHaveUnTrustCertificate && certificates == null) { keyVaultCertificates.refreshCertificates(); - Certificate[] refreshedChain = keyVaultCertificates.getCertificateChain(alias); - return refreshedChain == null ? null : refreshedChain.clone(); + return keyVaultCertificates.getCertificateChain(alias); } - return certificates == null ? null : certificates.clone(); + return certificates; } /**