Configure HostnameVerifier with reactor netty for spring-webflux WebClient
Asked Answered
G

3

19

I'm trying to configure spring-webflux WebClient (with reactor netty under the hood) with ssl and client hostname verification. I'm provided with javax.net.ssl.SSLContext, HostnameVerifier and a list of trusted hostnames (as string list).

So far I've configured WebClient with my SSLContext, but I can't find a way to configure hostname verification.

To state my problem: I have a set of trusted services hostnames (String list) and a HostnameVerifier. I want to configure my WebClient with it.

Is there a possibility to do it with the javax.net.ssl.HostnameVerifier? Is there an alternative approach in reactor netty?

This is what I've got so far:

WebClient.builder()
  .clientConnector(
    new ReactorClientHttpConnector(
      opt -> opt.sslContext(new JdkSslContext(mySSLContext, 
                      true, ClientAuth.OPTIONAL))))
  .build();
Granulation answered 26/7, 2018 at 15:5 Comment(0)
C
5

You should provide a valid certificate authority certificate (trustManager()) and optionally user cert with private key and private key password for authorization (keyManager()). Your service SSL certificate should be signed by the same CA that you've defined in trustManager().

Hostnames are being verified automatically with service hostname. If there's not match java.security.cert.CertificateException: No subject alternative names present exception will be thrown. Actually I can't find a way to omit hostnames verification (without omitting whole SSL certificate verification by using .trustManager(InsecureTrustManagerFactory.INSTANCE)).

I've tested this solution locally. My webservice is running on my local machine but its SSL certificate contains only DNS name, not the IP address. So for my debug purposes I've added entry to hosts file and mapped my service IP to proper DNS name.

SslContext sslContext = SslContextBuilder
        .forClient()
        .trustManager(new FileInputStream(caPath))
        .keyManager(
                new FileInputStream(userCertPath),
                new FileInputStream(userPrivateKeyPath),
                userPrivateKeyPassword
        )
        .build();

HttpClient httpClient = HttpClient.create()
        .secure(t -> t.sslContext(sslContext));

WebClient webClient = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();
Chelseachelsey answered 23/5, 2019 at 9:49 Comment(0)
R
1

I've tried following solution with Netty HttpClient and it worked as well (Disabling the hostname verification by using a custom hostname matcher)

public HttpClient getHttpClient(HttpClientProperties properties){

            // configure pool resources
            HttpClientProperties.Pool pool = properties.getPool();

            ConnectionProvider connectionProvider;
            if (pool.getType() == DISABLED) {
                connectionProvider = ConnectionProvider.newConnection();
            }
            else if (pool.getType() == FIXED) {
                connectionProvider = ConnectionProvider.fixed(pool.getName(),
                        pool.getMaxConnections(), pool.getAcquireTimeout());
            }
            else {
                connectionProvider = ConnectionProvider.elastic(pool.getName());
            }

            HttpClient httpClient = HttpClient.create(connectionProvider)
                    .tcpConfiguration(tcpClient -> {

                        if (properties.getConnectTimeout() != null) {
                            tcpClient = tcpClient.option(
                                    ChannelOption.CONNECT_TIMEOUT_MILLIS,
                                    properties.getConnectTimeout());
                        }

                        // configure proxy if proxy host is set.
                        HttpClientProperties.Proxy proxy = properties.getProxy();

                        if (StringUtils.hasText(proxy.getHost())) {

                            tcpClient = tcpClient.proxy(proxySpec -> {
                                ProxyProvider.Builder builder = proxySpec
                                        .type(ProxyProvider.Proxy.HTTP)
                                        .host(proxy.getHost());

                                PropertyMapper map = PropertyMapper.get();

                                map.from(proxy::getPort).whenNonNull().to(builder::port);
                                map.from(proxy::getUsername).whenHasText()
                                        .to(builder::username);
                                map.from(proxy::getPassword).whenHasText()
                                        .to(password -> builder.password(s -> password));
                                map.from(proxy::getNonProxyHostsPattern).whenHasText()
                                        .to(builder::nonProxyHosts);
                            });
                        }
                        return tcpClient;
                    });

            HttpClientProperties.Ssl ssl = properties.getSsl();
            if (ssl.getTrustedX509CertificatesForTrustManager().length > 0
                    || ssl.isUseInsecureTrustManager()) {
                httpClient = httpClient.secure(sslContextSpec -> {
                    // configure ssl
                    SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();

                    X509Certificate[] trustedX509Certificates = ssl
                            .getTrustedX509CertificatesForTrustManager();
                    if (trustedX509Certificates.length > 0) {
                        sslContextBuilder.trustManager(trustedX509Certificates);
                    }
                    else if (ssl.isUseInsecureTrustManager()) {
                        sslContextBuilder
                                .trustManager(InsecureTrustManagerFactory.INSTANCE);
                    }


                    sslContextSpec.sslContext(sslContextBuilder)
                            .defaultConfiguration(ssl.getDefaultConfigurationType())
                            .handshakeTimeout(ssl.getHandshakeTimeout())
                            .closeNotifyFlushTimeout(ssl.getCloseNotifyFlushTimeout())
                            .closeNotifyReadTimeout(ssl.getCloseNotifyReadTimeout())
                            .handlerConfigurator(
                                    (handler)->{
                                        SSLEngine engine = handler.engine();
                                        //engine.setNeedClientAuth(true);
                                        SSLParameters params = new SSLParameters();
                                        List<SNIMatcher> matchers = new LinkedList<>();
                                        SNIMatcher matcher = new SNIMatcher(0) {

                                            @Override
                                            public boolean matches(SNIServerName serverName) {
                                                return true;
                                            }
                                        };
                                        matchers.add(matcher);
                                        params.setSNIMatchers(matchers);
                                        engine.setSSLParameters(params);
                                    }
                            )
                    ;
                });
            }

            return httpClient;

        }

It uses nettys handlerConfigurator to configure SSLEngine and use it with custom matchers

Reedy answered 29/5, 2019 at 20:58 Comment(1)
I find this answer misleading as it mentions a solution for custom SNI (Server Name Indication) whereas the question seems more related to customizing SAN (Subject Alternative Name) verification (i.e. server name in certificate compared to real hostname used).Hippogriff
D
0

You can customize your WebClient

WebClient.builder()
    .clientConnector(new ReactorClientHttpConnector(httpClient.create()
          .secure(contextSpec -> contextSpec.sslContext(sslContext)
                 .handlerConfigurator(handler -> {
                       SSLEngine engine = handler.engine();
                       SSLParameters params = engine.getSSLParameters ();
                       params.setServerNames(List.of(new SNIHostName(<hostname-to-match-certificate-CN>)));
                       //params.setEndpointIdentificationAlgorithm(null); if you want disable hostname verification, but I would not recommend that 
                       engine.setSSLParameters(params);
              });
         )))
    .build();

providing the hostname in such a way will cause it to be verified with what we have in the certificate and then the url we are hitting can contain, for example, a clean IP in the host section

Dyadic answered 26/2 at 14:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.