When a connection to a remote URL from a Java application fails with javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
it may indicate that a certificate in the JRE keystore (or a custom one) has expired. Upgrading the JDK typically resolves the issue, but you can also verify which certificate has expired and check if the new keystore contains an updated version.
Below is a Java code SSLCertCheck.java
that identifies the problematic certificate chain:
import javax.net.ssl.*;
import java.net.URL;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.io.FileInputStream;
public class SSLCertCheck {
public static void main(String[] args) {
if (args.length != 2) {
System.out.println("Usage: java SSLCertCheck <URL> <cacerts path>");
return;
}
String urlString = args[0];
String trustStorePath = args[1];
try {
// Load the specified TrustStore (cacerts file)
FileInputStream trustStoreStream = new FileInputStream(trustStorePath);
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(trustStoreStream, "changeit".toCharArray()); // Default password is "changeit" for cacerts
// Create TrustManagerFactory from the loaded KeyStore
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
// Get the default X509 TrustManager
X509TrustManager defaultTm = null;
for (TrustManager tm : tmf.getTrustManagers()) {
if (tm instanceof X509TrustManager) {
defaultTm = (X509TrustManager) tm;
break;
}
}
if (defaultTm == null) {
throw new IllegalStateException("No default X509TrustManager found.");
}
// Wrap the default TrustManager to catch exceptions
final X509TrustManager finalTm = defaultTm;
TrustManager[] wrappedTrustManagers = new TrustManager[]{
new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
finalTm.checkClientTrusted(chain, authType);
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
try {
finalTm.checkServerTrusted(chain, authType);
} catch (CertificateException e) {
System.err.println("Certificate error: " + e.getMessage());
if (chain != null) {
System.err.println("Problematic certificate details:");
for (X509Certificate cert : chain) {
System.err.println(" Subject: " + cert.getSubjectDN());
System.err.println(" Issuer: " + cert.getIssuerDN());
System.err.println(" Expiry: " + cert.getNotAfter());
System.err.println(" Serial Number: " + cert.getSerialNumber());
}
}
throw e; // Ensure failure is still reported
}
}
public X509Certificate[] getAcceptedIssuers() {
return finalTm.getAcceptedIssuers();
}
}
};
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, wrappedTrustManagers, null);
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
// Try to open connection
URL url = new URL(urlString);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.connect();
System.out.println("Connection successful. No certificate issues found.");
} catch (Exception e) {
System.err.println("Connection failed: " + e.getMessage());
}
}
}
After complining it with javac SSLCertCheck.java
let’s try if a valid SSL connection can be established to our testing URL.
# java SSLCertCheck https://www.ecb.europa.eu/ /opt/jdk1.8.0_421/jre/lib/security/cacerts
Certificate error: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
Problematic certificate details:
Subject: CN=www.ecb.europa.eu, O=European Central Bank, L=Frankfurt am Main, C=DE
Issuer: CN=Entrust OV TLS Issuing ECC CA 1, O=SSL Corporation, C=US
Expiry: Fri Feb 13 17:00:00 CST 2026
Serial Number: 25584099522899652762104534182753886374
Subject: CN=Entrust OV TLS Issuing ECC CA 1, O=SSL Corporation, C=US
Issuer: CN=SSL.com TLS ECC Root CA 2022, O=SSL Corporation, C=US
Expiry: Sun Aug 22 12:53:06 CDT 2027
Serial Number: 96716820104204093748915326753122010098
Subject: CN=SSL.com TLS ECC Root CA 2022, O=SSL Corporation, C=US
Issuer: CN=SSL.com EV Root Certification Authority ECC, O=SSL Corporation, L=Houston, ST=Texas, C=US
Expiry: Wed Nov 03 12:26:26 CDT 2038
Serial Number: 78499417860755378313242233845787498908
Subject: CN=SSL.com EV Root Certification Authority ECC, O=SSL Corporation, L=Houston, ST=Texas, C=US
Issuer: CN=SSL.com EV Root Certification Authority ECC, O=SSL Corporation, L=Houston, ST=Texas, C=US
Expiry: Tue Feb 12 12:15:23 CST 2041
Serial Number: 3182246526754555285
Connection failed: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
Let’s check if the certificates are present in our Java keystore (starting with the bottom-most CA cert).
keytool -v -list -keystore /opt/jdk1.8.0_421/jre/lib/security/cacerts -storepass changeit | grep "SSL.com EV Root Certification Authority ECC"
Nothing is returned in our case, so let’s check if a newer JDK contains the certificate (note the change in the cacerts
path).
keytool -v -list -keystore /opt/jdk-22.0.1/lib/security/cacerts -storepass changeit | grep "SSL.com EV Root Certification Authority ECC"
If it does not, you can either add the missing certificates to the keystore or create and use a custom keystore. See the examples below for both options.
- Building a fresh up-to-date cacerts in JKS format (JDK 8 only supports JKS, newer JDKs also support PKCS12)
git clone https://github.com/adoptium/temurin-build.git
cd temurin-build/security
./mk-cacerts.sh
keytool -v -list -keystore cacerts -storepass changeit | grep "SSL.com EV Root Certification Authority ECC"
Copy the new cacerts
file to a location of your choice and inform Java about it by appending the following startup parameters (example):
-Djavax.net.ssl.trustStore=$HOME/cacerts -Djavax.net.ssl.trustStorePassword=changeit
Add these parameters to $JAVA_OPTS
or wherever you define your application’s startup options.
- Appending missing certificate to existing
cacerts
wget -O ecb_cert.pem https://www.ecb.europa.eu/certs.pem
keytool -import -trustcacerts -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit -noprompt -alias ecb_cert -file ecb_cert.pem
# or for a CA certiicate
keytool -import -trustcacerts -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit -noprompt -alias ssl_root -file SSL.com_EV_Root_CA_ECC.pem