繁体   English   中英

以编程方式重新加载 Java TrustStore

[英]Reload Java TrustStore programmatically

我已经阅读了很多关于这个话题的文章,这似乎是不可能的,但只是为了确定我还想再听听一个意见。

用例:Web 应用程序以编程方式连接到一项或多项 https 服务,服务是动态的,证书经常更新。

应用程序应该做的是用新证书更新 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 中:

  1. 通过文件的修改时间戳检查 TrustStore 是否被修改。 如果修改,则使用新的 TrustStore 创建一个新的 TrustManager。
  2. 调用嵌入式 X509TrustManager 的 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;
    }

如果您需要可重新加载的 KeyStore,可以执行类似的操作。 但是,该类应实现 X509KeyManager,而不是实现 X509TrustManager。 此自定义 KeyManager 作为数组传递给 sslContext.init() 的第一个参数。

我在这里发布了类似的答案: Reloading a java.net.http.HttpClient's SSLContext

基本上,您需要的是一个自定义信任管理器,它环绕着实际的信任管理器,它能够在需要时交换实际的信任管理器,例如,当信任库被更新时。

您可以在此处的链接上找到完整的答案,下面是一个小片段,它应该可以为您解决问题,它在幕后使用名为 hot swappable trustmanager 的包装器 trustmanager

SSLFactory sslFactory = SSLFactory.builder()
          .withSwappableTrustMaterial()
          .withTrustMaterial("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
TrustManagerUtils.swapTrustManager(sslFactory.getTrustManager().get(), anotherTrustManager);

// Cleanup old ssl sessions by invalidating them all. Forces to use new ssl sessions which will be created by the swapped TrustManager
SSLSessionUtils.invalidateCaches(sslFactory.getSslContext());

HttpResponse<String> response = httpClient.send(aRequest, HttpResponse.BodyHandlers.ofString());

似乎这个问题已经在这里得到了回答Programmatically Import CA trust cert into existing keystore file without using keytool

我认为问题在于,信任库和密钥库实际上是相同的东西,但它们与密钥管理器一起用于私钥(客户端身份验证(通常不使用且没有真正的签名权限))和信任管理器用于服务器身份验证(总是为完整的 tls 完成)连接)。

在这方面,您仍然以编程方式使用密钥库作为信任库。 希望我对此是正确的。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM