test me

Site Search:

How spring reactive webclient handle self-signed certificate

Have your spring reactive webclient ever get error:

reactor.core.Exceptions$ReactiveException: javax.net.ssl.SSLHandshakeException: General SSLEngine problem
...
Caused by: java.security.cert.CertificateException: No name matching my.host.name.for.post found
              at sun.security.util.HostnameChecker.matchDNS(HostnameChecker.java:231)

and you have to write code such as the following just to pass your QA integration test?

public Mono<ResponseWrapper> execute(Config conf, MyRequest request) throws SSLException {
    return org.spingframework.web.reactive.function.client.WebClient
                .builder()
                //.clientConnector(getHttpConnector(conf))
                .clientConnector(getQAOnlyConnector(conf)) //TODO: remove this!!!!Don't forget!!!!
                .defaultHeaders(getHeaderConsumer())
                .build()
                .post()
                .uri(getUri())
                .body(BodyInserters.fromValue(request))
                .exchange()
                .flatMap(clientResp -> clientResp.bodyToMono(getResponseClass()))
                .map(respObj -> {
                        ResponseWrapper wrapper = new ResponseWrapper();
                        wrapper.setObj(respObj);
                        return wrapper;
                 });
}

private ReactorClientHttpConnector getQAOnlyConnector(Config conf) throws SSLException {
    SslContext ssl = io.netty.handler.ssol.SslContextBuilder
                   .forClient()
                   .trustManager(io.netty.handler.sso.util.InsecureTrustManagerFactory.INSTANCE)
                   .build();
    HttpClient client = reactor.netty.http.client.HttpClient.create().secure(sslContextSpec -> sslContextSpec.sslContext(ssl);
    return new ReactorClientHttpConnector(client);
}


Well, it is time to get to the bottom of it.

First of all, we need to read the error message, "General SSLEngine problem" means the problem happened during certificate verification process. This exact problem is the hostname.

Let's see what the mismatch is. To rule out the java code error, let's use postman to double check.

The information you need to collect from the java code is the url, headers, post body.

Say, if the postman do get result back, something is "wrong" with your code. Why postman can success but spring reactive http client fails. The reason is, postman by default, don't verify server certificate, however, if you post to a url with https, the spring reactive webclient will try to verify the server's certificate. Some issue is found, and the spring webclient throws.

Ok, let's look into the issue further. If you don't already know, there are two clickable red links at right side above the main request window. One link is "Cookies", another link is "Code". Click that "Code" link, a curl command almost equivalent to the postman request will popup.

Use that curl command, we can look into the issue further.

The curl command could looks like the following:

curl --location --request POST 'https://my.host.name.for.post/someendpoint' \
--header 'Authorization: agiberishstring' \
--header 'Content-Type: txt/xml' \
--data-raw '<Request><id>123</id></Request>'

The curl will complain
curl: (60) SSL certificate problem: self signed certificate
...
If the default bundle file isn't adequate, you can specify an alternate file using the --cacert option.
...
If you'd like to turn off curl's verification of the certificate, use the -k (or --insecure) option.

The message says it all. The QA server is using a self-signed certificate, no surprise the certificate itself has discrepancies.

Let's reproduce what the postman and the spring reactive client with InsecureTrustManagerFactory's behavior.

The following curl command is the equivalent to the above 2.

curl --insecure --location --request POST 'https://my.host.name.for.post/someendpoint' \
--header 'Authorization: agiberishstring' \
--header 'Content-Type: txt/xml' \
--data-raw '<Request><id>123</id></Request>'

The --insecure flag just tell curl command, don't bother to verify the server's certificate, we don't care its identity or authenticity, just return me the response.
The java code
.trustManager(io.netty.handler.sso.util.InsecureTrustManagerFactory.INSTANCE)
basically convey same message to the spring reactive webclient.

Now let's figure out what happens if the http client (curl or postman or spring webclient) try to verify the server certificate. To do that. Let's supply the curl command the CA certificates.

The reason curl command or spring reactive client want a cacert file is because the CA or (Certificate Authority) file is the root of the trust chain. We may don't know the host's public key, but that public key is signed by someone else, whose public key we trust. The cacert file stores the public keys for all the root CAs we trust.

The trust is established like this: the server's public key is signed by an intermediate CA, whose public key is again signed by another CA,... like a chain..., finally, the chain end at root CA, whose public key is a self assigned certificate. The root CAs are so famous that everybody on the planet earth trust them. For example, root CAs such as verisign, bank of america, etc, they can act as public notary organization to verify other organizations by signing their public key. In other examples, CAs such as your father's company may not good as a public notary organization, but your company can trust your father's company's certificate as one of the root CAs.

Given the curl command a --cacert cacert.pem flag is to tell curl, cacert.pem has all the root CAs I trust, if the target host is eventually signed by any of them, move forward, otherwise stop.

In our case, since the QA server has a self-signed public key, nobody except itself verified its authenticity, we have 3 choices:
  •  trust that self-signed public key as one of the root CA, or 
  • don't trust that self-signed public key, reject it.
  • don't bother to verify the public key at all
Let's trust the self-signed certificate here, and use it as our trusted root CA file. 
To do that, first download CA cert from the server with openssl. The openssl can do that because the server's public key is self-signed, the root CA cert is the server's public key.

echo quit | openssl s_client -showcerts -connect my.host.name.for.post:443 > cacert.pem;

Then use the downloaded cacert.pem as root CA file to verify the server, so that we claim we (blindly) trust that server, want curl command to give it a try.

curl --cacert cacert.pem --location --request POST 'https://my.host.name.for.post/someendpoint' \
--header 'Authorization: agiberishstring' \
--header 'Content-Type: txt/xml' \
--data-raw '<Request><id>123</id></Request>'

This command will fail as the spring reactive webclient does.
curl: (51) SSL: certificate subject name 'localhost' does not match target host name 'my.host.name.for.post'

Great, that is the bottom of it. Even we (blindly) trust that self-signed public key, willing to give it a try, curl or spring webclient is throwing. The programs did a sanity check for the server's public key, find it is not even a correct one. The CN in the certificate is not matching the hostname in target url, which is definitely wrong, so the programs throw.

The operation team who deploy the server machine or VM generated a bad certificate, the host name set in the certificate is "localhost". They might have tested the ssh process on the server, everything is fine, because the test is done on "localhost"!

Let's approve this:

openssl x509 -noout -subject -in cacert.pem
subject= /C=CH/ST=Minst/L=Chill/O=MyOrg/OU=IT/CN=localhost

CN=localhost, but the hostname in our request url is my.host.name.for.post. That is what the spring webclient is complaining about!

If the command has output like this:
openssl x509 -noout -subject -in cacert.pem
subject= /C=CH/ST=Minst/L=Chill/O=MyOrg/OU=IT/CN=myorg.host.name.for.post

the shame is on us, we could have used the correct url in the curl command to make it work.

curl --cacert cacert.pem --location --request POST 'https://myorg.host.name.for.post/someendpoint' \
--header 'Authorization: agiberishstring' \
--header 'Content-Type: txt/xml' \
--data-raw '<Request><id>123</id></Request>'

But for CN=localhost, there is nothing we can do, the server side has to fix it...

Instead of fixing the java code, the right thing to do is to send a polite message to the server maintainer, kindly mention their self-signed certificate has a small problem, please fix it so that we don't have to work around it.

No comments:

Post a Comment