From 1fcd94348f89c92fe530319a2fea032d42308909 Mon Sep 17 00:00:00 2001 From: Moary Chen Date: Mon, 17 Aug 2026 16:12:49 +0800 Subject: [PATCH 01/11] feat(spring): support disabling Key Vault JCA AIA downloads --- sdk/spring/CHANGELOG.md | 1 + .../spring-cloud-azure-autoconfigure/pom.xml | 2 +- .../jca/AzureKeyVaultSslBundleRegistrar.java | 4 +++ .../AzureKeyVaultSslBundleProperties.java | 12 +++++++ ...zureKeyVaultJcaAutoConfigurationTests.java | 3 ++ .../AzureKeyVaultSslBundleRegistrarTests.java | 34 +++++++++++++++++++ .../pom.xml | 2 +- 7 files changed, 56 insertions(+), 2 deletions(-) diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md index dec32cd81bb0..29d46841abf7 100644 --- a/sdk/spring/CHANGELOG.md +++ b/sdk/spring/CHANGELOG.md @@ -6,6 +6,7 @@ #### Features Added - Added `spring.ssl.bundle.keyvault..keystore.certificate-alias-filter-patterns` and `spring.ssl.bundle.keyvault..truststore.certificate-alias-filter-patterns` configuration. The patterns are passed to the Key Vault JCA provider to limit which certificate aliases are loaded. ([#50013](https://github.com/Azure/azure-sdk-for-java/issues/50013)) +- Added `spring.ssl.bundle.keyvault..keystore.disable-aia-download` and `spring.ssl.bundle.keyvault..truststore.disable-aia-download` configuration to disable automatic Authority Information Access (AIA) certificate downloads. The default is `false`. ([#47977](https://github.com/Azure/azure-sdk-for-java/pull/47977)) #### Bugs Fixed diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml b/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml index 4aef3f66ff6f..a2e17502d80e 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml +++ b/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml @@ -283,7 +283,7 @@ com.azure azure-security-keyvault-jca - 2.12.0 + 2.13.0-beta.1 true diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrar.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrar.java index 94ad331ebfce..92376f59c075 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrar.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrar.java @@ -46,6 +46,7 @@ public class AzureKeyVaultSslBundleRegistrar implements SslBundleRegistrar, Reso private final Map sslBundles; private static final String CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY = "azure.keyvault.jca.certificate-alias-filter-pattern"; + private static final String DISABLE_AIA_DOWNLOAD_PROPERTY = "azure.keyvault.jca.disable-aia-download"; private static final String[] JCA_SYSTEM_PROPERTY_KEYS = new String[]{ "azure.keyvault.uri", "azure.keyvault.tenant-id", @@ -54,6 +55,7 @@ public class AzureKeyVaultSslBundleRegistrar implements SslBundleRegistrar, Reso "azure.keyvault.managed-identity", "azure.keyvault.jca.certificates-refresh-interval", CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY, + DISABLE_AIA_DOWNLOAD_PROPERTY, "azure.keyvault.jca.refresh-certificates-when-have-un-trust-certificate", "azure.cert-path.well-known", "azure.cert-path.custom" @@ -221,6 +223,8 @@ private static void configureJcaKeyStoreSystemProperties(AzureKeyVaultJcaPropert }); pm.from(keyStoreProperties.isRefreshCertificatesWhenHaveUntrustedCertificate()) .to(v -> System.setProperty("azure.keyvault.jca.refresh-certificates-when-have-un-trust-certificate", Boolean.toString(v))); + pm.from(keyStoreProperties.isDisableAiaDownload()) + .to(v -> System.setProperty(DISABLE_AIA_DOWNLOAD_PROPERTY, Boolean.toString(v))); pm.from(keyStoreProperties.getCertificatePaths().getWellKnown()) .to(v -> resolvePath(resourceLoader, v).ifPresent(path -> System.setProperty("azure.cert-path.well-known", path))); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/properties/AzureKeyVaultSslBundleProperties.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/properties/AzureKeyVaultSslBundleProperties.java index e61c3a2e9cd5..143bc1c6f6e1 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/properties/AzureKeyVaultSslBundleProperties.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/properties/AzureKeyVaultSslBundleProperties.java @@ -74,6 +74,10 @@ public static class KeyStoreProperties { * Whether to enable refresh certificate when get untrusted certificate. */ private boolean refreshCertificatesWhenHaveUntrustedCertificate; + /** + * Whether to disable automatic Authority Information Access (AIA) certificate chain completion downloads. + */ + private boolean disableAiaDownload; /** * Time interval to refresh all Key Vault certificate. */ @@ -104,6 +108,14 @@ public void setRefreshCertificatesWhenHaveUntrustedCertificate(boolean refreshCe this.refreshCertificatesWhenHaveUntrustedCertificate = refreshCertificatesWhenHaveUntrustedCertificate; } + public boolean isDisableAiaDownload() { + return disableAiaDownload; + } + + public void setDisableAiaDownload(boolean disableAiaDownload) { + this.disableAiaDownload = disableAiaDownload; + } + public Duration getCertificatesRefreshInterval() { return certificatesRefreshInterval; } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultJcaAutoConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultJcaAutoConfigurationTests.java index bb26939cac1c..df5ac9c4649b 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultJcaAutoConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultJcaAutoConfigurationTests.java @@ -52,6 +52,7 @@ void keyVaultJca() { "spring.cloud.azure.keyvault.jca.vaults.kv2.endpoint=" + String.format(ENDPOINT, "test2"), "spring.ssl.bundle.keyvault.testBundle1.truststore.certificate-paths.custom=classpath:keyvault/certificate-paths/custom", "spring.ssl.bundle.keyvault.testBundle2.truststore.keyvault-ref=kv2", + "spring.ssl.bundle.keyvault.testBundle2.truststore.disable-aia-download=true", "spring.ssl.bundle.keyvault.testBundle2.truststore.certificate-alias-filter-patterns[0]=^prod-.*", "spring.ssl.bundle.keyvault.testBundle2.truststore.certificate-alias-filter-patterns[1]=!^prod-deprecated$", "spring.ssl.bundle.keyvault.testBundle3.truststore.keyvault-ref=kv1", @@ -75,10 +76,12 @@ void keyVaultJca() { assertThat(sslBundlesProperties.getKeyvault()).hasSize(3); assertThat(sslBundlesProperties.getKeyvault().get("testBundle1").getTruststore().getCertificatePaths().getCustom()).isEqualTo("classpath:keyvault/certificate-paths/custom"); assertThat(sslBundlesProperties.getKeyvault().get("testBundle2").getTruststore().getKeyvaultRef()).isEqualTo("kv2"); + assertThat(sslBundlesProperties.getKeyvault().get("testBundle2").getTruststore().isDisableAiaDownload()).isTrue(); assertThat(sslBundlesProperties.getKeyvault().get("testBundle2").getTruststore() .getCertificateAliasFilterPatterns()).containsExactly("^prod-.*", "!^prod-deprecated$"); assertThat(sslBundlesProperties.getKeyvault().get("testBundle3").getTruststore().getKeyvaultRef()).isEqualTo("kv1"); assertThat(sslBundlesProperties.getKeyvault().get("testBundle3").getKeystore().getKeyvaultRef()).isEqualTo("kv2"); + assertThat(sslBundlesProperties.getKeyvault().get("testBundle3").getKeystore().isDisableAiaDownload()).isFalse(); assertThat(sslBundlesProperties.getKeyvault().get("testBundle3").getKeystore() .getCertificateAliasFilterPatterns()).containsExactly("client-cert", "!old-client-cert"); }); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrarTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrarTests.java index 664f746f8dfb..25389965d9f4 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrarTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrarTests.java @@ -266,6 +266,40 @@ void configureCertificateAliasFilterPatterns() { } } + @Test + void configureDisableAiaDownload() { + AzureKeyVaultJcaProperties jcaProperties = new AzureKeyVaultJcaProperties(); + AzureKeyVaultSslBundleProperties sslBundleProperties = new AzureKeyVaultSslBundleProperties(); + AzureKeyVaultSslBundleRegistrar registrar = new AzureKeyVaultSslBundleRegistrar(jcaProperties, sslBundleProperties); + registrar.setResourceLoader(new DefaultResourceLoader()); + + try (MockedStatic keyStoreMockedStatic = mockStatic(KeyStore.class)) { + KeyStore keyStore = Mockito.mock(KeyStore.class); + List configuredValues = new ArrayList<>(); + keyStoreMockedStatic.when(() -> KeyStore.getInstance(KeyVaultJcaProvider.PROVIDER_NAME)) + .thenAnswer(invocation -> { + configuredValues.add(System.getProperty("azure.keyvault.jca.disable-aia-download")); + return keyStore; + }); + + AzureKeyVaultJcaProperties.JcaVaultProperties vaultProperties + = new AzureKeyVaultJcaProperties.JcaVaultProperties(); + vaultProperties.setEndpoint("https://test.vault.azure.net/"); + jcaProperties.getVaults().put("keyvault1", vaultProperties); + + AzureKeyVaultSslBundleProperties.KeyVaultSslBundleProperties bundleProperties + = new AzureKeyVaultSslBundleProperties.KeyVaultSslBundleProperties(); + bundleProperties.getKeystore().setKeyvaultRef("keyvault1"); + bundleProperties.getKeystore().setDisableAiaDownload(true); + bundleProperties.getTruststore().setKeyvaultRef("keyvault1"); + sslBundleProperties.getKeyvault().put("testBundle", bundleProperties); + + registrar.registerBundles(Mockito.mock(SslBundleRegistry.class)); + + assertThat(configuredValues).containsExactly("true", "false"); + } + } + @Test void keyVaultProviderNotInsertedAtHighestPriority() { AzureKeyVaultJcaProperties jcaProperties = new AzureKeyVaultJcaProperties(); diff --git a/sdk/spring/spring-cloud-azure-starter-keyvault-jca/pom.xml b/sdk/spring/spring-cloud-azure-starter-keyvault-jca/pom.xml index a635a7bf2137..a253d9ced485 100644 --- a/sdk/spring/spring-cloud-azure-starter-keyvault-jca/pom.xml +++ b/sdk/spring/spring-cloud-azure-starter-keyvault-jca/pom.xml @@ -64,7 +64,7 @@ com.azure azure-security-keyvault-jca - 2.12.0 + 2.13.0-beta.1 From 64678dd133109168351eac95be1cd3e090906c3c Mon Sep 17 00:00:00 2001 From: Moary Chen Date: Mon, 17 Aug 2026 16:13:20 +0800 Subject: [PATCH 02/11] fix(keyvault): capture AIA download setting per client --- .../azure-security-keyvault-jca/CHANGELOG.md | 2 +- .../azure-security-keyvault-jca/README.md | 2 +- .../keyvault/jca/KeyVaultKeyStore.java | 15 +- .../jca/implementation/KeyVaultClient.java | 31 +++- .../certificates/KeyVaultCertificates.java | 26 ++- .../utils/AiaCertificateChainUtil.java | 25 +-- .../implementation/utils/CertificateUtil.java | 20 ++- .../implementation/KeyVaultClientTest.java | 3 +- .../KeyVaultCertificatesTest.java | 4 +- .../utils/AiaCertificateChainTest.java | 148 +++++++++++------- .../utils/CertificateOrderTest.java | 10 +- .../utils/CertificateUtilTest.java | 6 +- 12 files changed, 192 insertions(+), 100 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md index 836a94cb3f47..7684970aa9b7 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md +++ b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md @@ -13,7 +13,7 @@ - Fixed bug: `jarsigner` reports invalid certificate chain (`PKIX path building failed: unable to find valid certification path to requested target`) when using a non-exportable Azure Key Vault certificate. When the certificate chain returned by Azure Key Vault does not end in a self-signed root, the missing issuer certificates are now resolved at runtime using the CA Issuers URL in the AIA (Authority Information Access) extension of each certificate. Responses are cached by URL so subsequent loads can reuse them without another network request. ([#44267](https://github.com/Azure/azure-sdk-for-java/issues/44267)) ### Other Changes -- Added system property `azure.keyvault.jca.disable-aia-download` to disable automatic AIA chain completion. AIA chain completion downloads certificates from URLs embedded in certificate extensions, so this allows locked-down environments to prevent those outbound HTTP(S) requests, mitigating potential SSRF-like attack vectors when loading untrusted certificates. Set to `true` to disable (defaults to `false` for backward compatibility). +- Added system property `azure.keyvault.jca.disable-aia-download` to disable automatic AIA chain completion. AIA chain completion downloads certificates from URLs embedded in certificate extensions, so this allows locked-down environments to prevent those outbound HTTP(S) requests, mitigating potential SSRF-like attack vectors when loading untrusted certificates. The value is captured when each Key Vault client is initialized and retained for lazy certificate-chain loading, so multiple keystores can use different settings without overwriting one another. Set to `true` to disable (defaults to `false`). ## 2.12.0 (2026-07-24) diff --git a/sdk/keyvault/azure-security-keyvault-jca/README.md b/sdk/keyvault/azure-security-keyvault-jca/README.md index c60d90c37933..0cafc09d8c59 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/README.md +++ b/sdk/keyvault/azure-security-keyvault-jca/README.md @@ -143,7 +143,7 @@ The JCA library supports configuring the following options: * `azure.keyvault.jca.certificates-refresh-interval-in-ms`: The refresh interval time. * `azure.keyvault.jca.certificate-alias-filter-pattern`: A regex that filters which Key Vault certificate aliases are eligible for lazy loading. Append a suffix to the property name to configure more than one filter, for example `azure.keyvault.jca.certificate-alias-filter-pattern.1` or `azure.keyvault.jca.certificate-alias-filter-pattern.prod`. If no such property is configured, all discovered Key Vault aliases are eligible for lazy loading. See "Filtering Key Vault certificate aliases" below. * `azure.keyvault.disable-challenge-resource-verification`: Indicates whether to disable verification that the authentication challenge resource matches the Key Vault or Managed HSM domain. -* `azure.keyvault.jca.disable-aia-download`: Set to `true` to disable automatic AIA (Authority Information Access) certificate chain completion. Chain completion is only attempted when the chain returned by Azure Key Vault is incomplete, meaning it holds a single certificate or is missing an intermediate CA. When disabled, the provider will return certificate chains as provided by Azure Key Vault without downloading missing intermediate CA certificates. Use this in locked-down environments or when processing untrusted certificates to prevent outbound HTTP(S) requests to URLs embedded in certificate extensions. Defaults to `false` for backward compatibility. +* `azure.keyvault.jca.disable-aia-download`: Set to `true` to disable automatic AIA (Authority Information Access) certificate chain completion. Chain completion is only attempted when the chain returned by Azure Key Vault is incomplete, meaning it holds a single certificate or is missing an intermediate CA. When disabled, the provider will return certificate chains as provided by Azure Key Vault without downloading missing intermediate CA certificates. Use this in locked-down environments or when processing untrusted certificates to prevent outbound HTTP(S) requests to URLs embedded in certificate extensions. Defaults to `false` for backward compatibility. The value is captured when a Key Vault keystore and its client are initialized; create a new keystore after changing the property. You can configure these properties using: ```java 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 f477dbf2de4d..e420ffcb9804 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 @@ -61,6 +61,7 @@ public final class KeyVaultKeyStore extends KeyStoreSpi { static final String CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY = "azure.keyvault.jca.certificate-alias-filter-pattern"; + static final String DISABLE_AIA_DOWNLOAD_PROPERTY = "azure.keyvault.jca.disable-aia-download"; /** * Stores the Jre key store certificates. @@ -99,6 +100,8 @@ public final class KeyVaultKeyStore extends KeyStoreSpi { private final boolean refreshCertificatesWhenHaveUnTrustCertificate; + private final boolean disableAiaDownload; + /** * Store the path where the well-known certificate is placed */ @@ -120,7 +123,8 @@ public final class KeyVaultKeyStore extends KeyStoreSpi { * azure.keyvault.tenantId, * azure.keyvault.clientId, * azure.keyvault.clientSecret and - * azure.keyvault.managedIdentity to initialize the + * azure.keyvault.managedIdentity, and + * azure.keyvault.jca.disable-aia-download to initialize the * Key Vault client. *

*/ @@ -136,6 +140,7 @@ public KeyVaultKeyStore() { String accessToken = System.getProperty("azure.keyvault.access-token"); boolean disableChallengeResourceVerification = Boolean.parseBoolean(System.getProperty("azure.keyvault.disable-challenge-resource-verification")); + disableAiaDownload = Boolean.parseBoolean(System.getProperty(DISABLE_AIA_DOWNLOAD_PROPERTY)); long refreshInterval = getRefreshInterval(); refreshCertificatesWhenHaveUnTrustCertificate = Optional.of("azure.keyvault.jca.refresh-certificates-when-have-un-trust-certificate") @@ -152,9 +157,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, getKeyVaultCertificateAliasFilterPatterns()); + keyVaultCertificates = new KeyVaultCertificates(refreshInterval, keyVaultUri, tenantId, clientId, clientSecret, + managedIdentity, accessToken, disableChallengeResourceVerification, disableAiaDownload, + getKeyVaultCertificateAliasFilterPatterns()); LOGGER.log(FINE, () -> String.format("Loaded Key Vault certificates: %s.", keyVaultCertificates.getAliases())); classpathCertificates = new ClasspathCertificates(); @@ -446,7 +451,7 @@ public void engineLoad(KeyStore.LoadStoreParameter param) { keyVaultCertificates.updateKeyVaultClient(parameter.getUri(), parameter.getTenantId(), parameter.getClientId(), parameter.getClientSecret(), parameter.getManagedIdentity(), - parameter.getAccessToken(), parameter.isChallengeResourceVerificationDisabled()); + parameter.getAccessToken(), parameter.isChallengeResourceVerificationDisabled(), disableAiaDownload); } classpathCertificates.loadCertificatesFromClasspath(); diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java index 62856e8ca44a..3bb0b961888b 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java @@ -59,6 +59,7 @@ */ public class KeyVaultClient { private static final Logger LOGGER = Logger.getLogger(KeyVaultClient.class.getName()); + private static final String DISABLE_AIA_DOWNLOAD_PROPERTY = "azure.keyvault.jca.disable-aia-download"; /** * Stores the Key Vault cloud URI. @@ -105,6 +106,11 @@ public class KeyVaultClient { */ private final boolean disableChallengeResourceVerification; + /** + * Stores a flag indicating whether AIA certificate downloads are disabled. + */ + private final boolean disableAiaDownload; + /** * Constructor for authentication with user-assigned managed identity. * @@ -156,6 +162,25 @@ public KeyVaultClient(String keyVaultUri, String tenantId, String clientId, Stri */ public KeyVaultClient(String keyVaultUri, String tenantId, String clientId, String clientSecret, String managedIdentity, String providedAccessToken, boolean disableChallengeResourceVerification) { + this(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, providedAccessToken, + disableChallengeResourceVerification, false); + } + + /** + * Constructor. + * + * @param keyVaultUri The Azure Key Vault URI. + * @param tenantId The tenant ID. + * @param clientId The client ID. + * @param clientSecret The client secret. + * @param managedIdentity The user-assigned managed identity object ID. + * @param providedAccessToken The access token for authentication. + * @param disableChallengeResourceVerification Indicates if the challenge resource verification should be disabled. + * @param disableAiaDownload Indicates if AIA certificate downloads should be disabled. + */ + public KeyVaultClient(String keyVaultUri, String tenantId, String clientId, String clientSecret, + String managedIdentity, String providedAccessToken, boolean disableChallengeResourceVerification, + boolean disableAiaDownload) { LOGGER.log(INFO, "Using Azure Key Vault: {0}", keyVaultUri); @@ -172,6 +197,7 @@ public KeyVaultClient(String keyVaultUri, String tenantId, String clientId, Stri this.managedIdentity = managedIdentity; this.providedAccessToken = providedAccessToken; this.disableChallengeResourceVerification = disableChallengeResourceVerification; + this.disableAiaDownload = disableAiaDownload; } public static KeyVaultClient createKeyVaultClientBySystemProperty() { @@ -183,9 +209,10 @@ public static KeyVaultClient createKeyVaultClientBySystemProperty() { String accessToken = System.getProperty("azure.keyvault.access-token"); boolean disableChallengeResourceVerification = Boolean.parseBoolean(System.getProperty("azure.keyvault.disable-challenge-resource-verification")); + boolean disableAiaDownload = Boolean.parseBoolean(System.getProperty(DISABLE_AIA_DOWNLOAD_PROPERTY)); return new KeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, accessToken, - disableChallengeResourceVerification); + disableChallengeResourceVerification, disableAiaDownload); } /** @@ -464,7 +491,7 @@ public Certificate[] getCertificateChainForVersion(CertificateVersion certificat } try { - Certificate[] certificates = loadCertificatesFromSecretBundleValue(secretValue); + Certificate[] certificates = loadCertificatesFromSecretBundleValue(secretValue, disableAiaDownload); LOGGER.exiting("KeyVaultClient", "getCertificateChainForVersion", alias); return certificates; } catch (IOException | KeyStoreException | NoSuchAlgorithmException | CertificateException 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 56a493f5e720..987df1fa7ee0 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 @@ -113,12 +113,26 @@ public final class KeyVaultCertificates implements AzureCertificates { 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()); + disableChallengeResourceVerification, false, Collections.emptySet()); } + /** + * Creates a filtered Key Vault certificate cache with an explicit AIA download setting. + * + * @param refreshInterval Certificate refresh interval in milliseconds. + * @param keyVaultUri Key Vault URI. + * @param tenantId Tenant ID. + * @param clientId Client ID. + * @param clientSecret Client secret. + * @param managedIdentity Managed identity. + * @param accessToken Access token. + * @param disableChallengeResourceVerification Indicates if challenge resource verification should be disabled. + * @param disableAiaDownload Indicates if AIA certificate downloads should be disabled. + * @param certificateFilterPatterns Certificate alias filter patterns. + */ public KeyVaultCertificates(long refreshInterval, String keyVaultUri, String tenantId, String clientId, String clientSecret, String managedIdentity, String accessToken, boolean disableChallengeResourceVerification, - Set certificateFilterPatterns) { + boolean disableAiaDownload, Set certificateFilterPatterns) { this.refreshInterval = refreshInterval; Set normalizedFilterPatterns = normalizeFilterPatterns(certificateFilterPatterns); @@ -126,7 +140,7 @@ public KeyVaultCertificates(long refreshInterval, String keyVaultUri, String ten this.excludeAliasPatterns = getAliasPatterns(normalizedFilterPatterns, true); updateKeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, accessToken, - disableChallengeResourceVerification); + disableChallengeResourceVerification, disableAiaDownload); } public KeyVaultCertificates(long refreshInterval, KeyVaultClient keyVaultClient) { @@ -213,13 +227,15 @@ private synchronized void setKeyVaultClient(KeyVaultClient keyVaultClient) { * @param managedIdentity Managed identity. * @param accessToken Access token. * @param disableChallengeResourceVerification Indicates if the challenge resource verification should be disabled. + * @param disableAiaDownload Indicates if AIA certificate downloads should be disabled. */ public synchronized void updateKeyVaultClient(String keyVaultUri, String tenantId, String clientId, - String clientSecret, String managedIdentity, String accessToken, boolean disableChallengeResourceVerification) { + String clientSecret, String managedIdentity, String accessToken, boolean disableChallengeResourceVerification, + boolean disableAiaDownload) { if (keyVaultUri != null) { setKeyVaultClient(new KeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, - accessToken, disableChallengeResourceVerification)); + accessToken, disableChallengeResourceVerification, disableAiaDownload)); } else { setKeyVaultClient(null); } diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java index 88af091e5550..3c43fffafa55 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java @@ -83,30 +83,19 @@ static boolean shouldCompleteChainViaAia(Certificate[] certificates) { } /** - * Completes an incomplete certificate chain by downloading missing intermediate CA certificates - * using the AIA (Authority Information Access) extension embedded in each certificate. + * Completes an incomplete certificate chain when AIA downloads are allowed for the owning Key Vault client. * - *

Because completion may issue outbound HTTP requests on a cache miss, callers must restrict it to chains - * whose valid path does not end in a self-signed root (see {@link #shouldCompleteChainViaAia(Certificate[])}). - * - *

The method walks up the contiguous issuer path (leaf → intermediate → root) starting from - * the first certificate, downloading missing intermediates via AIA. Downloaded issuers are inserted - * immediately after the current end of the valid chain (before any unplaced/extra certificates). - * This process repeats until the chain reaches a self-signed root CA, no more AIA URLs are found, or - * the safety download limit is reached. - * - * @param orderedCertificates certificate array with contiguous issuer path + any unplaced certs appended - * @return the (potentially extended) certificate array with missing intermediates inserted in the valid chain + * @param orderedCertificates Certificate array with a contiguous issuer path and any unplaced certificates. + * @param disableAiaDownload Indicates if AIA certificate downloads should be disabled. + * @return The original or completed certificate chain. */ - static Certificate[] completeChainViaAia(Certificate[] orderedCertificates) { + static Certificate[] completeChainViaAia(Certificate[] orderedCertificates, boolean disableAiaDownload) { if (orderedCertificates == null || orderedCertificates.length == 0) { return orderedCertificates; } - // Check if AIA downloading is disabled by system property - String disableAiaDownload = System.getProperty(DISABLE_AIA_DOWNLOAD_PROPERTY); - if ("true".equalsIgnoreCase(disableAiaDownload)) { - LOGGER.log(FINE, "AIA chain completion is disabled by system property [{0}]", + if (disableAiaDownload) { + LOGGER.log(FINE, "AIA chain completion is disabled for this Key Vault client by configuration [{0}]", DISABLE_AIA_DOWNLOAD_PROPERTY); return orderedCertificates; } diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/CertificateUtil.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/CertificateUtil.java index 7e0c0733b7ab..027f81075d67 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/CertificateUtil.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/CertificateUtil.java @@ -41,8 +41,22 @@ public final class CertificateUtil { static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----"; private static final String END_CERTIFICATE = "-----END CERTIFICATE-----"; - public static Certificate[] loadCertificatesFromSecretBundleValue(String string) throws CertificateException, - IOException, KeyStoreException, NoSuchAlgorithmException, NoSuchProviderException, PKCSException { + /** + * Loads certificates from a Key Vault secret bundle value. + * + * @param string The secret bundle value. + * @param disableAiaDownload Indicates if AIA certificate downloads should be disabled. + * @return The loaded certificate chain. + * @throws CertificateException If a certificate cannot be parsed. + * @throws IOException If the secret bundle cannot be read. + * @throws KeyStoreException If the PKCS12 key store cannot be loaded. + * @throws NoSuchAlgorithmException If a required algorithm is unavailable. + * @throws NoSuchProviderException If a required provider is unavailable. + * @throws PKCSException If the PKCS data cannot be parsed. + */ + public static Certificate[] loadCertificatesFromSecretBundleValue(String string, boolean disableAiaDownload) + throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException, NoSuchProviderException, + PKCSException { Certificate[] certificates; if (string.contains(BEGIN_CERTIFICATE)) { certificates = loadCertificatesFromSecretBundleValuePem(string); @@ -59,7 +73,7 @@ public static Certificate[] loadCertificatesFromSecretBundleValue(String string) if (AiaCertificateChainUtil.shouldCompleteChainViaAia(certificates)) { LOGGER.log(FINE, "Certificate chain requires AIA completion; ordered chain contains {0} certificate(s).", certificates.length); - certificates = AiaCertificateChainUtil.completeChainViaAia(certificates); + certificates = AiaCertificateChainUtil.completeChainViaAia(certificates, disableAiaDownload); } else { LOGGER.log(FINE, "Certificate chain does not require AIA completion; ordered chain contains {0} " + "certificate(s).", diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/KeyVaultClientTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/KeyVaultClientTest.java index 43a275217983..503488e57f8b 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/KeyVaultClientTest.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/KeyVaultClientTest.java @@ -615,7 +615,8 @@ public void testCertificateChainDecodedWithoutCertificatesReturnsEmpty() throws configureHttpUtilityMethods(utilities); utilities.when(() -> HttpUtil.get(eq(VERSIONED_SECRET_ID + API_VERSION_POSTFIX), anyMap())) .thenReturn(JsonConverterUtil.toJson(secretBundle)); - certificateUtilities.when(() -> CertificateUtil.loadCertificatesFromSecretBundleValue("valid-empty-chain")) + certificateUtilities + .when(() -> CertificateUtil.loadCertificatesFromSecretBundleValue("valid-empty-chain", false)) .thenReturn(new Certificate[0]); KeyVaultClient keyVaultClient = createClientWithAccessToken(); 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 f055bf26f46a..290b5a33f4cc 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 @@ -535,7 +535,7 @@ public void testUpdateKeyVaultClientClearsCachedState() { Assertions.assertTrue(keyVaultCertificates.getAliases().contains("myalias")); Assertions.assertEquals(certificate, keyVaultCertificates.getCertificate("myalias")); - keyVaultCertificates.updateKeyVaultClient(null, null, null, null, null, null, false); + keyVaultCertificates.updateKeyVaultClient(null, null, null, null, null, null, false, false); Assertions.assertTrue(keyVaultCertificates.getAliases().isEmpty()); Assertions.assertTrue(keyVaultCertificates.getCertificates().isEmpty()); @@ -640,7 +640,7 @@ public void testClientReplacementDiscardsInFlightCertificate() throws Exception reader.start(); staleAnswer.awaitStarted(); - keyVaultCertificates.updateKeyVaultClient(null, null, null, null, null, null, false); + keyVaultCertificates.updateKeyVaultClient(null, null, null, null, null, null, false, false); staleAnswer.release(); joinThreads(Collections.singletonList(reader)); diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainTest.java index bffccde0e0d1..5346486431d1 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainTest.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainTest.java @@ -3,6 +3,9 @@ package com.azure.security.keyvault.jca.implementation.utils; +import com.azure.security.keyvault.jca.implementation.CertificateVersion; +import com.azure.security.keyvault.jca.implementation.KeyVaultClient; +import com.azure.security.keyvault.jca.implementation.model.SecretBundle; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.AccessDescription; import org.bouncycastle.asn1.x509.AuthorityInformationAccess; @@ -64,6 +67,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Tests for AIA-based certificate chain completion in {@link CertificateUtil}. @@ -164,7 +169,7 @@ void completeChainViaAiaLeafOnlyDownloadsIntermediateAndRoot() throws Exception mockAiaResponse(httpMock, AIA_INTERMEDIATE_URL, intermediateCert.getEncoded()); mockAiaResponse(httpMock, AIA_ROOT_URL, rootCert.getEncoded()); - Certificate[] completed = AiaCertificateChainUtil.completeChainViaAia(leafOnly); + Certificate[] completed = AiaCertificateChainUtil.completeChainViaAia(leafOnly, false); assertEquals(3, completed.length, "Chain should contain leaf + intermediate + root"); assertEquals(leafCert, completed[0], "First cert should be the leaf"); @@ -181,7 +186,7 @@ void completeChainViaAiaLeafAndIntermediateDownloadsRootOnly() throws Exception try (MockedStatic httpMock = Mockito.mockStatic(HttpUtil.class)) { mockAiaResponse(httpMock, AIA_ROOT_URL, rootCert.getEncoded()); - Certificate[] completed = AiaCertificateChainUtil.completeChainViaAia(partial); + Certificate[] completed = AiaCertificateChainUtil.completeChainViaAia(partial, false); assertEquals(3, completed.length, "Chain should contain leaf + intermediate + root"); assertEquals(rootCert, completed[2]); @@ -194,7 +199,7 @@ void completeChainViaAiaFullChainNoDownloadNeeded() throws Exception { Certificate[] full = new Certificate[] { leafCert, intermediateCert, rootCert }; try (MockedStatic httpMock = Mockito.mockStatic(HttpUtil.class)) { - Certificate[] result = AiaCertificateChainUtil.completeChainViaAia(full); + Certificate[] result = AiaCertificateChainUtil.completeChainViaAia(full, false); assertEquals(3, result.length); httpMock.verifyNoInteractions(); @@ -208,7 +213,7 @@ void completeChainViaAiaDownloadFailsReturnsOriginal() throws Exception { try (MockedStatic httpMock = Mockito.mockStatic(HttpUtil.class)) { mockAiaResponse(httpMock, AIA_INTERMEDIATE_URL, null); - Certificate[] result = AiaCertificateChainUtil.completeChainViaAia(leafOnly); + Certificate[] result = AiaCertificateChainUtil.completeChainViaAia(leafOnly, false); assertEquals(1, result.length, "Should return original chain when download fails"); } @@ -216,12 +221,12 @@ void completeChainViaAiaDownloadFailsReturnsOriginal() throws Exception { @Test void completeChainViaAiaNullInputReturnsNull() { - assertNull(AiaCertificateChainUtil.completeChainViaAia(null)); + assertNull(AiaCertificateChainUtil.completeChainViaAia(null, false)); } @Test void completeChainViaAiaEmptyInputReturnsEmpty() { - Certificate[] result = AiaCertificateChainUtil.completeChainViaAia(new Certificate[0]); + Certificate[] result = AiaCertificateChainUtil.completeChainViaAia(new Certificate[0], false); assertEquals(0, result.length); } @@ -280,7 +285,7 @@ void completeChainViaAiaRejectsIssuerWithoutKeyCertSign() throws Exception { mockAiaResponse(httpMock, AIA_BAD_ISSUER_URL, badIssuerCert.getEncoded()); Certificate[] result - = AiaCertificateChainUtil.completeChainViaAia(new Certificate[] { leafWithBadIssuerAia }); + = AiaCertificateChainUtil.completeChainViaAia(new Certificate[] { leafWithBadIssuerAia }, false); assertEquals(1, result.length, "Issuer without keyCertSign should be rejected even if basicConstraints indicates CA"); @@ -310,7 +315,7 @@ void completeChainViaAiaRejectsExpiredIssuer() throws Exception { mockAiaResponse(httpMock, AIA_BAD_ISSUER_URL, expiredIssuerCert.getEncoded()); Certificate[] result - = AiaCertificateChainUtil.completeChainViaAia(new Certificate[] { leafWithExpiredAia }); + = AiaCertificateChainUtil.completeChainViaAia(new Certificate[] { leafWithExpiredAia }, false); assertEquals(1, result.length, "An expired issuer certificate must be rejected and not inserted into the chain"); @@ -386,7 +391,7 @@ void pkixPathBuildingWithFixSucceeds() throws Exception { try (MockedStatic httpMock = Mockito.mockStatic(HttpUtil.class)) { mockAiaResponse(httpMock, AIA_INTERMEDIATE_URL, intermediateCert.getEncoded()); mockAiaResponse(httpMock, AIA_ROOT_URL, rootCert.getEncoded()); - completedChain = AiaCertificateChainUtil.completeChainViaAia(leafOnly); + completedChain = AiaCertificateChainUtil.completeChainViaAia(leafOnly, false); } assertEquals(3, completedChain.length, "Chain should be leaf + intermediate + root after fix"); @@ -412,43 +417,78 @@ void pkixPathBuildingWithFixSucceeds() throws Exception { "Path should contain leaf + intermediate (root is the trust anchor, not in path)"); } - /** - * Verifies that AIA chain completion can be disabled via system property. - * - *

When the system property {@code azure.keyvault.jca.disable-aia-download} is set to {@code true}, - * the AIA chain completion is skipped and the original chain is returned unchanged. - */ @Test - void aiaDownloadDisabledBySystemProperty() throws Exception { - // Set the disable system property - String originalValue = System.getProperty(AiaCertificateChainUtil.DISABLE_AIA_DOWNLOAD_PROPERTY); - System.setProperty(AiaCertificateChainUtil.DISABLE_AIA_DOWNLOAD_PROPERTY, "true"); + void aiaDownloadCanBeDisabled() throws Exception { + try (MockedStatic httpMock = Mockito.mockStatic(HttpUtil.class)) { + Certificate[] result = CertificateUtil.loadCertificatesFromSecretBundleValue(toPem(leafCert), true); - try { - // Simulate AKV returning only the leaf cert - Certificate[] leafOnly = new Certificate[] { leafCert }; + assertEquals(1, result.length, "Chain should remain unchanged when AIA download is disabled"); + assertEquals(leafCert, result[0], "The returned certificate should be the leaf certificate"); + httpMock.verify(() -> HttpUtil.getBytesWithMetadata(Mockito.anyString()), Mockito.never()); + } + } - // Mock HttpUtil BEFORE calling completeChainViaAia to ensure property check - // doesn't trigger real network I/O if it regresses - try (MockedStatic httpMock = Mockito.mockStatic(HttpUtil.class)) { - // Call completeChainViaAia with the property set to true - // It should return the same array without downloading anything - Certificate[] result = AiaCertificateChainUtil.completeChainViaAia(leafOnly); + @Test + void keyVaultClientKeepsAiaDownloadSettingFromConstruction() throws Exception { + String secretId = "https://fake.vault.azure.net/secrets/aia-test/version"; + SecretBundle secretBundle = new SecretBundle(); + secretBundle.setValue(toPem(leafCert)); + CertificateVersion certificateVersion = mock(CertificateVersion.class); + when(certificateVersion.getAlias()).thenReturn("aia-test"); + when(certificateVersion.getSecretId()).thenReturn(secretId); - // Verify the chain was NOT extended (still only 1 certificate) - assertEquals(1, result.length, "Chain should remain unchanged when AIA download is disabled"); - assertEquals(leafCert, result[0], "The returned certificate should be the leaf certificate"); + KeyVaultClient keyVaultClient + = new KeyVaultClient("https://fake.vault.azure.net/", null, null, null, null, "test-token", false, true); - // Verify that no HTTP calls were made (HttpUtil.getBytes should not be called) - httpMock.verify(() -> HttpUtil.getBytesWithMetadata(Mockito.anyString()), Mockito.never()); - } - } finally { - // Clean up: restore the original property value - if (originalValue != null) { - System.setProperty(AiaCertificateChainUtil.DISABLE_AIA_DOWNLOAD_PROPERTY, originalValue); - } else { - System.clearProperty(AiaCertificateChainUtil.DISABLE_AIA_DOWNLOAD_PROPERTY); - } + // Simulate another SSL bundle replacing the JVM-global value before this client lazily loads its chain. + System.setProperty(AiaCertificateChainUtil.DISABLE_AIA_DOWNLOAD_PROPERTY, "false"); + + try (MockedStatic httpMock = Mockito.mockStatic(HttpUtil.class)) { + httpMock + .when(() -> HttpUtil.get(secretId + HttpUtil.API_VERSION_POSTFIX, + Collections.singletonMap("Authorization", "Bearer test-token"))) + .thenReturn(JsonConverterUtil.toJson(secretBundle)); + mockAiaResponse(httpMock, AIA_INTERMEDIATE_URL, intermediateCert.getEncoded()); + mockAiaResponse(httpMock, AIA_ROOT_URL, rootCert.getEncoded()); + + Certificate[] result = keyVaultClient.getCertificateChainForVersion(certificateVersion); + + assertArrayEquals(new Certificate[] { leafCert }, result, + "The client must keep the AIA setting captured when it was constructed"); + httpMock.verify(() -> HttpUtil.getBytesWithMetadata(AIA_INTERMEDIATE_URL), Mockito.never()); + httpMock.verify(() -> HttpUtil.getBytesWithMetadata(AIA_ROOT_URL), Mockito.never()); + } + } + + @Test + void keyVaultClientKeepsAiaDownloadEnabledFromConstruction() throws Exception { + String secretId = "https://fake.vault.azure.net/secrets/aia-enabled/version"; + SecretBundle secretBundle = new SecretBundle(); + secretBundle.setValue(toPem(leafCert)); + CertificateVersion certificateVersion = mock(CertificateVersion.class); + when(certificateVersion.getAlias()).thenReturn("aia-enabled"); + when(certificateVersion.getSecretId()).thenReturn(secretId); + + KeyVaultClient keyVaultClient + = new KeyVaultClient("https://fake.vault.azure.net/", null, null, null, null, "test-token", false, false); + + // Simulate another SSL bundle replacing the JVM-global value before this client lazily loads its chain. + System.setProperty(AiaCertificateChainUtil.DISABLE_AIA_DOWNLOAD_PROPERTY, "true"); + + try (MockedStatic httpMock = Mockito.mockStatic(HttpUtil.class)) { + httpMock + .when(() -> HttpUtil.get(secretId + HttpUtil.API_VERSION_POSTFIX, + Collections.singletonMap("Authorization", "Bearer test-token"))) + .thenReturn(JsonConverterUtil.toJson(secretBundle)); + mockAiaResponse(httpMock, AIA_INTERMEDIATE_URL, intermediateCert.getEncoded()); + mockAiaResponse(httpMock, AIA_ROOT_URL, rootCert.getEncoded()); + + Certificate[] result = keyVaultClient.getCertificateChainForVersion(certificateVersion); + + assertArrayEquals(new Certificate[] { leafCert, intermediateCert, rootCert }, result, + "The client must keep the AIA setting captured when it was constructed"); + httpMock.verify(() -> HttpUtil.getBytesWithMetadata(AIA_INTERMEDIATE_URL), Mockito.times(1)); + httpMock.verify(() -> HttpUtil.getBytesWithMetadata(AIA_ROOT_URL), Mockito.times(1)); } } @@ -462,7 +502,7 @@ void loadCertificatesCompletesLeafOnlyChain() throws Exception { mockAiaResponse(httpMock, AIA_INTERMEDIATE_URL, intermediateCert.getEncoded()); mockAiaResponse(httpMock, AIA_ROOT_URL, rootCert.getEncoded()); - Certificate[] result = CertificateUtil.loadCertificatesFromSecretBundleValue(toPem(leafCert)); + Certificate[] result = CertificateUtil.loadCertificatesFromSecretBundleValue(toPem(leafCert), false); assertArrayEquals(new Certificate[] { leafCert, intermediateCert, rootCert }, result, "A leaf-only bundle must be completed up to the root CA"); @@ -475,7 +515,7 @@ void loadCertificatesCompletesChainWithMissingIntermediate() throws Exception { mockAiaResponse(httpMock, AIA_INTERMEDIATE_URL, intermediateCert.getEncoded()); Certificate[] result - = CertificateUtil.loadCertificatesFromSecretBundleValue(toPem(leafCert) + toPem(rootCert)); + = CertificateUtil.loadCertificatesFromSecretBundleValue(toPem(leafCert) + toPem(rootCert), false); assertArrayEquals(new Certificate[] { leafCert, intermediateCert, rootCert }, result, "An intermediate missing in the middle of the chain must still be downloaded"); @@ -488,10 +528,10 @@ void loadCertificatesCompletesChainWithoutRootAndCachesIssuer() throws Exception try (MockedStatic httpMock = Mockito.mockStatic(HttpUtil.class)) { mockAiaResponse(httpMock, AIA_ROOT_URL, rootCert.getEncoded()); - Certificate[] firstResult - = CertificateUtil.loadCertificatesFromSecretBundleValue(toPem(leafCert) + toPem(intermediateCert)); - Certificate[] secondResult - = CertificateUtil.loadCertificatesFromSecretBundleValue(toPem(leafCert) + toPem(intermediateCert)); + Certificate[] firstResult = CertificateUtil + .loadCertificatesFromSecretBundleValue(toPem(leafCert) + toPem(intermediateCert), false); + Certificate[] secondResult = CertificateUtil + .loadCertificatesFromSecretBundleValue(toPem(leafCert) + toPem(intermediateCert), false); assertArrayEquals(new Certificate[] { leafCert, intermediateCert, rootCert }, firstResult, "A contiguous chain must still be completed when its terminal certificate is not self-signed"); @@ -505,8 +545,8 @@ void loadCertificatesCompletesChainWithoutRootAndCachesIssuer() throws Exception @Test void loadCertificatesSkipsAiaForCompleteChain() throws Exception { try (MockedStatic httpMock = Mockito.mockStatic(HttpUtil.class)) { - Certificate[] result = CertificateUtil - .loadCertificatesFromSecretBundleValue(toPem(leafCert) + toPem(intermediateCert) + toPem(rootCert)); + Certificate[] result = CertificateUtil.loadCertificatesFromSecretBundleValue( + toPem(leafCert) + toPem(intermediateCert) + toPem(rootCert), false); assertArrayEquals(new Certificate[] { leafCert, intermediateCert, rootCert }, result); httpMock.verifyNoInteractions(); @@ -531,8 +571,8 @@ void loadCertificatesKeepsChainWithExpiredIssuerUntouched() throws Exception { "CN=Expired CA", expiredCaKeyPair.getPrivate(), false, AIA_INTERMEDIATE_URL); try (MockedStatic httpMock = Mockito.mockStatic(HttpUtil.class)) { - Certificate[] result - = CertificateUtil.loadCertificatesFromSecretBundleValue(toPem(leafOfExpiredCa) + toPem(expiredCaCert)); + Certificate[] result = CertificateUtil + .loadCertificatesFromSecretBundleValue(toPem(leafOfExpiredCa) + toPem(expiredCaCert), false); assertArrayEquals(new Certificate[] { leafOfExpiredCa, expiredCaCert }, result, "An expired certificate already in the chain must not change how the chain is ordered"); @@ -981,8 +1021,8 @@ public void close() { mockAiaResponse(httpMock, AIA_ROOT_URL, rootCert.getEncoded()); // The second run resolves the same two issuers entirely from the cache. - AiaCertificateChainUtil.completeChainViaAia(new Certificate[] { leafCert }); - AiaCertificateChainUtil.completeChainViaAia(new Certificate[] { leafCert }); + AiaCertificateChainUtil.completeChainViaAia(new Certificate[] { leafCert }, false); + AiaCertificateChainUtil.completeChainViaAia(new Certificate[] { leafCert }, false); httpMock.verify(() -> HttpUtil.getBytesWithMetadata(AIA_INTERMEDIATE_URL), Mockito.times(1)); httpMock.verify(() -> HttpUtil.getBytesWithMetadata(AIA_ROOT_URL), Mockito.times(1)); @@ -1021,7 +1061,7 @@ void completeChainViaAiaTerminatesOnCrossSignedIssuers() throws Exception { keyPairA.getPrivate(), true, null); Certificate[] result - = AiaCertificateChainUtil.completeChainViaAia(new Certificate[] { crossSignedA, crossSignedB }); + = AiaCertificateChainUtil.completeChainViaAia(new Certificate[] { crossSignedA, crossSignedB }, false); assertArrayEquals(new Certificate[] { crossSignedA, crossSignedB }, result, "Cross-signed issuers must be left in place instead of being repositioned"); diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/CertificateOrderTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/CertificateOrderTest.java index 495ef0ffb687..bb03870c86fc 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/CertificateOrderTest.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/CertificateOrderTest.java @@ -37,7 +37,7 @@ public void testPemCertificateChainOrder() throws CertificateException, IOExcept Paths.get("src/test/resources/certificate-util/SecretBundle.value/3-certificates-in-chain.pem")), StandardCharsets.UTF_8); - Certificate[] certs = CertificateUtil.loadCertificatesFromSecretBundleValue(pemString); + Certificate[] certs = CertificateUtil.loadCertificatesFromSecretBundleValue(pemString, false); assertEquals(3, certs.length, "Should have 3 certificates in chain"); @@ -79,7 +79,7 @@ public void testPkcs12CertificateChainOrder() throws CertificateException, IOExc Paths.get("src/test/resources/certificate-util/SecretBundle.value/3-certificates-in-chain.pfx")), StandardCharsets.UTF_8); - Certificate[] certs = CertificateUtil.loadCertificatesFromSecretBundleValue(pfxString); + Certificate[] certs = CertificateUtil.loadCertificatesFromSecretBundleValue(pfxString, false); assertEquals(3, certs.length, "Should have 3 certificates in chain"); @@ -121,7 +121,7 @@ public void testOrderCertificateChainReversed() throws CertificateException, IOE Paths.get("src/test/resources/certificate-util/SecretBundle.value/3-certificates-in-chain.pem")), StandardCharsets.UTF_8); - Certificate[] certs = CertificateUtil.loadCertificatesFromSecretBundleValue(pemString); + Certificate[] certs = CertificateUtil.loadCertificatesFromSecretBundleValue(pemString, false); // Reverse the certificate order to simulate the issue Certificate[] reversedCerts = new Certificate[certs.length]; @@ -170,7 +170,7 @@ public void testOrderCertificateChainEdgeCases() throws CertificateException, IO Files.readAllBytes( Paths.get("src/test/resources/certificate-util/SecretBundle.value/3-certificates-in-chain.pem")), StandardCharsets.UTF_8); - Certificate concreteSingleCert = CertificateUtil.loadCertificatesFromSecretBundleValue(pemString)[0]; + Certificate concreteSingleCert = CertificateUtil.loadCertificatesFromSecretBundleValue(pemString, false)[0]; Certificate[] singleCert = new Certificate[] { concreteSingleCert }; result = CertificateUtil.orderCertificateChain(singleCert); assertEquals(1, result.length, "Should return single certificate unchanged"); @@ -194,7 +194,7 @@ public void testOrderCertificateChainIncompleteRootFirst() throws CertificateExc Paths.get("src/test/resources/certificate-util/SecretBundle.value/3-certificates-in-chain.pem")), StandardCharsets.UTF_8); - Certificate[] fullChain = CertificateUtil.loadCertificatesFromSecretBundleValue(fullChainPem); + Certificate[] fullChain = CertificateUtil.loadCertificatesFromSecretBundleValue(fullChainPem, false); assertEquals(3, fullChain.length, "Full chain should have 3 certificates"); // Create incomplete chain: [root, leaf] (missing intermediate) diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/CertificateUtilTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/CertificateUtilTest.java index 407a0169f8db..d36054fcdeb1 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/CertificateUtilTest.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/CertificateUtilTest.java @@ -46,7 +46,7 @@ public void loadCertificateChainFromSecretBundleValueTest() throws CertificateEx private void assertCertNumberInCertChain(String pemFile, int expectedNumber) throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException, NoSuchProviderException, PKCSException { String pemString = new String(Files.readAllBytes(Paths.get(pemFile)), StandardCharsets.UTF_8); - assertEquals(expectedNumber, CertificateUtil.loadCertificatesFromSecretBundleValue(pemString).length); + assertEquals(expectedNumber, CertificateUtil.loadCertificatesFromSecretBundleValue(pemString, false).length); } @Test @@ -55,7 +55,7 @@ public void loadUnterminatedCertificatePemFails() throws IOException { String unterminatedPem = pemString.substring(0, pemString.indexOf("-----END CERTIFICATE-----")); CertificateException exception = assertThrows(CertificateException.class, - () -> CertificateUtil.loadCertificatesFromSecretBundleValue(unterminatedPem)); + () -> CertificateUtil.loadCertificatesFromSecretBundleValue(unterminatedPem, false)); assertEquals("Certificate PEM block is not terminated.", exception.getMessage()); } @@ -65,7 +65,7 @@ public void loadNestedCertificatePemFails() throws IOException { String nestedPem = "-----BEGIN CERTIFICATE-----\n" + readCertificatePem(); CertificateException exception = assertThrows(CertificateException.class, - () -> CertificateUtil.loadCertificatesFromSecretBundleValue(nestedPem)); + () -> CertificateUtil.loadCertificatesFromSecretBundleValue(nestedPem, false)); assertEquals("Certificate PEM block contains a nested BEGIN CERTIFICATE marker.", exception.getMessage()); } From e06aeab9493f5cbf892630e40f8eb797ef522987 Mon Sep 17 00:00:00 2001 From: Moary Chen Date: Mon, 17 Aug 2026 16:22:13 +0800 Subject: [PATCH 03/11] Update java doc --- .../keyvault/jca/implementation/KeyVaultClient.java | 2 +- .../implementation/utils/AiaCertificateChainUtil.java | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java index 3bb0b961888b..34f5d896e005 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java @@ -107,7 +107,7 @@ public class KeyVaultClient { private final boolean disableChallengeResourceVerification; /** - * Stores a flag indicating whether AIA certificate downloads are disabled. + * Stores a flag indicating whether Authority Information Access (AIA) certificate downloads are disabled. */ private final boolean disableAiaDownload; diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java index 3c43fffafa55..50fa8004a346 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java @@ -83,7 +83,16 @@ static boolean shouldCompleteChainViaAia(Certificate[] certificates) { } /** - * Completes an incomplete certificate chain when AIA downloads are allowed for the owning Key Vault client. + * Completes an incomplete certificate chain when Authority Information Access (AIA) downloads are allowed for the owning Key Vault client. + * + *

Because completion may issue outbound HTTP requests on a cache miss, callers must restrict it to chains + * whose valid path does not end in a self-signed root (see {@link #shouldCompleteChainViaAia(Certificate[])}). + * + *

The method walks up the contiguous issuer path (leaf → intermediate → root) starting from + * the first certificate, downloading missing intermediates via AIA. Downloaded issuers are inserted + * immediately after the current end of the valid chain (before any unplaced/extra certificates). + * This process repeats until the chain reaches a self-signed root CA, no more AIA URLs are found, or + * the safety download limit is reached. * * @param orderedCertificates Certificate array with a contiguous issuer path and any unplaced certificates. * @param disableAiaDownload Indicates if AIA certificate downloads should be disabled. From e6b00c24d996baefcc2319027d522ef1bcf6f0e3 Mon Sep 17 00:00:00 2001 From: Moary Chen Date: Mon, 17 Aug 2026 16:30:38 +0800 Subject: [PATCH 04/11] Update doc --- 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 0cafc09d8c59..82a7f1745a15 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/README.md +++ b/sdk/keyvault/azure-security-keyvault-jca/README.md @@ -143,7 +143,7 @@ The JCA library supports configuring the following options: * `azure.keyvault.jca.certificates-refresh-interval-in-ms`: The refresh interval time. * `azure.keyvault.jca.certificate-alias-filter-pattern`: A regex that filters which Key Vault certificate aliases are eligible for lazy loading. Append a suffix to the property name to configure more than one filter, for example `azure.keyvault.jca.certificate-alias-filter-pattern.1` or `azure.keyvault.jca.certificate-alias-filter-pattern.prod`. If no such property is configured, all discovered Key Vault aliases are eligible for lazy loading. See "Filtering Key Vault certificate aliases" below. * `azure.keyvault.disable-challenge-resource-verification`: Indicates whether to disable verification that the authentication challenge resource matches the Key Vault or Managed HSM domain. -* `azure.keyvault.jca.disable-aia-download`: Set to `true` to disable automatic AIA (Authority Information Access) certificate chain completion. Chain completion is only attempted when the chain returned by Azure Key Vault is incomplete, meaning it holds a single certificate or is missing an intermediate CA. When disabled, the provider will return certificate chains as provided by Azure Key Vault without downloading missing intermediate CA certificates. Use this in locked-down environments or when processing untrusted certificates to prevent outbound HTTP(S) requests to URLs embedded in certificate extensions. Defaults to `false` for backward compatibility. The value is captured when a Key Vault keystore and its client are initialized; create a new keystore after changing the property. +* `azure.keyvault.jca.disable-aia-download`: Set to `true` to disable automatic AIA (Authority Information Access) certificate chain completion. Chain completion is only attempted when the chain returned by Azure Key Vault is incomplete, meaning it holds a single certificate or is missing an intermediate CA. When disabled, the provider will return certificate chains as provided by Azure Key Vault without downloading missing intermediate CA certificates. Use this in locked-down environments or when processing untrusted certificates to prevent outbound HTTP(S) requests to URLs embedded in certificate extensions. Defaults to `false` for backward compatibility. The value is captured when a Key Vault keystore and its client are initialized. You can configure these properties using: ```java From 936b9b99c8b37968ba83dbae8c3f24a2f2564885 Mon Sep 17 00:00:00 2001 From: Moary Chen Date: Mon, 17 Aug 2026 16:39:11 +0800 Subject: [PATCH 05/11] Revert --- sdk/spring/spring-cloud-azure-autoconfigure/pom.xml | 2 +- sdk/spring/spring-cloud-azure-starter-keyvault-jca/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml b/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml index a2e17502d80e..4aef3f66ff6f 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml +++ b/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml @@ -283,7 +283,7 @@ com.azure azure-security-keyvault-jca - 2.13.0-beta.1 + 2.12.0 true diff --git a/sdk/spring/spring-cloud-azure-starter-keyvault-jca/pom.xml b/sdk/spring/spring-cloud-azure-starter-keyvault-jca/pom.xml index a253d9ced485..a635a7bf2137 100644 --- a/sdk/spring/spring-cloud-azure-starter-keyvault-jca/pom.xml +++ b/sdk/spring/spring-cloud-azure-starter-keyvault-jca/pom.xml @@ -64,7 +64,7 @@ com.azure azure-security-keyvault-jca - 2.13.0-beta.1 + 2.12.0 From eda534f68f8245dff62df4c41ddf9ca7e35061da Mon Sep 17 00:00:00 2001 From: Moary Chen Date: Mon, 17 Aug 2026 16:43:08 +0800 Subject: [PATCH 06/11] Update doc --- .../com/azure/security/keyvault/jca/KeyVaultKeyStore.java | 6 +++--- sdk/spring/CHANGELOG.md | 2 +- 2 files 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 e420ffcb9804..6b3097815394 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 @@ -122,9 +122,9 @@ public final class KeyVaultKeyStore extends KeyStoreSpi { * azure.keyvault.uri, * azure.keyvault.tenantId, * azure.keyvault.clientId, - * azure.keyvault.clientSecret and - * azure.keyvault.managedIdentity, and - * azure.keyvault.jca.disable-aia-download to initialize the + * azure.keyvault.clientSecret, + * azure.keyvault.managedIdentity, and + * azure.keyvault.jca.disable-aia-download to initialize the * Key Vault client. *

*/ diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md index 29d46841abf7..9f866a9d0c23 100644 --- a/sdk/spring/CHANGELOG.md +++ b/sdk/spring/CHANGELOG.md @@ -6,7 +6,7 @@ #### Features Added - Added `spring.ssl.bundle.keyvault..keystore.certificate-alias-filter-patterns` and `spring.ssl.bundle.keyvault..truststore.certificate-alias-filter-patterns` configuration. The patterns are passed to the Key Vault JCA provider to limit which certificate aliases are loaded. ([#50013](https://github.com/Azure/azure-sdk-for-java/issues/50013)) -- Added `spring.ssl.bundle.keyvault..keystore.disable-aia-download` and `spring.ssl.bundle.keyvault..truststore.disable-aia-download` configuration to disable automatic Authority Information Access (AIA) certificate downloads. The default is `false`. ([#47977](https://github.com/Azure/azure-sdk-for-java/pull/47977)) +- Added `spring.ssl.bundle.keyvault..keystore.disable-aia-download` and `spring.ssl.bundle.keyvault..truststore.disable-aia-download` configuration to disable automatic Authority Information Access (AIA) certificate downloads. The default is `false`. ([#50163](https://github.com/Azure/azure-sdk-for-java/pull/50163)) #### Bugs Fixed From a800640cfeafd76378453a03bb8f674a44663805 Mon Sep 17 00:00:00 2001 From: Moary Chen Date: Mon, 17 Aug 2026 17:08:58 +0800 Subject: [PATCH 07/11] Address Copilot review comments --- .../com/azure/security/keyvault/jca/KeyVaultKeyStore.java | 8 ++++---- .../jca/AzureKeyVaultSslBundleRegistrarTests.java | 1 + 2 files changed, 5 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 6b3097815394..89783db0954d 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 @@ -120,10 +120,10 @@ public final class KeyVaultKeyStore extends KeyStoreSpi { *

* The constructor uses System.getProperty for * azure.keyvault.uri, - * azure.keyvault.tenantId, - * azure.keyvault.clientId, - * azure.keyvault.clientSecret, - * azure.keyvault.managedIdentity, and + * azure.keyvault.tenant-id, + * azure.keyvault.client-id, + * azure.keyvault.client-secret, + * azure.keyvault.managed-identity, and * azure.keyvault.jca.disable-aia-download to initialize the * Key Vault client. *

diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrarTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrarTests.java index 25389965d9f4..60e820b75407 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrarTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrarTests.java @@ -43,6 +43,7 @@ class AzureKeyVaultSslBundleRegistrarTests { @AfterEach void cleanupProvider() { Security.removeProvider(KeyVaultJcaProvider.PROVIDER_NAME); + System.clearProperty("azure.keyvault.jca.disable-aia-download"); } @Test From a93919105912b64ac85a5c38f83e12e4a0559d22 Mon Sep 17 00:00:00 2001 From: Moary Chen Date: Mon, 17 Aug 2026 17:16:07 +0800 Subject: [PATCH 08/11] Update doc --- .../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 89783db0954d..888feb5ad507 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 @@ -120,10 +120,10 @@ public final class KeyVaultKeyStore extends KeyStoreSpi { *

* The constructor uses System.getProperty for * azure.keyvault.uri, - * azure.keyvault.tenant-id, - * azure.keyvault.client-id, - * azure.keyvault.client-secret, - * azure.keyvault.managed-identity, and + * azure.keyvault.tenant-id, + * azure.keyvault.client-id, + * azure.keyvault.client-secret, + * azure.keyvault.managed-identity, and * azure.keyvault.jca.disable-aia-download to initialize the * Key Vault client. *

From c584554d38bbd0ea39c077f701d5f1c564d9d461 Mon Sep 17 00:00:00 2001 From: Moary Chen Date: Tue, 18 Aug 2026 10:33:26 +0800 Subject: [PATCH 09/11] Centralize Key Vault JCA property names --- .../azure-security-keyvault-jca/CHANGELOG.md | 1 + .../azure-security-keyvault-jca/README.md | 4 +- .../jca/KeyVaultJcaPropertyNames.java | 88 +++++++++++++++++++ .../keyvault/jca/KeyVaultKeyStore.java | 69 +++++++-------- .../jca/implementation/KeyVaultClient.java | 21 ++--- .../certificates/KeyVaultCertificates.java | 13 +-- .../implementation/utils/AccessTokenUtil.java | 5 +- .../utils/AiaCertificateChainUtil.java | 7 +- .../jca/KeyVaultCertificatesTest.java | 2 +- .../keyvault/jca/KeyVaultKeyStoreTest.java | 6 +- .../jca/KeyVaultKeyStoreUnitTest.java | 16 ++-- .../jca/SpecificPathCertificatesTest.java | 8 +- .../KeyVaultKeylessEcSignatureTest.java | 3 +- ...KeyVaultKeylessRsaSsaPssSignatureTest.java | 3 +- .../utils/AiaCertificateChainTest.java | 9 +- 15 files changed, 176 insertions(+), 79 deletions(-) create mode 100644 sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultJcaPropertyNames.java diff --git a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md index 7684970aa9b7..f5315e0c70dc 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md +++ b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md @@ -13,6 +13,7 @@ - Fixed bug: `jarsigner` reports invalid certificate chain (`PKIX path building failed: unable to find valid certification path to requested target`) when using a non-exportable Azure Key Vault certificate. When the certificate chain returned by Azure Key Vault does not end in a self-signed root, the missing issuer certificates are now resolved at runtime using the CA Issuers URL in the AIA (Authority Information Access) extension of each certificate. Responses are cached by URL so subsequent loads can reuse them without another network request. ([#44267](https://github.com/Azure/azure-sdk-for-java/issues/44267)) ### Other Changes +- Added `KeyVaultJcaPropertyNames` as the central source for the system property names supported by the Azure Key Vault JCA provider. - Added system property `azure.keyvault.jca.disable-aia-download` to disable automatic AIA chain completion. AIA chain completion downloads certificates from URLs embedded in certificate extensions, so this allows locked-down environments to prevent those outbound HTTP(S) requests, mitigating potential SSRF-like attack vectors when loading untrusted certificates. The value is captured when each Key Vault client is initialized and retained for lazy certificate-chain loading, so multiple keystores can use different settings without overwriting one another. Set to `true` to disable (defaults to `false`). ## 2.12.0 (2026-07-24) diff --git a/sdk/keyvault/azure-security-keyvault-jca/README.md b/sdk/keyvault/azure-security-keyvault-jca/README.md index 82a7f1745a15..d99903494c42 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/README.md +++ b/sdk/keyvault/azure-security-keyvault-jca/README.md @@ -145,9 +145,9 @@ The JCA library supports configuring the following options: * `azure.keyvault.disable-challenge-resource-verification`: Indicates whether to disable verification that the authentication challenge resource matches the Key Vault or Managed HSM domain. * `azure.keyvault.jca.disable-aia-download`: Set to `true` to disable automatic AIA (Authority Information Access) certificate chain completion. Chain completion is only attempted when the chain returned by Azure Key Vault is incomplete, meaning it holds a single certificate or is missing an intermediate CA. When disabled, the provider will return certificate chains as provided by Azure Key Vault without downloading missing intermediate CA certificates. Use this in locked-down environments or when processing untrusted certificates to prevent outbound HTTP(S) requests to URLs embedded in certificate extensions. Defaults to `false` for backward compatibility. The value is captured when a Key Vault keystore and its client are initialized. -You can configure these properties using: +The supported system property names are available from `KeyVaultJcaPropertyNames`. You can configure them using: ```java -System.setProperty("azure.keyvault.uri", ""); +System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI, ""); ``` or as a JVM argument: ```shell diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultJcaPropertyNames.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultJcaPropertyNames.java new file mode 100644 index 000000000000..9f1ac477bd71 --- /dev/null +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultJcaPropertyNames.java @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.security.keyvault.jca; + +/** + * System property names supported by the Azure Key Vault JCA provider. + */ +public final class KeyVaultJcaPropertyNames { + + /** + * The Azure Key Vault endpoint property name. + */ + public static final String KEYVAULT_URI = "azure.keyvault.uri"; + + /** + * The Microsoft Entra tenant ID property name. + */ + public static final String KEYVAULT_TENANT_ID = "azure.keyvault.tenant-id"; + + /** + * The client ID property name. + */ + public static final String KEYVAULT_CLIENT_ID = "azure.keyvault.client-id"; + + /** + * The client secret property name. + */ + public static final String KEYVAULT_CLIENT_SECRET = "azure.keyvault.client-secret"; + + /** + * The managed identity property name. + */ + public static final String KEYVAULT_MANAGED_IDENTITY = "azure.keyvault.managed-identity"; + + /** + * The access token property name. + */ + public static final String KEYVAULT_ACCESS_TOKEN = "azure.keyvault.access-token"; + + /** + * The property name used to disable challenge resource verification. + */ + public static final String KEYVAULT_DISABLE_CHALLENGE_RESOURCE_VERIFICATION + = "azure.keyvault.disable-challenge-resource-verification"; + + /** + * The well-known certificate path property name. + */ + public static final String CERT_PATH_WELL_KNOWN = "azure.cert-path.well-known"; + + /** + * The custom certificate path property name. + */ + public static final String CERT_PATH_CUSTOM = "azure.cert-path.custom"; + + /** + * The certificate refresh interval property name. + */ + public static final String KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL + = "azure.keyvault.jca.certificates-refresh-interval"; + + /** + * The certificate refresh interval in milliseconds property name. + */ + public static final String KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL_IN_MS + = "azure.keyvault.jca.certificates-refresh-interval-in-ms"; + + /** + * The property name used to refresh certificates when an untrusted certificate is encountered. + */ + public static final String KEYVAULT_JCA_REFRESH_CERTIFICATES_WHEN_HAVE_UNTRUST_CERTIFICATE + = "azure.keyvault.jca.refresh-certificates-when-have-un-trust-certificate"; + + /** + * The certificate alias filter pattern property name. + */ + public static final String KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN + = "azure.keyvault.jca.certificate-alias-filter-pattern"; + + /** + * The property name used to disable Authority Information Access (AIA) certificate downloads. + */ + public static final String KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD = "azure.keyvault.jca.disable-aia-download"; + + private KeyVaultJcaPropertyNames() { + } +} 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 888feb5ad507..05055d456833 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 @@ -59,10 +59,6 @@ public final class KeyVaultKeyStore extends KeyStoreSpi { */ private static final Logger LOGGER = Logger.getLogger(KeyVaultKeyStore.class.getName()); - static final String CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY - = "azure.keyvault.jca.certificate-alias-filter-pattern"; - static final String DISABLE_AIA_DOWNLOAD_PROPERTY = "azure.keyvault.jca.disable-aia-download"; - /** * Stores the Jre key store certificates. */ @@ -105,26 +101,26 @@ public final class KeyVaultKeyStore extends KeyStoreSpi { /** * Store the path where the well-known certificate is placed */ - final String wellKnowPath - = Optional.ofNullable(System.getProperty("azure.cert-path.well-known")).orElse("/etc/certs/well-known/"); + final String wellKnowPath = Optional.ofNullable(System.getProperty(KeyVaultJcaPropertyNames.CERT_PATH_WELL_KNOWN)) + .orElse("/etc/certs/well-known/"); /** * Store the path where the custom certificate is placed */ - final String customPath - = Optional.ofNullable(System.getProperty("azure.cert-path.custom")).orElse("/etc/certs/custom/"); + final String customPath = Optional.ofNullable(System.getProperty(KeyVaultJcaPropertyNames.CERT_PATH_CUSTOM)) + .orElse("/etc/certs/custom/"); /** * Constructor. * *

* The constructor uses System.getProperty for - * azure.keyvault.uri, - * azure.keyvault.tenant-id, - * azure.keyvault.client-id, - * azure.keyvault.client-secret, - * azure.keyvault.managed-identity, and - * azure.keyvault.jca.disable-aia-download to initialize the + * {@value KeyVaultJcaPropertyNames#KEYVAULT_URI}, + * {@value KeyVaultJcaPropertyNames#KEYVAULT_TENANT_ID}, + * {@value KeyVaultJcaPropertyNames#KEYVAULT_CLIENT_ID}, + * {@value KeyVaultJcaPropertyNames#KEYVAULT_CLIENT_SECRET}, + * {@value KeyVaultJcaPropertyNames#KEYVAULT_MANAGED_IDENTITY}, and + * {@value KeyVaultJcaPropertyNames#KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD} to initialize the * Key Vault client. *

*/ @@ -132,18 +128,19 @@ public KeyVaultKeyStore() { LOGGER.log(FINE, "Constructing KeyVaultKeyStore."); creationDate = new Date(); - String keyVaultUri = System.getProperty("azure.keyvault.uri"); - String tenantId = System.getProperty("azure.keyvault.tenant-id"); - String clientId = System.getProperty("azure.keyvault.client-id"); - String clientSecret = System.getProperty("azure.keyvault.client-secret"); - String managedIdentity = System.getProperty("azure.keyvault.managed-identity"); - String accessToken = System.getProperty("azure.keyvault.access-token"); - boolean disableChallengeResourceVerification - = Boolean.parseBoolean(System.getProperty("azure.keyvault.disable-challenge-resource-verification")); - disableAiaDownload = Boolean.parseBoolean(System.getProperty(DISABLE_AIA_DOWNLOAD_PROPERTY)); + String keyVaultUri = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI); + String tenantId = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_TENANT_ID); + String clientId = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_ID); + String clientSecret = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_SECRET); + String managedIdentity = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_MANAGED_IDENTITY); + String accessToken = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_ACCESS_TOKEN); + boolean disableChallengeResourceVerification = Boolean.parseBoolean( + System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_DISABLE_CHALLENGE_RESOURCE_VERIFICATION)); + disableAiaDownload + = Boolean.parseBoolean(System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD)); long refreshInterval = getRefreshInterval(); refreshCertificatesWhenHaveUnTrustCertificate - = Optional.of("azure.keyvault.jca.refresh-certificates-when-have-un-trust-certificate") + = Optional.of(KeyVaultJcaPropertyNames.KEYVAULT_JCA_REFRESH_CERTIFICATES_WHEN_HAVE_UNTRUST_CERTIFICATE) .map(System::getProperty) .map(Boolean::parseBoolean) .orElse(false); @@ -171,8 +168,8 @@ public KeyVaultKeyStore() { Long getRefreshInterval() { return Stream - .of("azure.keyvault.jca.certificates-refresh-interval-in-ms", - "azure.keyvault.jca.certificates-refresh-interval") + .of(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL_IN_MS, + KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL) .map(System::getProperty) .filter(Objects::nonNull) .map(Long::valueOf) @@ -183,11 +180,11 @@ Long getRefreshInterval() { Set getKeyVaultCertificateAliasFilterPatterns() { // Each pattern gets its own property because any delimiter character can be part of a regex. Properties properties = System.getProperties(); - String suffixedPropertyPrefix = CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY + "."; + String suffixedPropertyPrefix = KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN + "."; return properties.stringPropertyNames() .stream() - .filter(name -> name.equals(CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY) + .filter(name -> name.equals(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN) || name.startsWith(suffixedPropertyPrefix)) .map(properties::getProperty) .filter(Objects::nonNull) @@ -211,13 +208,15 @@ public static KeyStore getKeyVaultKeyStoreBySystemProperty() KeyStore keyStore = KeyStore.getInstance(KeyVaultJcaProvider.PROVIDER_NAME); KeyVaultLoadStoreParameter keyVaultLoadStoreParameter - = new KeyVaultLoadStoreParameter(System.getProperty("azure.keyvault.uri"), - System.getProperty("azure.keyvault.tenant-id"), System.getProperty("azure.keyvault.client-id"), - System.getProperty("azure.keyvault.client-secret"), - System.getProperty("azure.keyvault.managed-identity")) - .setAccessToken(System.getProperty("azure.keyvault.access-token")); - - if (Boolean.parseBoolean(System.getProperty("azure.keyvault.disable-challenge-resource-verification"))) { + = new KeyVaultLoadStoreParameter(System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI), + System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_TENANT_ID), + System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_ID), + System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_SECRET), + System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_MANAGED_IDENTITY)) + .setAccessToken(System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_ACCESS_TOKEN)); + + if (Boolean.parseBoolean( + System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_DISABLE_CHALLENGE_RESOURCE_VERIFICATION))) { keyVaultLoadStoreParameter.disableChallengeResourceVerification(); } diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java index 34f5d896e005..3a7f2e7b9b1a 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java @@ -2,6 +2,7 @@ // Licensed under the MIT License. package com.azure.security.keyvault.jca.implementation; +import com.azure.security.keyvault.jca.KeyVaultJcaPropertyNames; import com.azure.security.keyvault.jca.implementation.model.AccessToken; import com.azure.security.keyvault.jca.implementation.model.CertificateBundle; import com.azure.security.keyvault.jca.implementation.model.CertificateItem; @@ -59,7 +60,6 @@ */ public class KeyVaultClient { private static final Logger LOGGER = Logger.getLogger(KeyVaultClient.class.getName()); - private static final String DISABLE_AIA_DOWNLOAD_PROPERTY = "azure.keyvault.jca.disable-aia-download"; /** * Stores the Key Vault cloud URI. @@ -201,15 +201,16 @@ public KeyVaultClient(String keyVaultUri, String tenantId, String clientId, Stri } public static KeyVaultClient createKeyVaultClientBySystemProperty() { - String keyVaultUri = System.getProperty("azure.keyvault.uri"); - String tenantId = System.getProperty("azure.keyvault.tenant-id"); - String clientId = System.getProperty("azure.keyvault.client-id"); - String clientSecret = System.getProperty("azure.keyvault.client-secret"); - String managedIdentity = System.getProperty("azure.keyvault.managed-identity"); - String accessToken = System.getProperty("azure.keyvault.access-token"); - boolean disableChallengeResourceVerification - = Boolean.parseBoolean(System.getProperty("azure.keyvault.disable-challenge-resource-verification")); - boolean disableAiaDownload = Boolean.parseBoolean(System.getProperty(DISABLE_AIA_DOWNLOAD_PROPERTY)); + String keyVaultUri = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI); + String tenantId = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_TENANT_ID); + String clientId = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_ID); + String clientSecret = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_SECRET); + String managedIdentity = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_MANAGED_IDENTITY); + String accessToken = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_ACCESS_TOKEN); + boolean disableChallengeResourceVerification = Boolean.parseBoolean( + System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_DISABLE_CHALLENGE_RESOURCE_VERIFICATION)); + boolean disableAiaDownload + = Boolean.parseBoolean(System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD)); return new KeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, accessToken, disableChallengeResourceVerification, disableAiaDownload); 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 987df1fa7ee0..4c3d41c436e1 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 @@ -3,6 +3,7 @@ package com.azure.security.keyvault.jca.implementation.certificates; +import com.azure.security.keyvault.jca.KeyVaultJcaPropertyNames; import com.azure.security.keyvault.jca.implementation.CertificateVersion; import com.azure.security.keyvault.jca.implementation.KeyVaultClient; @@ -29,9 +30,6 @@ * Caches certificate material loaded from Azure Key Vault. */ public final class KeyVaultCertificates implements AzureCertificates { - private static final String CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY - = "azure.keyvault.jca.certificate-alias-filter-pattern"; - /** * Stores the list of aliases. */ @@ -189,9 +187,12 @@ private Pattern compileRegexPattern(String regexPattern) { try { return Pattern.compile(regexPattern); } catch (PatternSyntaxException exception) { - throw new IllegalArgumentException("Invalid certificate alias filter regex pattern: " + regexPattern - + ". If configured via system property, check '" + CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY + "' and '" - + CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY + ".'.", exception); + throw new IllegalArgumentException( + "Invalid certificate alias filter regex pattern: " + regexPattern + + ". If configured via system property, check '" + + KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN + "' and '" + + KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN + ".'.", + exception); } } diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AccessTokenUtil.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AccessTokenUtil.java index 271a7b03f22b..d15f15ac0619 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AccessTokenUtil.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AccessTokenUtil.java @@ -2,6 +2,7 @@ // Licensed under the MIT License. package com.azure.security.keyvault.jca.implementation.utils; +import com.azure.security.keyvault.jca.KeyVaultJcaPropertyNames; import com.azure.security.keyvault.jca.implementation.model.AccessToken; import org.apache.hc.core5.http.ClassicHttpResponse; @@ -469,8 +470,8 @@ public static String getLoginUri(String resourceUri, boolean disableChallengeRes } else { if (!disableChallengeResourceVerification && !isChallengeResourceValid(resourceUri, scope)) { throw new IllegalStateException("The challenge resource " + scope + " does not match the requested " - + "domain. If you wish to disable this check, set the environment property " - + "'azure.keyvault.disable-challenge-resource-verification' to 'true'. See " + + "domain. If you wish to disable this check, set the environment property " + "'" + + KeyVaultJcaPropertyNames.KEYVAULT_DISABLE_CHALLENGE_RESOURCE_VERIFICATION + "' to 'true'. See " + "https://aka.ms/azsdk/blog/vault-uri for more information."); } diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java index 50fa8004a346..f97beed113fb 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java @@ -2,6 +2,7 @@ // Licensed under the MIT License. package com.azure.security.keyvault.jca.implementation.utils; +import com.azure.security.keyvault.jca.KeyVaultJcaPropertyNames; import org.bouncycastle.asn1.ASN1OctetString; import org.bouncycastle.asn1.x509.AccessDescription; import org.bouncycastle.asn1.x509.AuthorityInformationAccess; @@ -45,12 +46,12 @@ * a valid PKIX path to a trusted root CA and reports "PKIX path building failed" on verify. * *

Security note: completion issues outbound HTTP(S) requests to URLs embedded in certificates. - * Set the system property {@code azure.keyvault.jca.disable-aia-download=true} to disable it in locked-down + * Set the system property {@value KeyVaultJcaPropertyNames#KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD} to {@code true} to + * disable it in locked-down * environments or when loading untrusted certificates. */ final class AiaCertificateChainUtil { private static final Logger LOGGER = Logger.getLogger(AiaCertificateChainUtil.class.getName()); - static final String DISABLE_AIA_DOWNLOAD_PROPERTY = "azure.keyvault.jca.disable-aia-download"; private static final int AIA_CACHE_MAX_SIZE = 128; private static final long MAX_SUCCESS_TTL_IN_MILLIS = TimeUnit.HOURS.toMillis(24); private static final long NEGATIVE_TTL_IN_MILLIS = TimeUnit.MINUTES.toMillis(1); @@ -105,7 +106,7 @@ static Certificate[] completeChainViaAia(Certificate[] orderedCertificates, bool if (disableAiaDownload) { LOGGER.log(FINE, "AIA chain completion is disabled for this Key Vault client by configuration [{0}]", - DISABLE_AIA_DOWNLOAD_PROPERTY); + KeyVaultJcaPropertyNames.KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD); return orderedCertificates; } diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultCertificatesTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultCertificatesTest.java index f52651e293ff..d2d7fa7c9dce 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultCertificatesTest.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultCertificatesTest.java @@ -83,7 +83,7 @@ private X509Certificate getTestCertificate() { @Test public void testCertificatesRefreshInterval() throws Exception { - System.setProperty("azure.keyvault.jca.certificates-refresh-interval-in-ms", "1000"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL_IN_MS, "1000"); KeyStore keyStore = PropertyConvertorUtils.getKeyVaultKeyStore(); assertNotNull(keyStore.getCertificate(certificateName)); keyStore.deleteEntry(certificateName); diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultKeyStoreTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultKeyStoreTest.java index 1199492586c5..2c3cd1b4961c 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultKeyStoreTest.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultKeyStoreTest.java @@ -88,7 +88,8 @@ public void testEngineSize() { @Test public void testRefreshEngineGetCertificate() throws Exception { - System.setProperty("azure.keyvault.jca.refresh-certificates-when-have-un-trust-certificate", "true"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_REFRESH_CERTIFICATES_WHEN_HAVE_UNTRUST_CERTIFICATE, + "true"); KeyVaultJcaProvider provider = new KeyVaultJcaProvider(); Security.addProvider(provider); KeyStore ks = PropertyConvertorUtils.getKeyVaultKeyStore(); @@ -100,7 +101,8 @@ public void testRefreshEngineGetCertificate() throws Exception { @Test public void testNotRefreshEngineGetCertificate() throws Exception { - System.setProperty("azure.keyvault.jca.refresh-certificates-when-have-un-trust-certificate", "false"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_REFRESH_CERTIFICATES_WHEN_HAVE_UNTRUST_CERTIFICATE, + "false"); KeyVaultJcaProvider provider = new KeyVaultJcaProvider(); Security.addProvider(provider); KeyStore ks = PropertyConvertorUtils.getKeyVaultKeyStore(); 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 656a731afbe1..7e967c0639ff 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 @@ -58,14 +58,14 @@ public void testEngineStore() { @Test public void testGetRefreshInterval() { - System.clearProperty("azure.keyvault.jca.certificates-refresh-interval"); - System.clearProperty("azure.keyvault.jca.certificates-refresh-interval-in-ms"); + System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL); + System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL_IN_MS); KeyVaultKeyStore keystore = new KeyVaultKeyStore(); assertEquals(keystore.getRefreshInterval(), 0); - System.setProperty("azure.keyvault.jca.certificates-refresh-interval", "2000"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL, "2000"); keystore = new KeyVaultKeyStore(); assertEquals(keystore.getRefreshInterval(), 2000); - System.setProperty("azure.keyvault.jca.certificates-refresh-interval-in-ms", "1000"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL_IN_MS, "1000"); assertEquals(keystore.getRefreshInterval(), 1000); } @@ -108,7 +108,7 @@ public void testGetKeyVaultCertificateAliasFilterPatternsWhenNotConfigured() { @Test public void testGetKeyVaultCertificateAliasFilterPatternsFromBaseProperty() { - System.setProperty(KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY, " ^prod-.* "); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN, " ^prod-.* "); assertEquals(Collections.singleton("^prod-.*"), new KeyVaultKeyStore().getKeyVaultCertificateAliasFilterPatterns()); @@ -116,7 +116,7 @@ public void testGetKeyVaultCertificateAliasFilterPatternsFromBaseProperty() { @Test public void testGetKeyVaultCertificateAliasFilterPatternsFromSuffixedProperties() { - String base = KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY; + String base = KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN; System.setProperty(base, "myalias"); System.setProperty(base + ".1", "^prod-.*"); System.setProperty(base + ".prod", "^prod-a.*"); @@ -132,7 +132,7 @@ public void testGetKeyVaultCertificateAliasFilterPatternsFromSuffixedProperties( @Test public void testGetKeyVaultCertificateAliasFilterPatternsKeepsCommas() { - String base = KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY; + String base = KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN; System.setProperty(base + ".1", "^cert-\\d{1,5}$"); System.setProperty(base + ".2", "![a-z]{2,}"); @@ -144,7 +144,7 @@ public void testGetKeyVaultCertificateAliasFilterPatternsKeepsCommas() { @BeforeEach @AfterEach public void clearCertificateAliasFilterPatternProperties() { - String base = KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY; + String base = KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN; System.clearProperty(base); System.getProperties() .stringPropertyNames() diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/SpecificPathCertificatesTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/SpecificPathCertificatesTest.java index 8932a202a584..9519a3ce9c6e 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/SpecificPathCertificatesTest.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/SpecificPathCertificatesTest.java @@ -41,15 +41,15 @@ public static String getFilePath(String packageName) { @Test public void testGetSpecificPathCertificate() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { - System.setProperty("azure.cert-path.custom", getFilePath("custom")); + System.setProperty(KeyVaultJcaPropertyNames.CERT_PATH_CUSTOM, getFilePath("custom")); KeyStore keyStore = PropertyConvertorUtils.getKeyVaultKeyStore(); Assertions.assertNotNull(keyStore.getCertificate("sideload")); } @Test public void testCertificatePriority1() { - System.setProperty("azure.cert-path.well-known", getFilePath("well-known\\")); - System.setProperty("azure.cert-path.custom", getFilePath("custom\\")); + System.setProperty(KeyVaultJcaPropertyNames.CERT_PATH_WELL_KNOWN, getFilePath("well-known\\")); + System.setProperty(KeyVaultJcaPropertyNames.CERT_PATH_CUSTOM, getFilePath("custom\\")); KeyVaultKeyStore ks = new KeyVaultKeyStore(); ks.engineLoad(null); X509Certificate customCertificate = getCertificateByFile(new File(getFilePath("custom\\sideload.x509"))); @@ -60,7 +60,7 @@ public void testCertificatePriority1() { @Test public void testCertificatePriority2() { - System.setProperty("azure.cert-path.custom", getFilePath("custom\\")); + System.setProperty(KeyVaultJcaPropertyNames.CERT_PATH_CUSTOM, getFilePath("custom\\")); KeyVaultKeyStore ks = new KeyVaultKeyStore(); ks.engineLoad(null); X509Certificate specificPathCertificate = getCertificateByFile(new File(getFilePath("custom\\sideload2.pem"))); diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/signature/KeyVaultKeylessEcSignatureTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/signature/KeyVaultKeylessEcSignatureTest.java index 2dbc7d4482a8..684e9f72891c 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/signature/KeyVaultKeylessEcSignatureTest.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/signature/KeyVaultKeylessEcSignatureTest.java @@ -4,6 +4,7 @@ package com.azure.security.keyvault.jca.implementation.signature; import com.azure.security.keyvault.jca.KeyVaultEncode; +import com.azure.security.keyvault.jca.KeyVaultJcaPropertyNames; import com.azure.security.keyvault.jca.implementation.KeyVaultPrivateKey; import com.azure.security.keyvault.jca.implementation.KeyVaultClient; import org.junit.jupiter.api.Assertions; @@ -34,7 +35,7 @@ public class KeyVaultKeylessEcSignatureTest { @BeforeEach public void before() { - System.setProperty("azure.keyvault.uri", KEY_VAULT_TEST_URI_GLOBAL); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI, KEY_VAULT_TEST_URI_GLOBAL); keyVaultKeylessEcSignature = new KeyVaultKeylessEcSha256Signature(); } diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/signature/KeyVaultKeylessRsaSsaPssSignatureTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/signature/KeyVaultKeylessRsaSsaPssSignatureTest.java index bda7ae92447b..7502bfd49590 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/signature/KeyVaultKeylessRsaSsaPssSignatureTest.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/signature/KeyVaultKeylessRsaSsaPssSignatureTest.java @@ -3,6 +3,7 @@ package com.azure.security.keyvault.jca.implementation.signature; +import com.azure.security.keyvault.jca.KeyVaultJcaPropertyNames; import com.azure.security.keyvault.jca.implementation.KeyVaultPrivateKey; import com.azure.security.keyvault.jca.implementation.KeyVaultClient; import org.junit.jupiter.api.BeforeEach; @@ -34,7 +35,7 @@ public class KeyVaultKeylessRsaSsaPssSignatureTest { @BeforeEach public void before() { - System.setProperty("azure.keyvault.uri", KEY_VAULT_TEST_URI_GLOBAL); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI, KEY_VAULT_TEST_URI_GLOBAL); keyVaultKeylessRsaSsaPssSignature = new KeyVaultKeylessRsaSsaPssSignature(); } diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainTest.java index 5346486431d1..486d91d33d46 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainTest.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainTest.java @@ -3,6 +3,7 @@ package com.azure.security.keyvault.jca.implementation.utils; +import com.azure.security.keyvault.jca.KeyVaultJcaPropertyNames; import com.azure.security.keyvault.jca.implementation.CertificateVersion; import com.azure.security.keyvault.jca.implementation.KeyVaultClient; import com.azure.security.keyvault.jca.implementation.model.SecretBundle; @@ -117,14 +118,14 @@ static void generateTestChain() throws Exception { @BeforeEach void setupClean() { // Ensure each test starts with a clean state - clear the disable property - System.clearProperty(AiaCertificateChainUtil.DISABLE_AIA_DOWNLOAD_PROPERTY); + System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD); AiaCertificateChainUtil.clearAiaCache(); } @AfterEach void cleanup() { // Clear the property after each test to prevent interference with subsequent tests - System.clearProperty(AiaCertificateChainUtil.DISABLE_AIA_DOWNLOAD_PROPERTY); + System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD); AiaCertificateChainUtil.clearAiaCache(); } @@ -441,7 +442,7 @@ void keyVaultClientKeepsAiaDownloadSettingFromConstruction() throws Exception { = new KeyVaultClient("https://fake.vault.azure.net/", null, null, null, null, "test-token", false, true); // Simulate another SSL bundle replacing the JVM-global value before this client lazily loads its chain. - System.setProperty(AiaCertificateChainUtil.DISABLE_AIA_DOWNLOAD_PROPERTY, "false"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD, "false"); try (MockedStatic httpMock = Mockito.mockStatic(HttpUtil.class)) { httpMock @@ -473,7 +474,7 @@ void keyVaultClientKeepsAiaDownloadEnabledFromConstruction() throws Exception { = new KeyVaultClient("https://fake.vault.azure.net/", null, null, null, null, "test-token", false, false); // Simulate another SSL bundle replacing the JVM-global value before this client lazily loads its chain. - System.setProperty(AiaCertificateChainUtil.DISABLE_AIA_DOWNLOAD_PROPERTY, "true"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD, "true"); try (MockedStatic httpMock = Mockito.mockStatic(HttpUtil.class)) { httpMock From 51e61b3745b2ecff2c2257b738fbd96407260fe4 Mon Sep 17 00:00:00 2001 From: Moary Chen Date: Tue, 18 Aug 2026 10:45:19 +0800 Subject: [PATCH 10/11] Edit --- sdk/keyvault/azure-security-keyvault-jca/README.md | 2 +- .../security/keyvault/jca/KeyVaultJcaPropertyNames.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-jca/README.md b/sdk/keyvault/azure-security-keyvault-jca/README.md index d99903494c42..c65953a45538 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/README.md +++ b/sdk/keyvault/azure-security-keyvault-jca/README.md @@ -147,7 +147,7 @@ The JCA library supports configuring the following options: The supported system property names are available from `KeyVaultJcaPropertyNames`. You can configure them using: ```java -System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI, ""); +System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI, ""); ``` or as a JVM argument: ```shell diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultJcaPropertyNames.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultJcaPropertyNames.java index 9f1ac477bd71..73f0db5f5c21 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultJcaPropertyNames.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultJcaPropertyNames.java @@ -8,6 +8,9 @@ */ public final class KeyVaultJcaPropertyNames { + private KeyVaultJcaPropertyNames() { + } + /** * The Azure Key Vault endpoint property name. */ @@ -83,6 +86,4 @@ public final class KeyVaultJcaPropertyNames { */ public static final String KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD = "azure.keyvault.jca.disable-aia-download"; - private KeyVaultJcaPropertyNames() { - } } From 3a681fa550533ccf16fe553f194d93965ce4e6b4 Mon Sep 17 00:00:00 2001 From: Moary Chen Date: Tue, 18 Aug 2026 17:36:01 +0800 Subject: [PATCH 11/11] Fix Key Vault JCA configuration initialization --- .../azure-security-keyvault-jca/CHANGELOG.md | 3 +- .../azure-security-keyvault-jca/README.md | 27 ++ .../keyvault/jca/KeyVaultKeyStore.java | 126 +++------- .../jca/KeyVaultLoadStoreParameter.java | 231 +++++++++++++++++- .../jca/implementation/KeyVaultClient.java | 27 +- .../certificates/KeyVaultCertificates.java | 86 ++----- .../jca/KeyVaultKeyStoreUnitTest.java | 100 ++++---- .../jca/KeyVaultLoadStoreParameterTest.java | 141 +++++++++++ .../KeyVaultCertificatesTest.java | 84 +++++-- 9 files changed, 579 insertions(+), 246 deletions(-) create mode 100644 sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultLoadStoreParameterTest.java diff --git a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md index f5315e0c70dc..366ac061f8d7 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md +++ b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md @@ -11,10 +11,11 @@ ### Bugs Fixed - Stopped recording sensitive data in `FINER` level logs. Review any logs captured at the `FINER` level or lower in previous library versions and rotate any sensitive data contained there. - Fixed bug: `jarsigner` reports invalid certificate chain (`PKIX path building failed: unable to find valid certification path to requested target`) when using a non-exportable Azure Key Vault certificate. When the certificate chain returned by Azure Key Vault does not end in a self-signed root, the missing issuer certificates are now resolved at runtime using the CA Issuers URL in the AIA (Authority Information Access) extension of each certificate. Responses are cached by URL so subsequent loads can reuse them without another network request. ([#44267](https://github.com/Azure/azure-sdk-for-java/issues/44267)) +- Fixed an issue where `KeyStore.load(KeyVaultLoadStoreParameter)` could combine explicit client settings with certificate cache and path settings captured earlier from system properties. The parameter now carries the complete key store configuration. ([#50163](https://github.com/Azure/azure-sdk-for-java/pull/50163)) ### Other Changes -- Added `KeyVaultJcaPropertyNames` as the central source for the system property names supported by the Azure Key Vault JCA provider. - Added system property `azure.keyvault.jca.disable-aia-download` to disable automatic AIA chain completion. AIA chain completion downloads certificates from URLs embedded in certificate extensions, so this allows locked-down environments to prevent those outbound HTTP(S) requests, mitigating potential SSRF-like attack vectors when loading untrusted certificates. The value is captured when each Key Vault client is initialized and retained for lazy certificate-chain loading, so multiple keystores can use different settings without overwriting one another. Set to `true` to disable (defaults to `false`). +- Added `KeyVaultJcaPropertyNames` as the central source for the system property names supported by the Azure Key Vault JCA provider. ([#50163](https://github.com/Azure/azure-sdk-for-java/pull/50163)) ## 2.12.0 (2026-07-24) diff --git a/sdk/keyvault/azure-security-keyvault-jca/README.md b/sdk/keyvault/azure-security-keyvault-jca/README.md index c65953a45538..8109aa6a452f 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/README.md +++ b/sdk/keyvault/azure-security-keyvault-jca/README.md @@ -154,6 +154,33 @@ or as a JVM argument: -Dazure.keyvault.uri= ``` +#### Programmatic configuration + +Use `KeyVaultLoadStoreParameter` when each key store needs an explicit configuration instead of global system +properties: + +```java +KeyVaultLoadStoreParameter parameter = new KeyVaultLoadStoreParameter( + "", + "", + "", + "") + .setCertificatesRefreshIntervalInMs(60_000) + .setCertificateAliasFilterPatterns(Collections.singleton("^prod-.*")); +parameter.disableAiaDownload(); + +Security.addProvider(new KeyVaultJcaProvider()); +KeyStore keyStore = KeyStore.getInstance( + KeyVaultKeyStore.KEY_STORE_TYPE, + KeyVaultJcaProvider.PROVIDER_NAME); +keyStore.load(parameter); +``` + +When `load(parameter)` is used, the values and defaults in that parameter replace the complete configuration captured +from system properties. This prevents separate key stores from overwriting one another's configuration. Use +`KeyVaultLoadStoreParameter.fromSystemProperties()` when a programmatic caller needs the same system-property +snapshot used by the default key store initialization. + #### Filtering Key Vault certificate aliases Each filter is configured as its own property, so no delimiter is required and a pattern may contain any character. Filters use Java-based regex: 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 05055d456833..c375f26d44cb 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 @@ -27,13 +27,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Properties; -import java.util.Set; import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.Stream; import static java.util.logging.Level.FINE; import static java.util.logging.Level.WARNING; @@ -67,12 +61,12 @@ public final class KeyVaultKeyStore extends KeyStoreSpi { /** * Store well Know certificates loaded from specific path. */ - private final SpecificPathCertificates wellKnowCertificates; + private SpecificPathCertificates wellKnowCertificates; /** * Store custom certificates loaded from specific path. */ - private final SpecificPathCertificates customCertificates; + private SpecificPathCertificates customCertificates; /** * Store certificates loaded from KeyVault. @@ -87,116 +81,67 @@ public final class KeyVaultKeyStore extends KeyStoreSpi { /** * Stores all the certificates. */ - private final List allCertificates; + private List allCertificates; /** * Stores the creation date. */ private final Date creationDate; - private final boolean refreshCertificatesWhenHaveUnTrustCertificate; - - private final boolean disableAiaDownload; + boolean refreshCertificatesWhenHaveUnTrustCertificate; /** * Store the path where the well-known certificate is placed */ - final String wellKnowPath = Optional.ofNullable(System.getProperty(KeyVaultJcaPropertyNames.CERT_PATH_WELL_KNOWN)) - .orElse("/etc/certs/well-known/"); + String certPathWellKnown; /** * Store the path where the custom certificate is placed */ - final String customPath = Optional.ofNullable(System.getProperty(KeyVaultJcaPropertyNames.CERT_PATH_CUSTOM)) - .orElse("/etc/certs/custom/"); + String certPathCustom; /** * Constructor. * - *

- * The constructor uses System.getProperty for - * {@value KeyVaultJcaPropertyNames#KEYVAULT_URI}, - * {@value KeyVaultJcaPropertyNames#KEYVAULT_TENANT_ID}, - * {@value KeyVaultJcaPropertyNames#KEYVAULT_CLIENT_ID}, - * {@value KeyVaultJcaPropertyNames#KEYVAULT_CLIENT_SECRET}, - * {@value KeyVaultJcaPropertyNames#KEYVAULT_MANAGED_IDENTITY}, and - * {@value KeyVaultJcaPropertyNames#KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD} to initialize the - * Key Vault client. - *

+ *

The constructor uses {@link KeyVaultLoadStoreParameter#fromSystemProperties()} to capture the supported + * system properties and initialize all certificate sources from one configuration snapshot.

*/ public KeyVaultKeyStore() { LOGGER.log(FINE, "Constructing KeyVaultKeyStore."); creationDate = new Date(); - String keyVaultUri = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI); - String tenantId = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_TENANT_ID); - String clientId = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_ID); - String clientSecret = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_SECRET); - String managedIdentity = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_MANAGED_IDENTITY); - String accessToken = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_ACCESS_TOKEN); - boolean disableChallengeResourceVerification = Boolean.parseBoolean( - System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_DISABLE_CHALLENGE_RESOURCE_VERIFICATION)); - disableAiaDownload - = Boolean.parseBoolean(System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD)); - long refreshInterval = getRefreshInterval(); - refreshCertificatesWhenHaveUnTrustCertificate - = Optional.of(KeyVaultJcaPropertyNames.KEYVAULT_JCA_REFRESH_CERTIFICATES_WHEN_HAVE_UNTRUST_CERTIFICATE) - .map(System::getProperty) - .map(Boolean::parseBoolean) - .orElse(false); - + KeyVaultLoadStoreParameter parameter = KeyVaultLoadStoreParameter.fromSystemProperties(); jreCertificates = JreCertificates.getInstance(); LOGGER.log(FINE, String.format("Loaded jre certificates: %s.", jreCertificates.getAliases())); - wellKnowCertificates = SpecificPathCertificates.getSpecificPathCertificates(wellKnowPath); - LOGGER.log(FINE, String.format("Loaded well known certificates: %s.", wellKnowCertificates.getAliases())); - - 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, disableAiaDownload, - getKeyVaultCertificateAliasFilterPatterns()); - LOGGER.log(FINE, () -> String.format("Loaded Key Vault certificates: %s.", keyVaultCertificates.getAliases())); + keyVaultCertificates = new KeyVaultCertificates(parameter); + LOGGER.log(FINE, "Configured Key Vault certificate source."); classpathCertificates = new ClasspathCertificates(); LOGGER.log(FINE, String.format("Loaded classpath certificates: %s.", classpathCertificates.getAliases())); - allCertificates = Arrays.asList(jreCertificates, wellKnowCertificates, customCertificates, keyVaultCertificates, - classpathCertificates); + updateKeyStoreConfiguration(parameter); } - Long getRefreshInterval() { - return Stream - .of(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL_IN_MS, - KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL) - .map(System::getProperty) - .filter(Objects::nonNull) - .map(Long::valueOf) - .findFirst() - .orElse(0L); - } + private void updateKeyStoreConfiguration(KeyVaultLoadStoreParameter parameter) { + refreshCertificatesWhenHaveUnTrustCertificate = parameter.isRefreshCertificatesWhenHaveUnTrustCertificate(); + certPathWellKnown = parameter.getCertPathWellKnown(); + certPathCustom = parameter.getCertPathCustom(); + + wellKnowCertificates = SpecificPathCertificates.getSpecificPathCertificates(certPathWellKnown); + LOGGER.log(FINE, String.format("Loaded well known certificates: %s.", wellKnowCertificates.getAliases())); + + customCertificates = SpecificPathCertificates.getSpecificPathCertificates(certPathCustom); + LOGGER.log(FINE, String.format("Loaded custom certificates: %s.", customCertificates.getAliases())); - Set getKeyVaultCertificateAliasFilterPatterns() { - // Each pattern gets its own property because any delimiter character can be part of a regex. - Properties properties = System.getProperties(); - String suffixedPropertyPrefix = KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN + "."; - - return properties.stringPropertyNames() - .stream() - .filter(name -> name.equals(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN) - || name.startsWith(suffixedPropertyPrefix)) - .map(properties::getProperty) - .filter(Objects::nonNull) - .map(String::trim) - .filter(pattern -> !pattern.isEmpty()) - .collect(Collectors.toSet()); + allCertificates = Arrays.asList(jreCertificates, wellKnowCertificates, customCertificates, keyVaultCertificates, + classpathCertificates); } /** - * get key vault key store by system property + * Gets a Key Vault key store configured from a snapshot of the supported system properties. * - * @return KeyVault key store + * @return The Key Vault key store. * @throws CertificateException if any of the certificates in the * keystore could not be loaded * @throws NoSuchAlgorithmException when algorithm is unavailable. @@ -206,19 +151,8 @@ Set getKeyVaultCertificateAliasFilterPatterns() { public static KeyStore getKeyVaultKeyStoreBySystemProperty() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { + KeyVaultLoadStoreParameter keyVaultLoadStoreParameter = KeyVaultLoadStoreParameter.fromSystemProperties(); KeyStore keyStore = KeyStore.getInstance(KeyVaultJcaProvider.PROVIDER_NAME); - KeyVaultLoadStoreParameter keyVaultLoadStoreParameter - = new KeyVaultLoadStoreParameter(System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI), - System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_TENANT_ID), - System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_ID), - System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_SECRET), - System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_MANAGED_IDENTITY)) - .setAccessToken(System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_ACCESS_TOKEN)); - - if (Boolean.parseBoolean( - System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_DISABLE_CHALLENGE_RESOURCE_VERIFICATION))) { - keyVaultLoadStoreParameter.disableChallengeResourceVerification(); - } keyStore.load(keyVaultLoadStoreParameter); @@ -438,6 +372,7 @@ public boolean engineIsKeyEntry(String alias) { /** * Loads the keystore using the given {@code KeyStore.LoadStoreParameter}. + * A {@link KeyVaultLoadStoreParameter} replaces the complete configuration captured by the constructor. * * @param param the {@code KeyStore.LoadStoreParameter} * that specifies how to load the keystore, @@ -448,9 +383,8 @@ public void engineLoad(KeyStore.LoadStoreParameter param) { if (param instanceof KeyVaultLoadStoreParameter) { KeyVaultLoadStoreParameter parameter = (KeyVaultLoadStoreParameter) param; - keyVaultCertificates.updateKeyVaultClient(parameter.getUri(), parameter.getTenantId(), - parameter.getClientId(), parameter.getClientSecret(), parameter.getManagedIdentity(), - parameter.getAccessToken(), parameter.isChallengeResourceVerificationDisabled(), disableAiaDownload); + keyVaultCertificates.updateKeyVaultClient(parameter); + updateKeyStoreConfiguration(parameter); } classpathCertificates.loadCertificatesFromClasspath(); diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultLoadStoreParameter.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultLoadStoreParameter.java index 78bd35e4ac51..ae43595544a6 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultLoadStoreParameter.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultLoadStoreParameter.java @@ -4,13 +4,26 @@ package com.azure.security.keyvault.jca; import java.security.KeyStore; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; /** - * The Azure Key Vault LoadStoreParameter of the KeyStoreSpi. + * The Azure Key Vault configuration used to load the KeyStoreSpi. + * + *

Create an instance explicitly for programmatic configuration, or use {@link #fromSystemProperties()} to capture + * the supported system properties in a configuration snapshot.

* * @see KeyStore.LoadStoreParameter */ public final class KeyVaultLoadStoreParameter implements KeyStore.LoadStoreParameter { + private static final String DEFAULT_CERT_PATH_WELL_KNOWN = "/etc/certs/well-known/"; + + private static final String DEFAULT_CERT_PATH_CUSTOM = "/etc/certs/custom/"; + /** * Stores the Key Vault URI. */ @@ -46,6 +59,94 @@ public final class KeyVaultLoadStoreParameter implements KeyStore.LoadStoreParam */ private boolean disableChallengeResourceVerification = false; + /** + * Stores the certificate refresh interval in milliseconds. + */ + private long certificatesRefreshIntervalInMs; + + /** + * Stores a flag indicating whether AIA certificate downloads are disabled. + */ + private boolean disableAiaDownload; + + /** + * Stores the well-known certificate path. + */ + private String certPathWellKnown = DEFAULT_CERT_PATH_WELL_KNOWN; + + /** + * Stores the custom certificate path. + */ + private String certPathCustom = DEFAULT_CERT_PATH_CUSTOM; + + /** + * Stores whether certificates are refreshed when an untrusted certificate is encountered. + */ + private boolean refreshCertificatesWhenHaveUnTrustCertificate; + + /** + * Stores certificate alias filter patterns. + */ + private Set certificateAliasFilterPatterns = Collections.emptySet(); + + /** + * Creates a load-store parameter that captures all supported system properties and their defaults. + * + * @return The load-store parameter snapshot. + */ + public static KeyVaultLoadStoreParameter fromSystemProperties() { + KeyVaultLoadStoreParameter parameter + = new KeyVaultLoadStoreParameter(System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI), + System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_TENANT_ID), + System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_ID), + System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_SECRET), + System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_MANAGED_IDENTITY)) + .setAccessToken(System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_ACCESS_TOKEN)); + + if (Boolean.parseBoolean( + System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_DISABLE_CHALLENGE_RESOURCE_VERIFICATION))) { + parameter.disableChallengeResourceVerification(); + } + + String refreshInterval + = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL_IN_MS); + if (refreshInterval == null) { + refreshInterval = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL); + } + if (refreshInterval != null) { + parameter.setCertificatesRefreshIntervalInMs(Long.parseLong(refreshInterval)); + } + + if (Boolean.parseBoolean(System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD))) { + parameter.disableAiaDownload(); + } + + parameter.setCertPathWellKnown( + System.getProperty(KeyVaultJcaPropertyNames.CERT_PATH_WELL_KNOWN, DEFAULT_CERT_PATH_WELL_KNOWN)); + parameter + .setCertPathCustom(System.getProperty(KeyVaultJcaPropertyNames.CERT_PATH_CUSTOM, DEFAULT_CERT_PATH_CUSTOM)); + parameter.setRefreshCertificatesWhenHaveUnTrustCertificate(Boolean.parseBoolean(System + .getProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_REFRESH_CERTIFICATES_WHEN_HAVE_UNTRUST_CERTIFICATE))); + parameter.setCertificateAliasFilterPatterns(getCertificateAliasFilterPatternsFromSystemProperties()); + + return parameter; + } + + private static Set getCertificateAliasFilterPatternsFromSystemProperties() { + Properties properties = System.getProperties(); + String propertyName = KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN; + String suffixedPropertyPrefix = propertyName + "."; + + return properties.stringPropertyNames() + .stream() + .filter(name -> name.equals(propertyName) || name.startsWith(suffixedPropertyPrefix)) + .map(properties::getProperty) + .filter(Objects::nonNull) + .map(String::trim) + .filter(pattern -> !pattern.isEmpty()) + .collect(Collectors.toSet()); + } + /** * Constructor. * @@ -181,6 +282,15 @@ boolean isChallengeResourceVerificationDisabled() { return disableChallengeResourceVerification; } + /** + * Gets whether challenge resource verification is disabled. + * + * @return {@code true} if challenge resource verification is disabled; otherwise, {@code false}. + */ + public boolean isDisableChallengeResourceVerification() { + return disableChallengeResourceVerification; + } + /** * Disables verifying if the authentication challenge resource matches the Key Vault or Managed HSM domain. This * verification is performed by default. @@ -188,4 +298,123 @@ boolean isChallengeResourceVerificationDisabled() { public void disableChallengeResourceVerification() { disableChallengeResourceVerification = true; } + + /** + * Gets the certificate refresh interval in milliseconds. + * + * @return The certificate refresh interval in milliseconds. + */ + public long getCertificatesRefreshIntervalInMs() { + return certificatesRefreshIntervalInMs; + } + + /** + * Sets the certificate refresh interval in milliseconds. + * + * @param certificatesRefreshIntervalInMs The certificate refresh interval in milliseconds. + * @return The KeyVaultLoadStoreParameter. + */ + public KeyVaultLoadStoreParameter setCertificatesRefreshIntervalInMs(long certificatesRefreshIntervalInMs) { + this.certificatesRefreshIntervalInMs = certificatesRefreshIntervalInMs; + return this; + } + + /** + * Gets whether AIA certificate downloads are disabled. + * + * @return {@code true} if AIA certificate downloads are disabled; otherwise, {@code false}. + */ + public boolean isAiaDownloadDisabled() { + return disableAiaDownload; + } + + /** + * Disables AIA certificate downloads. + */ + public void disableAiaDownload() { + disableAiaDownload = true; + } + + /** + * Gets the well-known certificate path. + * + * @return The well-known certificate path. + */ + public String getCertPathWellKnown() { + return certPathWellKnown; + } + + /** + * Sets the well-known certificate path. + * + * @param certPathWellKnown The well-known certificate path. + * @return The KeyVaultLoadStoreParameter. + */ + public KeyVaultLoadStoreParameter setCertPathWellKnown(String certPathWellKnown) { + this.certPathWellKnown = certPathWellKnown; + return this; + } + + /** + * Gets the custom certificate path. + * + * @return The custom certificate path. + */ + public String getCertPathCustom() { + return certPathCustom; + } + + /** + * Sets the custom certificate path. + * + * @param certPathCustom The custom certificate path. + * @return The KeyVaultLoadStoreParameter. + */ + public KeyVaultLoadStoreParameter setCertPathCustom(String certPathCustom) { + this.certPathCustom = certPathCustom; + return this; + } + + /** + * Gets whether certificates are refreshed when an untrusted certificate is encountered. + * + * @return {@code true} if certificates are refreshed; otherwise, {@code false}. + */ + public boolean isRefreshCertificatesWhenHaveUnTrustCertificate() { + return refreshCertificatesWhenHaveUnTrustCertificate; + } + + /** + * Sets whether certificates are refreshed when an untrusted certificate is encountered. + * + * @param refreshCertificatesWhenHaveUnTrustCertificate Whether certificates are refreshed. + * @return The KeyVaultLoadStoreParameter. + */ + public KeyVaultLoadStoreParameter + setRefreshCertificatesWhenHaveUnTrustCertificate(boolean refreshCertificatesWhenHaveUnTrustCertificate) { + this.refreshCertificatesWhenHaveUnTrustCertificate = refreshCertificatesWhenHaveUnTrustCertificate; + return this; + } + + /** + * Gets the certificate alias filter patterns. + * + * @return A copy of the certificate alias filter patterns. + */ + public Set getCertificateAliasFilterPatterns() { + return new HashSet<>(certificateAliasFilterPatterns); + } + + /** + * Sets the certificate alias filter patterns. + * + * @param certificateAliasFilterPatterns The certificate alias filter patterns. + * @return The KeyVaultLoadStoreParameter. + */ + public KeyVaultLoadStoreParameter setCertificateAliasFilterPatterns(Set certificateAliasFilterPatterns) { + this.certificateAliasFilterPatterns = certificateAliasFilterPatterns == null + ? Collections.emptySet() + : new HashSet<>(certificateAliasFilterPatterns); + return this; + } } diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java index 3a7f2e7b9b1a..ee5315deac1b 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java @@ -2,7 +2,7 @@ // Licensed under the MIT License. package com.azure.security.keyvault.jca.implementation; -import com.azure.security.keyvault.jca.KeyVaultJcaPropertyNames; +import com.azure.security.keyvault.jca.KeyVaultLoadStoreParameter; import com.azure.security.keyvault.jca.implementation.model.AccessToken; import com.azure.security.keyvault.jca.implementation.model.CertificateBundle; import com.azure.security.keyvault.jca.implementation.model.CertificateItem; @@ -111,6 +111,17 @@ public class KeyVaultClient { */ private final boolean disableAiaDownload; + /** + * Creates a client using the specified Key Vault load-store configuration. + * + * @param parameter The Key Vault load-store configuration. + */ + public KeyVaultClient(KeyVaultLoadStoreParameter parameter) { + this(parameter.getUri(), parameter.getTenantId(), parameter.getClientId(), parameter.getClientSecret(), + parameter.getManagedIdentity(), parameter.getAccessToken(), + parameter.isDisableChallengeResourceVerification(), parameter.isAiaDownloadDisabled()); + } + /** * Constructor for authentication with user-assigned managed identity. * @@ -201,19 +212,7 @@ public KeyVaultClient(String keyVaultUri, String tenantId, String clientId, Stri } public static KeyVaultClient createKeyVaultClientBySystemProperty() { - String keyVaultUri = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI); - String tenantId = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_TENANT_ID); - String clientId = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_ID); - String clientSecret = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_SECRET); - String managedIdentity = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_MANAGED_IDENTITY); - String accessToken = System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_ACCESS_TOKEN); - boolean disableChallengeResourceVerification = Boolean.parseBoolean( - System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_DISABLE_CHALLENGE_RESOURCE_VERIFICATION)); - boolean disableAiaDownload - = Boolean.parseBoolean(System.getProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD)); - - return new KeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, accessToken, - disableChallengeResourceVerification, disableAiaDownload); + return new KeyVaultClient(KeyVaultLoadStoreParameter.fromSystemProperties()); } /** 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 4c3d41c436e1..813261322fba 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 @@ -4,6 +4,7 @@ package com.azure.security.keyvault.jca.implementation.certificates; import com.azure.security.keyvault.jca.KeyVaultJcaPropertyNames; +import com.azure.security.keyvault.jca.KeyVaultLoadStoreParameter; import com.azure.security.keyvault.jca.implementation.CertificateVersion; import com.azure.security.keyvault.jca.implementation.KeyVaultClient; @@ -102,56 +103,30 @@ public final class KeyVaultCertificates implements AzureCertificates { private KeyVaultClient keyVaultClient; - private final long refreshInterval; + private long certificatesRefreshIntervalInMs; - private final List includeAliasPatterns; + private List includeAliasPatterns; - private final List excludeAliasPatterns; - - public KeyVaultCertificates(long refreshInterval, String keyVaultUri, String tenantId, String clientId, - String clientSecret, String managedIdentity, String accessToken, boolean disableChallengeResourceVerification) { - this(refreshInterval, keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, accessToken, - disableChallengeResourceVerification, false, Collections.emptySet()); - } + private List excludeAliasPatterns; /** - * Creates a filtered Key Vault certificate cache with an explicit AIA download setting. + * Creates a Key Vault certificate cache using the specified configuration. * - * @param refreshInterval Certificate refresh interval in milliseconds. - * @param keyVaultUri Key Vault URI. - * @param tenantId Tenant ID. - * @param clientId Client ID. - * @param clientSecret Client secret. - * @param managedIdentity Managed identity. - * @param accessToken Access token. - * @param disableChallengeResourceVerification Indicates if challenge resource verification should be disabled. - * @param disableAiaDownload Indicates if AIA certificate downloads should be disabled. - * @param certificateFilterPatterns Certificate alias filter patterns. + * @param parameter The Key Vault load-store configuration. */ - public KeyVaultCertificates(long refreshInterval, String keyVaultUri, String tenantId, String clientId, - String clientSecret, String managedIdentity, String accessToken, boolean disableChallengeResourceVerification, - boolean disableAiaDownload, Set certificateFilterPatterns) { - - this.refreshInterval = refreshInterval; - Set normalizedFilterPatterns = normalizeFilterPatterns(certificateFilterPatterns); - this.includeAliasPatterns = getAliasPatterns(normalizedFilterPatterns, false); - this.excludeAliasPatterns = getAliasPatterns(normalizedFilterPatterns, true); - - updateKeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, accessToken, - disableChallengeResourceVerification, disableAiaDownload); + public KeyVaultCertificates(KeyVaultLoadStoreParameter parameter) { + updateKeyVaultClient(parameter); } - public KeyVaultCertificates(long refreshInterval, KeyVaultClient keyVaultClient) { - this(refreshInterval, keyVaultClient, Collections.emptySet()); - } + private void updateCertificateConfiguration(KeyVaultLoadStoreParameter parameter) { + Objects.requireNonNull(parameter, "'parameter' cannot be null."); + Set normalizedFilterPatterns = normalizeFilterPatterns(parameter.getCertificateAliasFilterPatterns()); + List updatedIncludeAliasPatterns = getAliasPatterns(normalizedFilterPatterns, false); + List updatedExcludeAliasPatterns = getAliasPatterns(normalizedFilterPatterns, true); - public KeyVaultCertificates(long refreshInterval, KeyVaultClient keyVaultClient, - Set certificateFilterPatterns) { - this.refreshInterval = refreshInterval; - setKeyVaultClient(keyVaultClient); - Set normalizedFilterPatterns = normalizeFilterPatterns(certificateFilterPatterns); - this.includeAliasPatterns = getAliasPatterns(normalizedFilterPatterns, false); - this.excludeAliasPatterns = getAliasPatterns(normalizedFilterPatterns, true); + certificatesRefreshIntervalInMs = parameter.getCertificatesRefreshIntervalInMs(); + includeAliasPatterns = updatedIncludeAliasPatterns; + excludeAliasPatterns = updatedExcludeAliasPatterns; } private Set normalizeFilterPatterns(Set filterPatterns) { @@ -219,28 +194,16 @@ private synchronized void setKeyVaultClient(KeyVaultClient keyVaultClient) { } /** - * Update KeyVaultClient. + * Updates the Key Vault client and its certificate cache configuration. * - * @param keyVaultUri Key Vault URI. - * @param tenantId Tenant ID. - * @param clientId Client ID. - * @param clientSecret Client secret. - * @param managedIdentity Managed identity. - * @param accessToken Access token. - * @param disableChallengeResourceVerification Indicates if the challenge resource verification should be disabled. - * @param disableAiaDownload Indicates if AIA certificate downloads should be disabled. + * @param parameter The Key Vault load-store configuration. */ - public synchronized void updateKeyVaultClient(String keyVaultUri, String tenantId, String clientId, - String clientSecret, String managedIdentity, String accessToken, boolean disableChallengeResourceVerification, - boolean disableAiaDownload) { - - if (keyVaultUri != null) { - setKeyVaultClient(new KeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, - accessToken, disableChallengeResourceVerification, disableAiaDownload)); - } else { - setKeyVaultClient(null); - } + public synchronized void updateKeyVaultClient(KeyVaultLoadStoreParameter parameter) { + Objects.requireNonNull(parameter, "'parameter' cannot be null."); + KeyVaultClient updatedKeyVaultClient = parameter.getUri() == null ? null : new KeyVaultClient(parameter); + updateCertificateConfiguration(parameter); + setKeyVaultClient(updatedKeyVaultClient); clearCachedState(); } @@ -278,7 +241,8 @@ synchronized boolean certificatesNeedRefresh() { return true; } - return refreshInterval > 0 && lastRefreshTime.getTime() + refreshInterval < new Date().getTime(); + return certificatesRefreshIntervalInMs > 0 + && lastRefreshTime.getTime() + certificatesRefreshIntervalInMs < new Date().getTime(); } /** 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 7e967c0639ff..98264d7c197b 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,6 +3,7 @@ package com.azure.security.keyvault.jca; +import com.azure.security.keyvault.jca.implementation.KeyVaultClient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -14,15 +15,17 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Base64; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; +import java.util.List; + +import org.mockito.MockedConstruction; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mockConstruction; @ResourceLock(Resources.SYSTEM_PROPERTIES) public class KeyVaultKeyStoreUnitTest { @@ -56,19 +59,6 @@ public void testEngineStore() { keystore.engineStore(null, null); } - @Test - public void testGetRefreshInterval() { - System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL); - System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL_IN_MS); - KeyVaultKeyStore keystore = new KeyVaultKeyStore(); - assertEquals(keystore.getRefreshInterval(), 0); - System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL, "2000"); - keystore = new KeyVaultKeyStore(); - assertEquals(keystore.getRefreshInterval(), 2000); - System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL_IN_MS, "1000"); - assertEquals(keystore.getRefreshInterval(), 1000); - } - @Test public void testEngineGetCertificateAlias() { KeyVaultKeyStore keystore = new KeyVaultKeyStore(); @@ -102,55 +92,51 @@ public void testEngineSetCertificateEntry() { } @Test - public void testGetKeyVaultCertificateAliasFilterPatternsWhenNotConfigured() { - assertTrue(new KeyVaultKeyStore().getKeyVaultCertificateAliasFilterPatterns().isEmpty()); - } - - @Test - public void testGetKeyVaultCertificateAliasFilterPatternsFromBaseProperty() { - System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN, " ^prod-.* "); - - assertEquals(Collections.singleton("^prod-.*"), - new KeyVaultKeyStore().getKeyVaultCertificateAliasFilterPatterns()); + public void testEngineLoadParameterOverridesSystemProperties() { + System.setProperty(KeyVaultJcaPropertyNames.CERT_PATH_WELL_KNOWN, "/ambient/well-known"); + System.setProperty(KeyVaultJcaPropertyNames.CERT_PATH_CUSTOM, "/ambient/custom"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_REFRESH_CERTIFICATES_WHEN_HAVE_UNTRUST_CERTIFICATE, + "false"); + + KeyVaultKeyStore keyStore = new KeyVaultKeyStore(); + KeyVaultLoadStoreParameter parameter + = new KeyVaultLoadStoreParameter(null).setCertPathWellKnown("/explicit/well-known") + .setCertPathCustom("/explicit/custom") + .setRefreshCertificatesWhenHaveUnTrustCertificate(true); + + keyStore.engineLoad(parameter); + + assertEquals("/explicit/well-known", keyStore.certPathWellKnown); + assertEquals("/explicit/custom", keyStore.certPathCustom); + assertTrue(keyStore.refreshCertificatesWhenHaveUnTrustCertificate); } @Test - public void testGetKeyVaultCertificateAliasFilterPatternsFromSuffixedProperties() { - String base = KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN; - System.setProperty(base, "myalias"); - System.setProperty(base + ".1", "^prod-.*"); - System.setProperty(base + ".prod", "^prod-a.*"); - System.setProperty(base + ".PROD", "^prod-b.*"); - System.setProperty(base + ".exclude-old", "!.*-old$"); - System.setProperty(base + ".blank", " "); - - Set expected - = new HashSet<>(Arrays.asList("myalias", "^prod-.*", "^prod-a.*", "^prod-b.*", "!.*-old$")); - - assertEquals(expected, new KeyVaultKeyStore().getKeyVaultCertificateAliasFilterPatterns()); - } - - @Test - public void testGetKeyVaultCertificateAliasFilterPatternsKeepsCommas() { - String base = KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN; - System.setProperty(base + ".1", "^cert-\\d{1,5}$"); - System.setProperty(base + ".2", "![a-z]{2,}"); - - Set expected = new HashSet<>(Arrays.asList("^cert-\\d{1,5}$", "![a-z]{2,}")); + public void testEngineLoadPassesExplicitParameterToKeyVaultClient() { + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI, "https://ambient.vault.azure.net"); + KeyVaultLoadStoreParameter parameter = new KeyVaultLoadStoreParameter("https://explicit.vault.azure.net"); + parameter.disableAiaDownload(); + List> constructorArguments = new ArrayList<>(); + + try (MockedConstruction mockedConstruction = mockConstruction(KeyVaultClient.class, + (mock, context) -> constructorArguments.add(context.arguments()))) { + KeyVaultKeyStore keyStore = new KeyVaultKeyStore(); + keyStore.engineLoad(parameter); + assertEquals(2, mockedConstruction.constructed().size()); + } - assertEquals(expected, new KeyVaultKeyStore().getKeyVaultCertificateAliasFilterPatterns()); + assertEquals(2, constructorArguments.size()); + assertSame(parameter, constructorArguments.get(1).get(0)); + assertTrue(((KeyVaultLoadStoreParameter) constructorArguments.get(1).get(0)).isAiaDownloadDisabled()); } @BeforeEach @AfterEach public void clearCertificateAliasFilterPatternProperties() { - String base = KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN; - System.clearProperty(base); - System.getProperties() - .stringPropertyNames() - .stream() - .filter(name -> name.startsWith(base + ".")) - .forEach(System::clearProperty); + System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI); + System.clearProperty(KeyVaultJcaPropertyNames.CERT_PATH_WELL_KNOWN); + System.clearProperty(KeyVaultJcaPropertyNames.CERT_PATH_CUSTOM); + System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_REFRESH_CERTIFICATES_WHEN_HAVE_UNTRUST_CERTIFICATE); } } diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultLoadStoreParameterTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultLoadStoreParameterTest.java new file mode 100644 index 000000000000..cd28a487bc82 --- /dev/null +++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultLoadStoreParameterTest.java @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.security.keyvault.jca; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.api.parallel.Resources; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ResourceLock(Resources.SYSTEM_PROPERTIES) +public class KeyVaultLoadStoreParameterTest { + + @BeforeEach + @AfterEach + public void clearSystemProperties() { + System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI); + System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_TENANT_ID); + System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_ID); + System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_SECRET); + System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_MANAGED_IDENTITY); + System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_ACCESS_TOKEN); + System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_DISABLE_CHALLENGE_RESOURCE_VERIFICATION); + System.clearProperty(KeyVaultJcaPropertyNames.CERT_PATH_WELL_KNOWN); + System.clearProperty(KeyVaultJcaPropertyNames.CERT_PATH_CUSTOM); + System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL); + System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL_IN_MS); + System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_REFRESH_CERTIFICATES_WHEN_HAVE_UNTRUST_CERTIFICATE); + System.clearProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD); + String aliasFilterProperty = KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN; + System.getProperties() + .stringPropertyNames() + .stream() + .filter(name -> name.equals(aliasFilterProperty) || name.startsWith(aliasFilterProperty + ".")) + .forEach(System::clearProperty); + } + + @Test + public void testFromSystemPropertiesCapturesAllProperties() { + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI, "https://test.vault.azure.net"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_TENANT_ID, "tenant-id"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_ID, "client-id"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_CLIENT_SECRET, "client-secret"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_MANAGED_IDENTITY, "managed-identity"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_ACCESS_TOKEN, "access-token"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_DISABLE_CHALLENGE_RESOURCE_VERIFICATION, "true"); + System.setProperty(KeyVaultJcaPropertyNames.CERT_PATH_WELL_KNOWN, "/well-known"); + System.setProperty(KeyVaultJcaPropertyNames.CERT_PATH_CUSTOM, "/custom"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL_IN_MS, "1000"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_REFRESH_CERTIFICATES_WHEN_HAVE_UNTRUST_CERTIFICATE, + "true"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN, " alias-one "); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATE_ALIAS_FILTER_PATTERN + ".prod", + "^prod-.*"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD, "true"); + + KeyVaultLoadStoreParameter parameter = KeyVaultLoadStoreParameter.fromSystemProperties(); + + assertEquals("https://test.vault.azure.net", parameter.getUri()); + assertEquals("tenant-id", parameter.getTenantId()); + assertEquals("client-id", parameter.getClientId()); + assertEquals("client-secret", parameter.getClientSecret()); + assertEquals("managed-identity", parameter.getManagedIdentity()); + assertEquals("access-token", parameter.getAccessToken()); + assertTrue(parameter.isDisableChallengeResourceVerification()); + assertEquals("/well-known", parameter.getCertPathWellKnown()); + assertEquals("/custom", parameter.getCertPathCustom()); + assertEquals(1000L, parameter.getCertificatesRefreshIntervalInMs()); + assertTrue(parameter.isRefreshCertificatesWhenHaveUnTrustCertificate()); + assertEquals(new HashSet<>(Arrays.asList("alias-one", "^prod-.*")), + parameter.getCertificateAliasFilterPatterns()); + assertTrue(parameter.isAiaDownloadDisabled()); + } + + @Test + public void testFromSystemPropertiesUsesDefaults() { + KeyVaultLoadStoreParameter parameter = KeyVaultLoadStoreParameter.fromSystemProperties(); + + assertNull(parameter.getUri()); + assertEquals("/etc/certs/well-known/", parameter.getCertPathWellKnown()); + assertEquals("/etc/certs/custom/", parameter.getCertPathCustom()); + assertEquals(0L, parameter.getCertificatesRefreshIntervalInMs()); + assertFalse(parameter.isRefreshCertificatesWhenHaveUnTrustCertificate()); + assertTrue(parameter.getCertificateAliasFilterPatterns().isEmpty()); + assertFalse(parameter.isDisableChallengeResourceVerification()); + assertFalse(parameter.isAiaDownloadDisabled()); + } + + @Test + public void testRefreshIntervalInMsTakesPrecedence() { + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL, "2000"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL_IN_MS, "1000"); + + assertEquals(1000L, KeyVaultLoadStoreParameter.fromSystemProperties().getCertificatesRefreshIntervalInMs()); + } + + @Test + public void testLegacyRefreshIntervalIsFallback() { + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_CERTIFICATES_REFRESH_INTERVAL, "2000"); + + assertEquals(2000L, KeyVaultLoadStoreParameter.fromSystemProperties().getCertificatesRefreshIntervalInMs()); + } + + @Test + public void testCertificateAliasFilterPatternsAreDefensivelyCopied() { + Set filterPatterns = new HashSet<>(Arrays.asList("alias-one", "alias-two")); + KeyVaultLoadStoreParameter parameter + = new KeyVaultLoadStoreParameter(null).setCertificateAliasFilterPatterns(filterPatterns); + + filterPatterns.clear(); + Set returnedPatterns = parameter.getCertificateAliasFilterPatterns(); + returnedPatterns.clear(); + + assertEquals(new HashSet<>(Arrays.asList("alias-one", "alias-two")), + parameter.getCertificateAliasFilterPatterns()); + } + + @Test + public void testFromSystemPropertiesCreatesSnapshot() { + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI, "https://first.vault.azure.net"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD, "true"); + + KeyVaultLoadStoreParameter parameter = KeyVaultLoadStoreParameter.fromSystemProperties(); + + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_URI, "https://second.vault.azure.net"); + System.setProperty(KeyVaultJcaPropertyNames.KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD, "false"); + + assertEquals("https://first.vault.azure.net", parameter.getUri()); + assertTrue(parameter.isAiaDownloadDisabled()); + } +} 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 290b5a33f4cc..9b3ab4c25090 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 @@ -9,8 +9,10 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.azure.security.keyvault.jca.KeyVaultLoadStoreParameter; import com.azure.security.keyvault.jca.implementation.CertificateVersion; import com.azure.security.keyvault.jca.implementation.KeyVaultClient; +import java.lang.reflect.Field; import java.security.Key; import java.security.cert.Certificate; import java.util.ArrayList; @@ -49,6 +51,29 @@ public class KeyVaultCertificatesTest { private KeyVaultCertificates keyVaultCertificates; + private KeyVaultCertificates createKeyVaultCertificates(KeyVaultClient client) { + return createKeyVaultCertificates(client, Collections.emptySet()); + } + + private KeyVaultCertificates createKeyVaultCertificates(KeyVaultClient client, Set filterPatterns) { + KeyVaultLoadStoreParameter parameter + = new KeyVaultLoadStoreParameter(null).setCertificatesRefreshIntervalInMs(60_000) + .setCertificateAliasFilterPatterns(filterPatterns); + KeyVaultCertificates certificates = new KeyVaultCertificates(parameter); + setKeyVaultClient(certificates, client); + return certificates; + } + + private void setKeyVaultClient(KeyVaultCertificates certificates, KeyVaultClient client) { + try { + Field keyVaultClientField = KeyVaultCertificates.class.getDeclaredField("keyVaultClient"); + keyVaultClientField.setAccessible(true); + keyVaultClientField.set(certificates, client); + } catch (ReflectiveOperationException exception) { + throw new IllegalStateException("Failed to inject the test KeyVaultClient.", exception); + } + } + @BeforeEach public void beforeEach() { List aliases = new ArrayList<>(); @@ -58,7 +83,7 @@ public void beforeEach() { when(keyVaultClient.getKeyForVersion(certificateVersion, null)).thenReturn(key); when(keyVaultClient.getCertificateForVersion(certificateVersion)).thenReturn(certificate); when(keyVaultClient.getCertificateChainForVersion(certificateVersion)).thenReturn(certificateChain); - keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient); + keyVaultCertificates = createKeyVaultCertificates(keyVaultClient); } @Test @@ -271,7 +296,7 @@ public void testConfiguredAliasesFilter() { aliases.add("otheralias"); when(keyVaultClient.getAliases()).thenReturn(aliases); - keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("myalias")); + keyVaultCertificates = createKeyVaultCertificates(keyVaultClient, Collections.singleton("myalias")); List result = keyVaultCertificates.getAliases(); Assertions.assertEquals(1, result.size()); @@ -286,7 +311,7 @@ public void testFilterPatternsIncludeRegex() { aliases.add("dev-cert"); when(keyVaultClient.getAliases()).thenReturn(aliases); - keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("^prod-.*")); + keyVaultCertificates = createKeyVaultCertificates(keyVaultClient, Collections.singleton("^prod-.*")); Assertions.assertEquals(Collections.singletonList("prod-cert"), keyVaultCertificates.getAliases()); verify(keyVaultClient, times(1)).getAliases(); @@ -300,7 +325,7 @@ public void testFilterPatternsExcludeRegex() { when(keyVaultClient.getAliases()).thenReturn(aliases); Set filterPatterns = new HashSet<>(Arrays.asList("^prod-.*", "!^prod-deprecated$")); - keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, filterPatterns); + keyVaultCertificates = createKeyVaultCertificates(keyVaultClient, filterPatterns); Assertions.assertEquals(Collections.singletonList("prod-active"), keyVaultCertificates.getAliases()); verify(keyVaultClient, times(1)).getAliases(); @@ -308,7 +333,7 @@ public void testFilterPatternsExcludeRegex() { @Test public void testConfiguredAliasesFilterAfterRefresh() { - keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("myalias")); + keyVaultCertificates = createKeyVaultCertificates(keyVaultClient, Collections.singleton("myalias")); Assertions.assertEquals(Collections.singletonList("myalias"), keyVaultCertificates.getAliases()); @@ -326,8 +351,7 @@ public void testConfiguredAliasesFilterAfterRefresh() { public void testConfiguredAliasesFilterUsesListApi() { when(keyVaultClient.getAliases()).thenReturn(Arrays.asList("configured-alias", "other-alias")); - keyVaultCertificates - = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("configured-alias")); + keyVaultCertificates = createKeyVaultCertificates(keyVaultClient, Collections.singleton("configured-alias")); Assertions.assertEquals(Collections.singletonList("configured-alias"), keyVaultCertificates.getAliases()); verify(keyVaultClient, times(1)).getAliases(); @@ -336,7 +360,7 @@ public void testConfiguredAliasesFilterUsesListApi() { @Test public void testConfiguredAliasesIgnoreNullEntries() { Set configuredAliases = new HashSet<>(Arrays.asList("myalias", null)); - keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, configuredAliases); + keyVaultCertificates = createKeyVaultCertificates(keyVaultClient, configuredAliases); Assertions.assertEquals(Collections.singletonList("myalias"), keyVaultCertificates.getAliases()); verify(keyVaultClient, times(1)).getAliases(); @@ -347,22 +371,21 @@ public void testInvalidFilterPatternThrows() { Set filterPatterns = new HashSet<>(Collections.singletonList("[invalid")); Assertions.assertThrows(IllegalArgumentException.class, - () -> new KeyVaultCertificates(60_000, keyVaultClient, filterPatterns)); + () -> createKeyVaultCertificates(keyVaultClient, filterPatterns)); } @Test public void testFilterPatternWithBoundedQuantifier() { when(keyVaultClient.getAliases()).thenReturn(Arrays.asList("cert-42", "cert-1234567", "cert-abc")); - keyVaultCertificates - = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("^cert-\\d{1,5}$")); + keyVaultCertificates = createKeyVaultCertificates(keyVaultClient, Collections.singleton("^cert-\\d{1,5}$")); Assertions.assertEquals(Collections.singletonList("cert-42"), keyVaultCertificates.getAliases()); } @Test public void testGetCertificateWithUnconfiguredAliasDoesNotFetchDetails() { - keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient, Collections.singleton("myalias")); + keyVaultCertificates = createKeyVaultCertificates(keyVaultClient, Collections.singleton("myalias")); Assertions.assertNull(keyVaultCertificates.getCertificate("otheralias")); @@ -535,7 +558,7 @@ public void testUpdateKeyVaultClientClearsCachedState() { Assertions.assertTrue(keyVaultCertificates.getAliases().contains("myalias")); Assertions.assertEquals(certificate, keyVaultCertificates.getCertificate("myalias")); - keyVaultCertificates.updateKeyVaultClient(null, null, null, null, null, null, false, false); + keyVaultCertificates.updateKeyVaultClient(new KeyVaultLoadStoreParameter(null)); Assertions.assertTrue(keyVaultCertificates.getAliases().isEmpty()); Assertions.assertTrue(keyVaultCertificates.getCertificates().isEmpty()); @@ -544,6 +567,35 @@ public void testUpdateKeyVaultClientClearsCachedState() { Assertions.assertNull(keyVaultCertificates.getCertificate("myalias")); } + @Test + public void testUpdateKeyVaultClientAppliesAliasFilterPatterns() { + when(keyVaultClient.getAliases()).thenReturn(Arrays.asList("prod-cert", "dev-cert")); + KeyVaultLoadStoreParameter parameter + = new KeyVaultLoadStoreParameter(null).setCertificateAliasFilterPatterns(Collections.singleton("^prod-.*")); + + keyVaultCertificates.updateKeyVaultClient(parameter); + setKeyVaultClient(keyVaultCertificates, keyVaultClient); + + Assertions.assertEquals(Collections.singletonList("prod-cert"), keyVaultCertificates.getAliases()); + } + + @Test + public void testInvalidClientUpdatePreservesExistingAliasFilters() { + when(keyVaultClient.getAliases()).thenReturn(Arrays.asList("myalias", "otheralias")); + keyVaultCertificates = createKeyVaultCertificates(keyVaultClient, Collections.singleton("myalias")); + Assertions.assertEquals(Collections.singletonList("myalias"), keyVaultCertificates.getAliases()); + + Set invalidFilterPatterns = new HashSet<>(Arrays.asList("otheralias", "![invalid")); + KeyVaultLoadStoreParameter parameter + = new KeyVaultLoadStoreParameter(null).setCertificateAliasFilterPatterns(invalidFilterPatterns); + + Assertions.assertThrows(IllegalArgumentException.class, + () -> keyVaultCertificates.updateKeyVaultClient(parameter)); + keyVaultCertificates.refreshCertificates(); + + Assertions.assertEquals(Collections.singletonList("myalias"), keyVaultCertificates.getAliases()); + } + @Test public void testConcurrentCertificateLoadsShareSingleRequest() throws Exception { BlockingAnswer blockingAnswer = new BlockingAnswer<>(certificate); @@ -640,7 +692,7 @@ public void testClientReplacementDiscardsInFlightCertificate() throws Exception reader.start(); staleAnswer.awaitStarted(); - keyVaultCertificates.updateKeyVaultClient(null, null, null, null, null, null, false, false); + keyVaultCertificates.updateKeyVaultClient(new KeyVaultLoadStoreParameter(null)); staleAnswer.release(); joinThreads(Collections.singletonList(reader)); @@ -665,7 +717,7 @@ public void testConcurrentForceRefreshAppliesLatestAliases() throws Exception { return Collections.singletonList("fresh-alias"); }); - keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient); + keyVaultCertificates = createKeyVaultCertificates(keyVaultClient); Thread slowRefresh = new Thread(keyVaultCertificates::refreshCertificates); slowRefresh.start(); @@ -693,7 +745,7 @@ public void testConcurrentRefreshIssuesSingleAliasListCall() throws Exception { return Collections.singletonList("myalias"); }); - keyVaultCertificates = new KeyVaultCertificates(60_000, keyVaultClient); + keyVaultCertificates = createKeyVaultCertificates(keyVaultClient); List readers = new ArrayList<>(); for (int i = 0; i < 4; i++) {