以编程方式重新加载 Java TrustStore

问题描述 投票:0回答:3

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

使用案例: 以编程方式与一个或多个 https 服务连接的 Web 应用程序,这些服务是动态的,并且证书会频繁更新。

应用程序应该做的是使用新证书更新 TrustStore 并使用它们,而无需重新启动应用程序。重要的是,不应实现任何新代码来进行 https 连接(因此,它应该无缝集成)。

我尝试过(没有运气)覆盖默认的 Java TrustManager,任何帮助将不胜感激。

编辑:我已经尝试了评论/答案中提出的一些解决方案,但之后我仍然需要重新启动我的tomcat

java truststore trustmanager
3个回答
1
投票

虽然它发布在另一篇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;
    }

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


1
投票

我在这里发布了类似的答案: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());

-3
投票

似乎这个问题已在这里得到解答以编程方式将 CA 信任证书导入到现有密钥库文件中,而不使用 keytool .

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

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

© www.soinside.com 2019 - 2024. All rights reserved.