While it is posted in the comment of this other SO post, I want to mention this method as a potential answer because it helped me solve this problem too.
This article tells us about creating a new SSLContext that contains a wrapper (ReloadableX509TrustManager) around the standard X509TrustManager: https://jcalcote.wordpress.com/2010/06/22/managing-a-dynamic-java-trust-store/
Whenever a client/server is being authenticated (with checkClientTrusted/checkServerTrusted), the X509ReloadableTrustManager will call the related method of the X509TrustManager within. If it fails (CertificateException is thrown), then it will reload the TrustStore before making another attempt. Each "reload" actually replaces the X509TrustManager with a new instance, since we cannot touch the certificates within.
Personally, I varied from this article slightly. Within checkClientTrusted/checkServerTrusted of ReloadableX509TrustManager:
- Check if the TrustStore was modified, by the file's modified timestamp. If modified, then create a new TrustManager with the new TrustStore.
- Call checkClientTrusted/checkServerTrusted of the embedded X509TrustManager.
To reduce the number of file I/O requests, I kept track of the timestamp of the last check on the TrustStore, to restrict the polling interval on the TrustStore to a minimum of 15s.
I believe my method is slightly better because it would allow authentication to take place with the current TrustStore, which could also have certificates removed from it. The original method would still allow the application to continuously trust a client/server, even if the related certificate(s) was/were removed.
EDIT: In retrospect, I think the reloading process should be made thread-safe because I cannot find anything that indicates that the checkClientTrusted() and checkServerTrusted() methods within X509TrustManager may be designed without thread-safety in mind. In fact, the checkTrustedInit() method of the default X509TrustManagerImpl class has some synchronized blocks - which might hint that these functions must be made thread-safe.
EDIT 2021/04/10: Here is a sample implementation:
package com.test.certificate;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchProviderException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.List;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ReloadableX509TrustManager implements X509TrustManager {
private static final Logger logger = LoggerFactory.getLogger(ReloadableX509TrustManager.class);
private X509TrustManager trustManager;
private static final String KEYSTORE_RUNTIME_FORMAT = "JKS";
private static final String CERTIFICATE_ENTRY_FORMAT = "X.509";
public ReloadableX509TrustManager() throws Exception {
reload();
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
try {
reload();
} catch (Exception ex) {
logger.warn("Failed to reload TrustStore due to " + ex, ex);
}
trustManager.checkClientTrusted(chain, authType);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
try {
reload();
} catch (Exception ex) {
logger.warn("Failed to reload TrustStore due to " + ex, ex);
}
trustManager.checkServerTrusted(chain, authType);
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return trustManager.getAcceptedIssuers();
}
/**
* Reloads the inner TrustStore.
* For performance, reloading of the TrustStore will only be done if there is a change.
* @throws Exception
*/
public synchronized void reload() throws Exception {
if (!isUpdated())
return;
KeyStore trustStore = KeyStore.getInstance(KEYSTORE_RUNTIME_FORMAT);
trustStore.load(null, null);
List<TrustedCertificate> certs = getCertificates();
CertificateFactory cf = CertificateFactory.getInstance(CERTIFICATE_ENTRY_FORMAT);
for (TrustedCertificate cert : certs) {
InputStream is = new ByteArrayInputStream(cert.getCertificate());
Certificate certEntry;
try {
certEntry = cf.generateCertificate(is);
} catch (CertificateException e) {
logger.error("Failed to generate certificate " + cert.getAliasForKeystore() + " due to: " + e);
continue;
} finally {
is.close();
}
try {
trustStore.setCertificateEntry(cert.getAliasForKeystore(), certEntry);
} catch (KeyStoreException e) {
logger.error("Failed to insert certificate " + cert.getAliasForKeystore() + " due to: " + e);
continue;
}
}
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
// Locate the X509TrustManager and get a reference to it.
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
for (TrustManager tm : trustManagers) {
if (tm instanceof X509TrustManager) {
trustManager = (X509TrustManager)tm;
return;
}
}
throw new NoSuchProviderException("X509TrustManager not available from TrustManagerFactory.");
}
/**
* Indicates whether the TrustStore was updated.
* @return Whether the TrustStore was updated.
*/
private boolean isUpdated() {
// TODO Write your logic to check whether the TrustStore was updated.
// If disk I/O is used, it may be good to limit how often the file is accessed for performance.
return false;
}
/**
* Returns a list of certificates from the TrustStore.
* @return A list of certificates from the TrustStore.
* @throws Exception
*/
private List<TrustedCertificate> getCertificates() throws Exception {
// TODO Write your logic to retrieve all certificates from the TrustStore.
return ;
}
}
And then to generate a new SSLContext that uses the new TrustManager:
private SSLContext initContext() throws Exception {
TrustManager[] trustManagers = { getTrustManager() };
//Initialize a new SSLContext, with our custom TrustManager.
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagers, null);
return sslContext;
}
Something similar can be done if you needed a reloadable KeyStore. But instead of implementing X509TrustManager, the class shall implement X509KeyManager instead. This custom KeyManager is passed as an array to the first argument of sslContext.init().