SSL Certificate

Sometimes when we call an endpoint That has https And the https certificate is self signed its not signed by public certificate providers then we can get an error back java test store cannot be a certificate.

WebClientException PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

This is a common problem when dealing with HTTPS endpoints that have self-signed certificates or certificates not in Java's trusted keystore.

Why This Exception Occurs

  1. Untrusted SSL/TLS Certificate:

    • The server you're calling (the one causing the exception) is using an SSL/TLS certificate that is not trusted by the Java TrustStore.

    • This can happen if:

      • The certificate is self-signed (not issued by a trusted Certificate Authority).

      • The certificate is issued by a private or internal CA that is not included in the Java TrustStore.

      • The certificate is expired or invalid.

  2. Java TrustStore:

    • Java uses a TrustStore (cacerts) to store trusted root certificates. If the server's certificate is not signed by a CA in this TrustStore, Java cannot validate it, and this exception is thrown.

  3. Local Machine Testing:

    • When testing from your local machine, the server you're calling might be using a self-signed or internal certificate that is not trusted by default in the Java TrustStore.

How to Fix It

There are several ways you can fix an issue-

  • by adding server certificate to Java trust store

  • Bypass SSL Validation (For Testing Only)

  • Use a Custom TrustStore and use it with webclient

1. Adding certificate to java truststore

  • Export the certificate from the website:

openssl s_client -connect securewebsite.com:443 < /dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > server-certificate.pem
  • Import it into Java's truststore:

keytool -import -alias cerfgs -file server-certificate.pem -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit

here the changeit is the default password that we can need to change. This need to be done in every environment.

2. Bypass SSL Validation (For Testing Only)

In this approach we bypass the SSL validation, This to be done in a development or testing environment. Do not use this in production.

import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;

SslContext sslContext = SslContextBuilder
        .forClient()
        .trustManager(InsecureTrustManagerFactory.INSTANCE)
        .build();

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

WebClient webClient = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();

3. Use a Custom TrustStore [ Best for me ]

In this approach, we first create a custom truststore then configure it in webclient calling that particular endpoint.

Here are the following steps involved -

Step 1: Export the Certificate from the Server

# Use openssl to export the certificate
openssl s_client -connect secure.website.com:443 < /dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > website_certificate.pem

Step 2: Create a New Truststore

# Create a new truststore and import the certificate
keytool -import -file website_certificate.pem -alias cerfgs -keystore custom_truststore.jks

# You'll be prompted to:
# 1. Create a secure password for the truststore
# 2. Trust the certificate (type 'yes')

additionally, we can verify the generated certificate

# List certificates in your truststore
keytool -list -v -keystore custom_truststore.jks

Step 3: Configure Spring Boot Application

Now we need to use this generated truststore in spring boot app and configure it for webclient.

  1. Move the truststore file to your resources folder:

# copy the generated certificate and paste it in resources folder
cp customer_trustsotore.js src/main/resources/security/custom_truststore.jks
  1. Update application.properties/yaml:

# application.properties
ssl.trust-store=classpath:security/custom_truststore.jks
ssl.trust-store-password=your_truststore_password
  1. Create Custom TrustStore and configure

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.web.reactive.function.client.WebClient;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;

@Configuration
public class WebClientConfig {

    @Value("${ssl.trust-store}")
    private Resource trustStore;
    
    @Value("${ssl.trust-store-password}")
    private String trustStorePassword;

    @Bean
    public WebClient webClient() throws Exception {
        System.setProperty("javax.net.debug", "ssl,handshake");
        
        // loading the truststore
        KeyStore keyStore = KeyStore.getInstance("JKS");
        keyStore.load(trustStore.getInputStream(), trustStorePassword.toCharArray());

        // creating trust manager factory
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);

        // creatign ssl context with truststore
        SslContext sslContext = SslContextBuilder.forClient().trustManager(trustManagerFactory).build();

        // creating http client with custom ssl context
        HttpClient httpClient = HttpClient.create().secure(t -> t.sslContext(sslContext));

        // configuring httpclient in webclient
        return WebClient.builder()
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .baseUrl("https://secure.website.com")
            .build();
    }
}

Additional Security Tips:

  1. Store sensitive information like passwords in a secure vault (e.g., HashiCorp Vault, AWS Secrets Manager)

# application.properties
ssl.trust-store-password=${TRUST_STORE_PASSWORD}
  1. Regularly update certificates and truststore:

# Check certificate expiration
keytool -list -v -keystore custom_truststore.jks
  1. Add certificate rotation procedures to your deployment pipeline

# Script to update certificate (example)
#!/bin/bash
export CERT_PATH="/path/to/certificates"
export TRUSTSTORE_PATH="/path/to/truststore"

# Export new certificate
openssl s_client -connect secure.website.com:443 < /dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > $CERT_PATH/new_cert.pem

# Delete old certificate
keytool -delete -alias cerfgs -keystore $TRUSTSTORE_PATH/custom_truststore.jks -storepass $TRUSTSTORE_PASSWORD

# Import new certificate
keytool -import -file $CERT_PATH/new_cert.pem -alias cerfgs -keystore $TRUSTSTORE_PATH/custom_truststore.jks -storepass $TRUSTSTORE_PASSWORD

Last updated