Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to configure a Reactive WebClient to use 2-way TLS?

I'm trying to configure a reactive WebClient to use 2-way TLS. I used this answer as a reference. (The one using a WebClientCustomizer, not the one using an InsecureTrustManager).

I double-checked the keystores and truststores on both client and server side, but the server sends back an error saying that the client is not presenting any certificate:

  @Bean
  WebClientCustomizer configureWebclient(@Value("${server.ssl.trust-store}") String trustStorePath, @Value("${server.ssl.trust-store-password}") String trustStorePass,
      @Value("${server.ssl.key-store}") String keyStorePath, @Value("${server.ssl.key-store-password}") String keyStorePass, @Value("${server.ssl.key-alias}") String keyAlias) {

    return new WebClientCustomizer() {

      @Override
      public void customize(Builder webClientBuilder) {
        SslContext sslContext;
        try {
          KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
          trustStore.load(new FileInputStream(ResourceUtils.getFile(trustStorePath)), trustStorePass.toCharArray());

          List<Certificate> certificateCollcetion = Collections.list(trustStore.aliases()).stream().filter(t -> {
            try {
              return trustStore.isCertificateEntry(t);
            } catch (KeyStoreException e1) {
              throw new RuntimeException("Error reading truststore", e1);
            }
          }).map(t -> {
            try {
              return trustStore.getCertificate(t);
            } catch (KeyStoreException e2) {
              throw new RuntimeException("Error reading truststore", e2);
            }
          }).collect(Collectors.toList());

          KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
          keyStore.load(new FileInputStream(ResourceUtils.getFile(keyStorePath)), keyStorePass.toCharArray());
          sslContext = SslContextBuilder.forClient()
              .keyManager((PrivateKey) keyStore.getKey(keyAlias, keyStorePass.toCharArray()))
              .trustManager((X509Certificate[]) certificateCollcetion.toArray(new X509Certificate[certificateCollcetion.size()]))
              .build();
        } catch (Exception e) {
          log.error("Error creating web client", e);
          throw new RuntimeException(e);
        }
        ClientHttpConnector connector = new ReactorClientHttpConnector((opt) -> {
          opt.sslContext(sslContext);
        });
        webClientBuilder.clientConnector(connector);
      }
    };
  }

Can somebody please share insight on how to correctly configure a reactive WebClient to use 2-way TLS?

like image 698
Skywarp Avatar asked Jan 02 '23 14:01

Skywarp


1 Answers

For some reason the server would not accept the client certificate when the ssl context was built like this:

sslContext = SslContextBuilder.forClient()
          .keyManager((PrivateKey) keyStore.getKey(keyAlias, keyStorePass.toCharArray()))
          .trustManager((X509Certificate[]) certificateCollcetion.toArray(new X509Certificate[certificateCollcetion.size()]))
          .build();

To fix this, I had to initialize a KeyManagerFactory:

KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keyStore, keyStorePass.toCharArray());

Then I initialized the ssl context with the factory:

SslContext sslContext = SslContextBuilder.forClient()
                    .keyManager(keyManagerFactory)
                    .trustManager((X509Certificate[]) certificateCollection.toArray(new X509Certificate[certificateCollection.size()]))
                    .build();

After that, the server accepted the certificate and I could connect.

In summary, I used this cleaner solution that utilizes factories for both the key-store and the trust-store:

@Value("${server.ssl.trust-store}")
String trustStorePath;
@Value("${server.ssl.trust-store-password}")
String trustStorePass;
@Value("${server.ssl.key-store}")
String keyStorePath;
@Value("${server.ssl.key-store-password}")
String keyStorePass;

@Bean
public WebClient create2WayTLSWebClient() {

    ClientHttpConnector connector = new ReactorClientHttpConnector(
            options -> {
                options.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
                options.sslContext(get2WaySSLContext());
            }
    );

    return WebClient.builder()
            .clientConnector(connector)
            .build();

}

private SslContext get2WaySSLContext() {

    try {

        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(new FileInputStream(ResourceUtils.getFile(keyStorePath)), keyStorePass.toCharArray());

        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
        keyManagerFactory.init(keyStore, keyStorePass.toCharArray());

        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        trustStore.load(new FileInputStream(ResourceUtils.getFile(trustStorePath)), trustStorePass.toCharArray());

        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
        trustManagerFactory.init(trustStore);

        return SslContextBuilder.forClient()
                .keyManager(keyManagerFactory)
                .trustManager(trustManagerFactory)
                .build();

    } catch (Exception e) {
        logger.error("Error creating 2-Way TLS WebClient. Check key-store and trust-store.");
        e.printStackTrace();
    }

    return null;
}

Just a note, if you are using Spring 5.1 or newer, this specific implementation will not work as you can no longer pass HttpClientOptions to a ReactorClientHttpConnector. Use this link as a guide for that configuration. However the meat of the code in this answer should still be applicable to that sort of configuration.

like image 127
Skywarp Avatar answered Jan 04 '23 02:01

Skywarp