【问题标题】:How to update Truststore dynamically?如何动态更新信任库?
【发布时间】:2019-11-30 06:59:00
【问题描述】:

我目前在我的 Spring Boot 应用程序中实现了双向 TLS,并且我正在以编程方式进行,如下所示:

@Bean
public ServletWebServerFactory servContainer() {
    TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
    TomcatConnectorCustomizer tomcatConnectorCustomizer = new TomcatConnectorCustomizer() {
        @Override
        public void customize(Connector connector) {
            connector.setPort(8443);
            connector.setScheme("https");
            connector.setSecure(true);
            Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();

            protocol.setSSLEnabled(true);
            protocol.setKeystoreType("PKCS12");
            protocol.setKeystoreFile(keystorePath);
            protocol.setKeystorePass(keystorePass);
            //client must be authenticated (the cert he sends should be in our trust store)
            protocol.setSSLVerifyClient(Boolean.toString(true));
            protocol.setTruststoreFile(truststorePath);
            protocol.setTruststorePass(truststorePass);
            protocol.setKeyAlias("APP");
        }
    };
    tomcat.addConnectorCustomizers(tomcatConnectorCustomizer);
    return tomcat;
}

这工作正常且符合预期,但我需要在运行时更新信任库(例如,当调用 @getmapping 端点时)。

具体来说,我需要在不停止/重新启动应用程序的情况下将新证书添加到 TrustStore。 所以我将不得不以某种方式修改我的应用程序的内存信任存储。

我该怎么做?

我尝试动态添加一个 bean,它将一个新的信任管理器添加到 SslContext,但这不起作用。

@GetMapping("/register")
public String Register() throws Exception {
    ConfigurableApplicationContext configContext = (ConfigurableApplicationContext) appContext;
    ConfigurableListableBeanFactory beanRegistry = configContext.getBeanFactory();
    SSLContext sslContext = getSSLContext();
    beanRegistry.registerSingleton("sslContext", sslContext);
    return "okay";
}


public  SSLContext getSSLContext() throws Exception {
    TrustManager[] trustManagers = new TrustManager[] {
            new ReloadableX509TrustManager(truststoreNewPath)
    };
    SSLContext sslContext = SSLContext.getInstance("SSL");
    sslContext.init(null, trustManagers, null);
    SSLContext.setDefault(sslContext);
    return sslContext;
}

我还尝试将上述getSSLContext() 作为@bean 调用,但也不起作用。

我目前的解决方案是基于这些链接,这些链接适用于 Java,但我不确定如何在我的 Spring 应用程序中实现它们。

我找到了一个解决方案,它准确描述了如何拥有动态信任库,但我无法弄清楚如何在运行时重新加载信任库。例如,当调用 GET 端点时。

Client Certificate authentication without local truststore 我有一个证书列表,我只需要知道如何调用ReloadableX509TrustManageraddCertificates() 方法。

【问题讨论】:

  • 但这不起作用因为...? 这也不起作用。因为...(之二)??
  • @Antoniossss 我真的不知道。
  • 您发布的文章似乎涵盖了该主题。 ReloadableX509TrustManager 是成为 bean 并使用它来创建像上面这样的上下文的完美候选人
  • @Antoniossss 我真的不知道在此之后如何进行。我尝试了很多东西
  • @Antoniossss 你能告诉我怎么做吗?

标签: java spring spring-boot ssl truststore


【解决方案1】:

首先,将您的 ReloadableX509TrustManager 设为托管 bean - 例如使用 @Component 进行注释

@Component
class ReloadableX509TrustManager 
    implements X509TrustManager {
.....
    public ReloadableX509TrustManager(@Value("someValueFromAppConfig")String tspath){....}
.....

其次,在你的控制器中使用它而不是创建一个新的例如

@GetMapping("/register")
public String Register() throws Exception {
    ConfigurableApplicationContext configContext = (ConfigurableApplicationContext) appContext;
    ConfigurableListableBeanFactory beanRegistry = configContext.getBeanFactory();
    SSLContext sslContext = getSSLContext();
    beanRegistry.registerSingleton("sslContext", sslContext);
    return "okay";
}

@Autowired private ReloadableX509TrustManager reloadableManager;
public  SSLContext getSSLContext() throws Exception {
    TrustManager[] trustManagers = new TrustManager[] {
            reloadableManager
    };
    SSLContext sslContext = SSLContext.getInstance("SSL");
    sslContext.init(null, trustManagers, null);
    SSLContext.setDefault(sslContext);
    return sslContext;
}

第三,按照文章了解如何“重新加载”该信任管理器。可以通过将大多数方法更改为 package protected 并从某种证书服务调用它来完成 - 或者将其公开并直接调用。选择权在你。

【讨论】:

  • 我会试试这个并回复你。
  • 我试过你上面的方法。它运行没有错误,但 Spring 信任存储仍未更新?我认为当我们注册新的 bean “SSLContext”时,它不会被应用程序接收到
  • 我想我正在这样做,我在 getSSLContext() 方法的开头调用了reloadableManager.reloadTrustManager();
  • 为了让这个解决方案发挥作用,spring 应该首先公开一个 SslContext bean,对吧?
  • 使 ReloadableManager 原型或请求作用域可以提供帮助
【解决方案2】:

更新 2:

我当前的解决方案基于这些链接,这些链接适用于 Java,但 我不确定如何在我的 Spring 应用程序中实现它们。

https://jcalcote.wordpress.com/2010/06/22/managing-a-dynamic-java-trust-store/

Spring 与否,您的主要目标不需要对广义上的应用技术进行限制,重要的是您的应用程序(Client)将能够在运行时随时将新证书加载到客户端中,并即时进行更改。 假设要实现上面所说的,我们可以在我们的一个类 (TrustManager) 中实现我们的逻辑,实现 X509TrustManager

在客户端中实现信任管理器

这里的目标是告诉运行时您想使用这个新类来验证证书。我们必须用我们的TrustManager 实例化一个新的SSLContext 并使用它来指定一个新的SSLSocketFactory

try {
    String trustStorePath = "path to a truststore that you have";
    String trustStorePassword = "password of trustStore";
    String defaultTrustStore = "path to default truststore";

    // Initialize the new trustManager with the default trust store
    TrustManager trustManager = new TrustManager(defaultTrustStore);

    // Load the new Keystore and decrypt it
    KeyStore ks = KeyStore.getInstance("JKS");
    ks.load(new FileInputStream(trustStorePath), trustStorePassword.toCharArray());

    // Add all of the certficates in the truststore and add them to the trust manager
    Enumeration<String> enumerator = ks.aliases();
    ArrayList<Certificate> certs = new ArrayList<>();
    while (enumerator.hasMoreElements()) {
        String currentAlias = enumerator.nextElement();
        certs.add(ks.getCertificate(currentAlias));
    }
    trustManager.addCertificates(certs);

    // Initialize the SSLContext and add it to the client conduit.
    SSLContext sc = SSLContext.getInstance("SSL");
    sc.init(null, new TrustManager[] {trustManager}, null);

    // Set the new TrustManager in the client.
    HTTPConduit httpConduit = (HTTPConduit) ClientProxy.getClient(service).getConduit();
    TLSClientParameters tlsCP = new TLSClientParameters(); 
    httpConduit.setTlsClientParameters(tlsCP);
    tlsCP.setSSLSocketFactory(sc.getSocketFactory());
} catch (Exception e) {
    e.printStackTrace();
}

如果你需要参考,here 你会找到它。

【讨论】:

  • 我不想在客户端中实现信任管理器。我想在运行时修改/更新 Spring 的 tomcat 服务器的信任管理器。
  • 一旦您有了信任管理器,是什么阻止您配置 Spring 以使用该信任管理器?
【解决方案3】:

我也遇到了同样的情况,我必须即时更新我的​​ Spring Boot 应用程序的 TrustManager。

  1. 我将证书上传到我的默认 cacert 文件(即默认 SSL 上下文 TrustManager) - 这将负责通过默认 SSLContext 建立的连接。
  2. 但我也有两个不同的 REST 模板 Bean,一个用于建立 1way/Mutual SSL 连接,另一个用于 2Way SSL 连接。由于这些 Bean 在启动期间已初始化
    • 更改默认 TrustManager 不会更新 REST 模板的 TrustManager。
    • 为此,我使用新的请求工厂动态更新了 RestTemplate Bean,从而解决了我的问题。

【讨论】:

    【解决方案4】:

    我之前已经尝试过使用 Spring Boot 和嵌入式 Tomcat 服务器,但没有任何运气。我确实找到了一种使用 Spring Boot 和 Jetty 服务器来完成它的方法。 Tomcat 只能配置属性。该库不公开底层的 sslcontext,因此更难操作,对于这个用例,我找不到 tomcat 的方法。 Jetty 允许传递您可以操作的 sslcontext。

    所以我创建了一个自定义的 TrustManager,它只是实际 TrustManager 的包装器。这个自定义的 TrustManager 可以将传入的方法调用委托给实际的 trustmanager。但是,此自定义信任管理器具有随时更改实际信任管理器的附加功能,例如当您想要更新信任库时。

    我已经为这个用例创建了一个库,所以它可能很方便,但你可以在没有库的情况下完成同样的工作。这是没有使用附加库作为选项 1 的原版代码:

    选项 1

    HotSwappableX509ExtendedTrustManager

    import javax.net.ssl.SSLEngine;
    import javax.net.ssl.X509ExtendedTrustManager;
    import java.net.Socket;
    import java.security.cert.CertificateException;
    import java.security.cert.X509Certificate;
    import java.util.Arrays;
    import java.util.Objects;
    
    public class HotSwappableX509ExtendedTrustManager extends X509ExtendedTrustManager {
    
        private X509ExtendedTrustManager trustManager;
    
        public HotSwappableX509ExtendedTrustManager(X509ExtendedTrustManager trustManager) {
            this.trustManager = Objects.requireNonNull(trustManager);
        }
    
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            trustManager.checkClientTrusted(chain, authType);
        }
    
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
            trustManager.checkClientTrusted(chain, authType, socket);
        }
    
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException {
            trustManager.checkClientTrusted(chain, authType, sslEngine);
        }
    
        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            trustManager.checkServerTrusted(chain, authType);
        }
    
        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
            trustManager.checkServerTrusted(chain, authType, socket);
        }
    
        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) throws CertificateException {
            trustManager.checkServerTrusted(chain, authType, sslEngine);
        }
    
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            X509Certificate[] acceptedIssuers = trustManager.getAcceptedIssuers();
            return Arrays.copyOf(acceptedIssuers, acceptedIssuers.length);
        }
    
        public void setTrustManager(X509ExtendedTrustManager trustManager) {
            this.trustManager = Objects.requireNonNull(trustManager);
        }
    
    }
    

    用法

    // Your key and trust manager created from your truststore file
    X509ExtendedTrustManager aTrustManager = ...
    
    // Wrapping it into your hot swappable trust manager
    HotSwappableX509ExtendedTrustManager swappableTrustManager = new HotSwappableX509ExtendedTrustManager(aTrustManager);
    
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, new TrustManager[]{ swappableTrustManager }, null)
    
    // Give the sslContext instance to your server
    
    // After some time change the TrustManager with the following snippet:
    X509ExtendedTrustManager anotherTrustManager = ... // Created from the updated truststore
    
    // Set your new trust manager into your swappable managers
    swappableTrustManager.setTrustManager(anotherTrustManager)
    

    选项 2

    如果您不想将自定义 (HotSwappableTrustManager) 代码添加到您的代码库中,您也可以使用我的库:

    <dependency>
        <groupId>io.github.hakky54</groupId>
        <artifactId>sslcontext-kickstart-for-jetty</artifactId>
        <version>7.0.2</version>
    </dependency>
    

    用法

    SSLFactory sslFactory = SSLFactory.builder()
              .withSwappableIdentityMaterial()
              .withIdentityMaterial("identity.jks", "password".toCharArray())
              .withSwappableTrustMaterial()
              .withTrustMaterial("truststore.jks", "password".toCharArray())
              .build();
    
    SSLContext sslContext = sslFactory.getSslContext();
              
    // Give the sslContext instance to your server or client
    // After some time change the KeyManager and TrustManager with the following snippet:
    
    // swap identity and trust materials and reuse existing http client
    KeyManagerUtils.swapKeyManager(sslFactory.getKeyManager().get(), anotherKeyManager);
    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 KeyManager/TrustManager
    SSLSessionUtils.invalidateCaches(sslContext);
    

    这个项目正好演示了这个用例,见这里:GitHub - Instant server ssl reloading。该项目演示了两种更新 ssl 材料的方法:

    • 通过休息控制器:AdminController
      • 这使得从外部 api 调用更新 ssl 材料成为可能
    • 通过文件监听器:FileBasedSslUpdateService
      • 这使得每次更新信任存储文件时都可以刷新信任材料

    你可以在这里找到图书馆:GitHub - SSLContext Kickstart

    以下是完整的服务器配置:

    这个例子可以很容易地用于选项 1 和选项 2

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jetty</artifactId>
    </dependency>
    

    SSLConfig 类

    @Configuration
    public class SSLConfig {
    
        @Bean
        public SSLFactory sslFactory(ApplicationProperty applicationProperty) {
            return SSLFactory.builder()
                    .withSwappableIdentityMaterial()
                    .withIdentityMaterial(keyStorePath, keyStorePassword)
                    .withSwappableTrustMaterial()
                    .withTrustMaterial(trustStorePath, trustStorePassword)
                    .withNeedClientAuthentication(true)
                    .build();
        }
    
        @Bean
        public SslContextFactory.Server sslContextFactory(SSLFactory sslFactory) {
            return JettySslUtils.forServer(sslFactory);
        }
    
        @Bean
        public X509ExtendedKeyManager keyManager(SSLFactory sslFactory) {
            return sslFactory.getKeyManager().orElseThrow();
        }
    
        @Bean
        public X509ExtendedTrustManager trustManager(SSLFactory sslFactory) {
            return sslFactory.getTrustManager().orElseThrow();
        }
    
        @Bean
        public SSLSessionContext serverSessionContext(SSLFactory sslFactory) {
            return sslFactory.getSslContext().getServerSessionContext();
        }
    
    }
    

    服务器配置类

    @Configuration
    public class ServerConfig {
    
        @Bean
        public ConfigurableServletWebServerFactory webServerFactory(SslContextFactory.Server sslContextFactory) {
            JettyServletWebServerFactory factory = new JettyServletWebServerFactory();
        
            JettyServerCustomizer jettyServerCustomizer = server -> {
                ServerConnector serverConnector = new ServerConnector(server, sslContextFactory);
                serverConnector.setPort(8443);
                server.setConnectors(new Connector[]{serverConnector});
            };
            factory.setServerCustomizers(Collections.singletonList(jettyServerCustomizer));
        
            return factory;
        }
    
    }
    

    以及 Swappable ssl 服务

    @Service
    public class SwappableSslService {
    
        private final SSLSessionContext sslSessionContext;
        private final X509ExtendedKeyManager swappableKeyManager;
        private final X509ExtendedTrustManager swappableTrustManager;
    
        public SwappableSslService(SSLSessionContext sslSessionContext,
                                   X509ExtendedKeyManager swappableKeyManager,
                                   X509ExtendedTrustManager swappableTrustManager) {
    
            this.sslSessionContext = sslSessionContext;
            this.swappableKeyManager = swappableKeyManager;
            this.swappableTrustManager = swappableTrustManager;
        }
    
        public void updateSslMaterials(X509ExtendedKeyManager keyManager, X509ExtendedTrustManager trustManager) {
            KeyManagerUtils.swapKeyManager(swappableKeyManager, keyManager);
            TrustManagerUtils.swapTrustManager(swappableTrustManager, trustManager);
            SSLSessionUtils.invalidateCaches(sslSessionContext);
        }
    
    }
    

    现在您可以使用以下两个类中的任何一个轻松地动态更新信任库:AdminControllerFileBasedSslUpdateService

    【讨论】: