5

I have 2 services A & B which should communicate over with each other over HTTPS. I have enabled TLS using server.ssl.* properties of Spring boot for both the applications. I am using WebClient for the communication where Service A will call B using the webclient and B would send a response back to A. For communication over TLS my understanding is that the Webclient would need the truststore data which would have the certiifcates of the service that is being called i.e. Service B.

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;

import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManagerFactory;

import org.springframework.boot.web.server.Ssl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;


import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import lombok.extern.slf4j.Slf4j;
import reactor.netty.http.client.HttpClient;


@Slf4j
@Configuration
public class WebClientConfig {

    private final Ssl ssl;

    public WebClientConfig(final Ssl ssl) {
        this.ssl = ssl;
    }

    @Bean
    public WebClient createWebClient() throws Exception {

        if (ssl.isEnabled()) {

            return buildSslEnabledWebClient();

        }

        return WebClient.builder().build();
    }

    private WebClient buildSslEnabledWebClient() throws Exception {

        final String trustStorePath = ssl.getTrustStore();
        final String trustStorePassword = ssl.getTrustStorePassword();
        final KeyStore trustStore = createKeyStore(trustStorePath, trustStorePassword);

        try {

            final TrustManagerFactory trustManager = TrustManagerFactory
                    .getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManager.init(trustStore);

            final SslContext sslContext = SslContextBuilder.forClient().trustManager(trustManager)
                    .build();

            final HttpClient httpClient = HttpClient.create().secure(ssl -> {
                ssl.sslContext(sslContext);
            });

            return WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).build();

        } catch (NoSuchAlgorithmException | KeyStoreException | SSLException e) {
            log.error("Could not initialize Webclient with the trustore data", e);
            throw e;
        }
    }

    private static KeyStore createKeyStore(final String keyStoreLocation, final String keyStorePassword) {

        try (FileInputStream fis = new FileInputStream(keyStoreLocation)) {

            final KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());

            ks.load(fis, keyStorePassword.toCharArray());

            return ks;

        } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
            throw new IllegalArgumentException(e);
        }
    }
}

I have created self signed certificates using the keytool and this seems to be working. I could make a call from A to B and the response back.

However I somehow feel that the setup is not complete and hence my questions are :

  1. I am missing something?
  2. Is this the correct way to do it?
  3. Will this is work in production with real certificates?

UPDATE : Fixed the code, realized the mistake after reading a below answer. I am actually using the updated code.

humbleCoder
  • 667
  • 14
  • 27

1 Answers1

2

It looks OK to me, you are properly loading the keystore file and also properly initializing the trustmanagerfactory with the keystore object.

However you are not using the trustmanagerfactory for configuring your httpclient. Instead of that you are again loading the keystore from file while passing it as an object to the method SslContextBuilder.forClient().trustManager. However this will not work because this method expect a file containing a list of trusted certificates in a PEM format, while you are passing a keystore. The javadoc contains the following documentation:

public SslContextBuilder trustManager(java.io.File trustCertCollectionFile)

Trusted certificates for verifying the remote endpoint's certificate. The file should contain an X.509 certificate collection in PEM format. null uses the system default.

If you are loading a PEM formatted file, than the example what you have posted would be working. If you are loading a keystore file than I would advise the following snippet:

final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);

final SslContext sslContext = SslContextBuilder.forClient()
    .trustManager(trustManagerFactory)
    .build();

final HttpClient httpClient = HttpClient.create()
    .secure(ssl -> {
        ssl.sslContext(sslContext);
    });
Hakan54
  • 3,121
  • 1
  • 23
  • 37
  • Hi thanks for pointing out the mistake. I updated the code with the code I am actually using which is same as what you have pointed out. The code that I had pasted was from an earlier trial and error. And yes this only works for PEM format. Is there any other way to make it work for other formats? – humbleCoder Aug 10 '20 at 09:33
  • You can use the trustmanagerfactory which you already created to create the sslcontext. It has multiple overloaded methods where you can pass different kinds of objects and one of them is the trustmanagerfactory. Please have a look at the second code snippet. It calls the trustmanager method with the trustmanagerfactory object. That should do the trick for your use case. Please let me now at which point you got stuck – Hakan54 Aug 10 '20 at 10:33