diff --git a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md index 836a94cb3f47..366ac061f8d7 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md +++ b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md @@ -11,9 +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 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`). +- 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 c60d90c37933..8109aa6a452f 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/README.md +++ b/sdk/keyvault/azure-security-keyvault-jca/README.md @@ -143,17 +143,44 @@ 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. -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 -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/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..73f0db5f5c21 --- /dev/null +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultJcaPropertyNames.java @@ -0,0 +1,89 @@ +// 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 { + + private 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"; + +} 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..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; @@ -59,9 +53,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"; - /** * Stores the Jre key store certificates. */ @@ -70,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. @@ -90,111 +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; + boolean refreshCertificatesWhenHaveUnTrustCertificate; /** * 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/"); + String certPathWellKnown; /** * Store the path where the custom certificate is placed */ - final String customPath - = Optional.ofNullable(System.getProperty("azure.cert-path.custom")).orElse("/etc/certs/custom/"); + String certPathCustom; /** * Constructor. * - *

- * The constructor uses System.getProperty for - * azure.keyvault.uri, - * azure.keyvault.tenantId, - * azure.keyvault.clientId, - * azure.keyvault.clientSecret and - * azure.keyvault.managedIdentity 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("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")); - long refreshInterval = getRefreshInterval(); - refreshCertificatesWhenHaveUnTrustCertificate - = Optional.of("azure.keyvault.jca.refresh-certificates-when-have-un-trust-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, 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("azure.keyvault.jca.certificates-refresh-interval-in-ms", - "azure.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 = CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY + "."; - - return properties.stringPropertyNames() - .stream() - .filter(name -> name.equals(CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY) - || 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. @@ -204,17 +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("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"))) { - keyVaultLoadStoreParameter.disableChallengeResourceVerification(); - } keyStore.load(keyVaultLoadStoreParameter); @@ -434,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, @@ -444,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()); + 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 62856e8ca44a..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,6 +2,7 @@ // Licensed under the MIT License. package com.azure.security.keyvault.jca.implementation; +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; @@ -105,6 +106,22 @@ public class KeyVaultClient { */ private final boolean disableChallengeResourceVerification; + /** + * Stores a flag indicating whether Authority Information Access (AIA) certificate downloads are disabled. + */ + 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. * @@ -156,6 +173,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,20 +208,11 @@ 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() { - 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")); - - return new KeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, accessToken, - disableChallengeResourceVerification); + return new KeyVaultClient(KeyVaultLoadStoreParameter.fromSystemProperties()); } /** @@ -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..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 @@ -3,6 +3,8 @@ 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; @@ -29,9 +31,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. */ @@ -104,42 +103,30 @@ public final class KeyVaultCertificates implements AzureCertificates { private KeyVaultClient keyVaultClient; - private final long refreshInterval; - - private final List includeAliasPatterns; - - private final List excludeAliasPatterns; - - public KeyVaultCertificates(long refreshInterval, String keyVaultUri, String tenantId, String clientId, - String clientSecret, String managedIdentity, String accessToken, boolean disableChallengeResourceVerification) { - this(refreshInterval, keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, accessToken, - disableChallengeResourceVerification, Collections.emptySet()); - } + private long certificatesRefreshIntervalInMs; - public KeyVaultCertificates(long refreshInterval, String keyVaultUri, String tenantId, String clientId, - String clientSecret, String managedIdentity, String accessToken, boolean disableChallengeResourceVerification, - Set certificateFilterPatterns) { + private List includeAliasPatterns; - this.refreshInterval = refreshInterval; - Set normalizedFilterPatterns = normalizeFilterPatterns(certificateFilterPatterns); - this.includeAliasPatterns = getAliasPatterns(normalizedFilterPatterns, false); - this.excludeAliasPatterns = getAliasPatterns(normalizedFilterPatterns, true); + private List excludeAliasPatterns; - updateKeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, accessToken, - disableChallengeResourceVerification); + /** + * Creates a Key Vault certificate cache using the specified configuration. + * + * @param parameter The Key Vault load-store configuration. + */ + 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) { @@ -175,9 +162,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); } } @@ -204,26 +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 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) { - - if (keyVaultUri != null) { - setKeyVaultClient(new KeyVaultClient(keyVaultUri, tenantId, clientId, clientSecret, managedIdentity, - accessToken, disableChallengeResourceVerification)); - } 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(); } @@ -261,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/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 88af091e5550..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); @@ -83,8 +84,7 @@ 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 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[])}). @@ -95,19 +95,18 @@ static boolean shouldCompleteChainViaAia(Certificate[] 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}]", - DISABLE_AIA_DOWNLOAD_PROPERTY); + if (disableAiaDownload) { + LOGGER.log(FINE, "AIA chain completion is disabled for this Key Vault client by configuration [{0}]", + KeyVaultJcaPropertyNames.KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD); 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/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..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("azure.keyvault.jca.certificates-refresh-interval"); - System.clearProperty("azure.keyvault.jca.certificates-refresh-interval-in-ms"); - KeyVaultKeyStore keystore = new KeyVaultKeyStore(); - assertEquals(keystore.getRefreshInterval(), 0); - System.setProperty("azure.keyvault.jca.certificates-refresh-interval", "2000"); - keystore = new KeyVaultKeyStore(); - assertEquals(keystore.getRefreshInterval(), 2000); - System.setProperty("azure.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(KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY, " ^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 = KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY; - 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 = KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY; - 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 = KeyVaultKeyStore.CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY; - 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/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/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..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); + 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); + 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++) { 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 bffccde0e0d1..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,10 @@ 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; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.AccessDescription; import org.bouncycastle.asn1.x509.AuthorityInformationAccess; @@ -64,6 +68,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}. @@ -112,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(); } @@ -164,7 +170,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 +187,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 +200,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 +214,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 +222,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 +286,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 +316,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 +392,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 +418,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(KeyVaultJcaPropertyNames.KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD, "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(KeyVaultJcaPropertyNames.KEYVAULT_JCA_DISABLE_AIA_DOWNLOAD, "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 +503,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 +516,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 +529,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 +546,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 +572,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 +1022,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 +1062,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()); } diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md index dec32cd81bb0..9f866a9d0c23 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`. ([#50163](https://github.com/Azure/azure-sdk-for-java/pull/50163)) #### Bugs Fixed 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..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 @@ -266,6 +267,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();