Security Top

I just announced the new Learn Spring Security course, including the full material focused on the new OAuth2 stack in Spring Security 5:

>> CHECK OUT THE COURSE

1. Overview

In this article, we'll see how to initialize and configure an OkHttpClient to trust self-signed certificates. For this purpose, we'll set up a minimal HTTPS-enabled Spring Boot application secured by a self-signed certificate.

Refer to our collection of articles on OkHttp for more specifics on the library.

2. The Fundamentals

Before we dive into the code responsible for making this work, let's hit the bottom line. The essence of SSL is that it establishes a secure connection between any two parties, commonly a client and a server. Also, it helps in safeguarding the privacy and integrity of data transferred over a network.

The JSSE API, a security component of the Java SE, provides complete API support for the SSL/TLS protocol.

SSL certificates, a.k.a digital certificates, play a vital role in establishing a TLS handshake, facilitating encryption and trust between the communicating parties. Self-signed SSL certificates are the ones that aren't issued by a well-known and trusted certificate authority (CA). They can be easily generated and signed by a developer to enable HTTPS for their software.

As the self-signed certificates aren't trustworthy, neither browsers nor standard HTTPS clients like OkHttp and Apache HTTP Client trust them by default.

Lastly, we can conveniently fetch the server certificate using a web browser or the OpenSSL command-line utility.

3. Setting Up the Test Environment

To demonstrate an application accepting and trusting a self-signed certificate using OkHttp, let's quickly configure and spin up a simple Spring Boot application with HTTPS enabled (secured by a self-signed certificate).

The default configuration will start a Tomcat server listening on port 8443 and expose a secured REST API accessible at “https://localhost:8443/welcome”.

Now, let's use an OkHttp client to make an HTTPS request to this server and consume the “/welcome” API.

4. OkHttpClient and SSL

This section will initialize an OkHttpClient and use it to connect to the test environment we just set up. Additionally, we'll examine the errors encountered in our path, and step by step, reach our final goal of trusting a self-signed certificate using OkHttp.

First, let create a builder for the OkHttpClient:

OkHttpClient.Builder builder = new OkHttpClient.Builder();

Also, let's declare the HTTPS URL that we'll use throughout this tutorial:

int SSL_APPLICATION_PORT = 8443;
String HTTPS_WELCOME_URL = "https://localhost:" + SSL_APPLICATION_PORT + "/welcome";

4.1. The SSLHandshakeException

Without configuring the OkHttpClient for SSL, if we attempt to consume an HTTPS URL, we get a security exception:

@Test(expected = SSLHandshakeException.class)
public void whenHTTPSSelfSignedCertGET_thenException() {
    builder.build()
    .newCall(new Request.Builder()
    .url(HTTPS_WELCOME_URL).build())
    .execute();
}

The stack trace is:

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

The above error precisely means that the server uses a self-signed certificate that is not signed by a Certificate Authority (CA).

Therefore, the client could not verify the chain of trust up to the root certificate, so it threw an SSLHandshakeException.

4.2. The SSLPeerUnverifiedException

Now, let's configure the OkHttpClient that trusts a certificate regardless of its nature – CA-signed or self-signed.

First, we need to create our own TrustManager that nullifies the default certificate validations and overrides those with our custom implementation:

TrustManager TRUST_ALL_CERTS = new X509TrustManager() {
    @Override
    public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {
    }

    @Override 
    public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {
    }

    @Override
    public java.security.cert.X509Certificate[] getAcceptedIssuers() {
        return new java.security.cert.X509Certificate[] {};
    }
};

Next, we'll use the above TrustManager to initialize an SSLContext, and also set the OkHttpClient builder's SSLSocketFactory:

SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, new TrustManager[] { TRUST_ALL_CERTS }, new java.security.SecureRandom());
builder.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) TRUST_ALL_CERTS);

Again, let's run the test. It won't be hard to believe that even after the above tweaks, consuming the HTTPS URL throws an error:

@Test(expected = SSLPeerUnverifiedException.class)
public void givenTrustAllCerts_whenHTTPSSelfSignedCertGET_thenException() {
    // initializing the SSLContext and set the sslSocketFactory
    builder.build()
        .newCall(new Request.Builder()
        .url(HTTPS_WELCOME_URL).build())
        .execute();
}

The exact error is:

javax.net.ssl.SSLPeerUnverifiedException: Hostname localhost not verified:
    certificate: sha256/bzdWeeiDwIVjErFX98l+ogWy9OFfBJsTRWZLB/bBxbw=
    DN: CN=localhost, OU=localhost, O=localhost, L=localhost, ST=localhost, C=IN
    subjectAltNames: []

This is due to a well-known problem – the hostname verification failure. Most of the HTTP libraries perform hostname verification against the certificate's SubjectAlternativeName's DNS Name field, which is unavailable in the server's self-signed certificate, as seen in the detailed stack trace above.

4.3. Overriding the HostnameVerifier

The last step towards configuring the OkHttpClient correctly is to disable the default HostnameVerifier and replace it with another one that bypasses the hostname verification.

Let's put in this last chunk of customization:

builder.hostnameVerifier(new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        return true;
    }
});

Now, let's run our test one last time:

@Test
public void givenTrustAllCertsSkipHostnameVerification_whenHTTPSSelfSignedCertGET_then200OK() {
    // initializing the SSLContext and set the sslSocketFactory
    // set the custom hostnameVerifier
    Response response = builder.build()
        .newCall(new Request.Builder()
        .url(HTTPS_WELCOME_URL).build())
        .execute();
    assertEquals(200, response.code());
    assertNotNull(response.body());
    assertEquals("<h1>Welcome to Secured Site</h1>", response.body()
        .string());
}

Finally, the OkHttpClient is successfully able to consume the HTTPS URL secured by a self-signed certificate.

5. Conclusion

In this tutorial, we learned about configuring SSL for an OkHttpClient such that it's able to trust a self-signed certificate and consume any HTTPS URL.

However, an important point to consider is that, although this design entirely omits certificate validation and hostname verification, all communications between the client and the server are still encrypted. The trust between the two parties is lost, but the SSL handshake and the encryption aren't compromised.

As always, we can find the complete source code over on GitHub.

Security bottom

I just announced the new Learn Spring Security course, including the full material focused on the new OAuth2 stack in Spring Security 5:

>> CHECK OUT THE COURSE
Generic footer banner
Comments are closed on this article!