The Java java.net.http.HttpClient is one of the most impacting addition in "recent" Java releases (Java 11 originally but it becomes usable a bit after). However its API is not 1-1 with the alternatives we are used too (Apache HttpClient). Let see the common "disable the SSL hostname validation" need and how to solve it with this new client.

The need

It is quite common - in particular in Kubernetes - to request a service with a name different from its certificate - due to domain name aliasing or client IP lookup.

This means that, by default where validation is enabled for security reason, a call to a HTTPS endpoint will often end up with something like:

javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

Long story short, this error means the client and server certificates didn’t match so the call is rejected.

Let’s now see how to move forward to get a valid call at the end.

Configure it right, enable the validation

Before showing how to ignore the validation, let see how to enable it - which should be your default.

To enable properly a HTTPS call two main steps are needed:

  1. Configure the certificate to use (if you use mTLS you need to add the key too but this is another post story),

  2. Enable the certificate name validation.

Last step is the default so we’ll not detail it much more in this part.

At that stage you probably think "Hey, let just set this property, I don’t care about security in dev" - indeed nobody does it in production environment ;).

But the story is a bit more complex, if you only have standard HTTPS clients this will work for sure, however, if you have mTLS clients you MUST keep the validation enabled so using this system property will just break your clients and you’ll not be able to do the calls anymore even if you set the certificate and key property in the SSLContext.

So long story short, avoid the usage of the system property alone and prefer explicit configuration to have a proper validation in all cases, in particular in libraries.

To enable the validation, you have to create SSLParameters and set endpointIdentificationAlgorithm to HTTPS.

Show me the code

final var sslParameters = new SSLParameters();
sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); (1)

// tip: don't forget to close the client on java >= 21
final var client = HttpClient.newBuilder()
        .sslParameters(sslParameters) (2)
        .sslContext(createSSLContext()) (3)
        .build();
1 We create SSLParameters with the identification algorithm to HTTPS to force the validations,
2 We wire the parameters we just created to the client using the HttpClient builder API,
3 We wire the certificate using a SSLContext with the valid TrustManager instances.

To create the SSLContext there are multiple ways but here is one:

final var pems = "...your X509 certificates in PEM format...";
try {
    (1)
    final var x509Factory = CertificateFactory.getInstance("X.509");
    final Collection<Certificate> certs;
    try (final var in = new ByteArrayInputStream(pems.getBytes(UTF_8))) {
        certs = new ArrayList<>(x509Factory.generateCertificates(in));
    }

    (2)
    final var pwd = "ignored!".toCharArray();
    final var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    keyStore.load(null, pwd);

    (3)
    final var counter = new AtomicInteger();
    certs.forEach(c -> {
        try {
            keyStore.setCertificateEntry(
                    c instanceof X509Certificate x
                            ? getNameOf(x).orElseGet(() -> "ca-" + counter.incrementAndGet())
                            : ("ca-" + counter.incrementAndGet()),
                    c);
        } catch (final KeyStoreException e) {
            throw new IllegalArgumentException(e);
        }
    });

    (4)
    final var trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
    trustManagerFactory.init(keyStore);

    (5)
    final var sslContext = SSLContext.getInstance("TLS");
    sslContext.init(km, trustManagerFactory.getTrustManagers(), new SecureRandom());

    return sslContext;
} catch (final GeneralSecurityException | IOException e) {
    throw new IllegalStateException(e);
}
1 Load PEM certificates - can be a full chain if PEM are sorted - from some configuration (or hardcoded but I don’t recommend it since certificates must be rotated regularly),
2 Create a KeyStore which will be used to store the certificates and initialize the trust manager factory,
3 Initialize the empty in memory KeyStore with the loaded certificates,
4 Create a TrustManagerFactory from the KeyStore,
5 Create a SSLContext from the TrustManager instances coming from the factory we just created.
if you need the getNameOf method, it can be as simple as ofNullable(x.getSubjectX500Principal()).map(p → p.getName(CANONICAL)) with x the X509Certificate.

Disable the HTTPS validation

Now we know how to enable the validation property, let’s disable it.

The first option is to set -Djdk.internal.httpclient.disableHostnameVerification=true system property on your JVM and be it.

Note that this system property basically disables to hostname verification activation, not the hostname verification itself. What it means is that if you want to configure per client the hostname verification you must set it even if you enable it for some client. In other words, it enables you to configure per client the validation activation or not whereas not setting it will force the hostname validation in all cases.

If using the global property alone does not match your application because you are in one of these cases:

  • You don’t want to disable it for all calls - only for internal calls but keep it enabled for external ones for example,

  • You have mTLS calls too in your application,

then you can configure every client to validate or not the SSL constraints.

this is not the core topic of this post but you can also specify the cipher suites and protocols you want to support per client using SSLParameters.

The key there is to ensure to have one HttpClient instance per "target" - DNS often - but you can share resources like the threads you associate to the NIO processing executor() on the HttpClient.Builder.

To do that, we’ll follow the same steps than to properly setup our client but we’ll:

  1. Use an unsafe SSLContext without any certificate - indeed if you set the same SSLContext than in previous part it will work too but it is pointless since it is then ignored for the validation,

  2. Set an empty endpointIdentificationAlgorithm which will disable the validation.

But we’ll also ensure our JVM has the system property -Djdk.internal.httpclient.disableHostnameVerification=true set.

You can find a ton of example over the net - or in IA prompts - but here is an "unsafe" (empty actually) SSLContext bootstrap code:

final var sslContext = SSLContext.getInstance("TLS");
sslContext.init(
    null,
    new TrustManager[] {
      new X509TrustManager() { (1)
        @Override
        public void checkClientTrusted(final X509Certificate[] chain, final String authType) {
            // no-op
        }

        @Override
        public void checkServerTrusted(final X509Certificate[] chain, final String authType) {
            // no-op
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }
      }
    },
    new SecureRandom());
return sslContext;
1 We set a single TrustManager which basically does nothing.

Then instead of doing sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); we just do sslParameters.setEndpointIdentificationAlgorithm("");.

And here we are! We have an unsafe "per instance" HttpClient instance which means another instance can keep SSL validations (using "SSL" algorithm and a valid TrustManager).

If you just use an "unsafe" SSLContext and not disable the hostname validation (endpointIdentificationAlgorithm) then you’ll get this kind of error:

javax.net.ssl.SSLHandshakeException: No name matching localhost found

Fusion HttpClient case

At Yupiik, we use Fusion HttpClient wrapper, a.k.a. ExtendedHttpClient which enables to wrap a standard HttpClient, reuse the exact same API for library compatibility but add listeners to the requests/responses - to log them for example.

With this API, passing SSLParameters is as easy as tuning the HttpClient and passing it to the ExtendedHttpClientConfiguration:

// note: don't forget to close it
final var client = new ExtendedHttpClient(new ExtendedHttpClientConfiguration()
    .setDelegate(newHttpClientWithSslParameters());

Conclusion

Even if the API is not as straight forward than a plain .disableHostnameValidation() on HttpClient.Builder, the new Java HttpClient should really be seen as a replacement of most HTTP client in Java ecosystem (it replaces very well Apache HttpClient, OkHttp, async-http-client, …​).

While disabling host name validation is not the best option, it stays very convenient in dev environments to limit the work around SSL but keeping it enabled.

So while this post shows you how to disable it, please ensure to have a toggle to disable it and keep it on by default to avoid to get such setup in production by error.

From the same author:

In the same category: