我已经阅读了很多关于这个主题的文章,这似乎是不可能的,但只是为了确定我还想要一个意见。
使用案例: 以编程方式与一个或多个 https 服务连接的 Web 应用程序,这些服务是动态的,并且证书会频繁更新。
应用程序应该做的是使用新证书更新 TrustStore 并使用它们,而无需重新启动应用程序。重要的是,不应实现任何新代码来进行 https 连接(因此,它应该无缝集成)。
我尝试过(没有运气)覆盖默认的 Java TrustManager,任何帮助将不胜感激。
编辑:我已经尝试了评论/答案中提出的一些解决方案,但之后我仍然需要重新启动我的tomcat
虽然它发布在另一篇SO帖子的评论中,但我想提到这个方法作为一个潜在的答案,因为它也帮助我解决了这个问题。
本文告诉我们如何创建一个新的 SSLContext,其中包含标准 X509TrustManager 的包装器 (ReloadableX509TrustManager):https://jcalcote.wordpress.com/2010/06/22/managing-a-dynamic-java-trust-store /
每当客户端/服务器进行身份验证(使用 checkClientTrusted/checkServerTrusted)时,X509ReloadableTrustManager 将调用 X509TrustManager 中的相关方法。如果失败(抛出CertificateException),那么它将在再次尝试之前重新加载TrustStore。每次“重新加载”实际上都会用一个新实例替换 X509TrustManager,因为我们无法触及其中的证书。
就我个人而言,我与这篇文章略有不同。 ReloadableX509TrustManager 的 checkClientTrusted/checkServerTrusted 内:
为了减少文件 I/O 请求的数量,我跟踪了 TrustStore 上最后一次检查的时间戳,以将 TrustStore 上的轮询间隔限制为最小 15 秒。
我相信我的方法稍微好一点,因为它允许使用当前的 TrustStore 进行身份验证,也可以从中删除证书。即使相关证书被删除,原始方法仍然允许应用程序持续信任客户端/服务器。
编辑:回想起来,我认为重新加载过程应该是线程安全的,因为我找不到任何表明 X509TrustManager 中的 checkClientTrusted() 和 checkServerTrusted() 方法可能在设计时没有考虑线程安全的内容。事实上,默认 X509TrustManagerImpl 类的 checkTrustedInit() 方法有一些同步块 - 这可能暗示这些函数必须是线程安全的。
编辑 2021/04/10:这是一个示例实现:
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 ;
}
}
然后生成一个使用新 TrustManager 的新 SSLContext:
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;
}
如果您需要可重新加载的密钥库,可以执行类似的操作。但该类不应实现 X509TrustManager,而应实现 X509KeyManager。此自定义 KeyManager 作为数组传递给 sslContext.init() 的第一个参数。
我在这里发布了类似的答案:Reloading a java.net.http.HttpClient's SSLContext
基本上,您需要的是一个自定义信任管理器,它包围实际的信任管理器,能够在需要时交换实际的信任管理器,例如当信任库更新时。
您可以在链接上找到完整的答案这里,下面是一个小片段,应该可以为您解决问题,它在幕后使用名为热插拔信任管理器的包装器信任管理器
SSLFactory baseSslFactory = SSLFactory.builder()
.withSwappableTrustMaterial()
.withTrustMaterial(Paths.get("path/to/truststore.jks"), "password".toCharArray())
.build();
HttpClient httpClient = HttpClient.newBuilder()
.sslParameters(sslFactory.getSslParameters())
.sslContext(sslFactory.getSslContext())
.build()
// execute https request
HttpResponse<String> response = httpClient.send(aRequest, HttpResponse.BodyHandlers.ofString());
// swap trust materials and reuse existing http client
SSLFactory updatedSslFactory = SSLFactory.builder()
.withTrustMaterial(Paths.get("path/to/truststore.jks"), "password".toCharArray())
.build();
SSLFactoryUtils.reload(baseSslFactory, updatedSslFactory);
HttpResponse<String> response = httpClient.send(aRequest, HttpResponse.BodyHandlers.ofString());
似乎这个问题已在这里得到解答以编程方式将 CA 信任证书导入到现有密钥库文件中,而不使用 keytool .
我认为问题在于,信任库和密钥库实际上是相同的东西,但它们与用于私钥的密钥管理器(客户端身份验证(通常不使用且没有真正的签名权限))和用于服务器身份验证的信任管理器(始终完成完整的tls 连接)。
在这方面,您仍然以编程方式使用密钥库作为信任库。希望我在这一点上是正确的。