For a Stack Exchange project, I'm downloading various links from all over the internet with a Java program that uses Apache HttpClient. It checks for expired SSL certificates, which can be one of the reasons images aren't visible anymore.
I noticed that sometimes, the Java program thinks an SSL certificate is expired, while my browser thinks it's not. An example is the following URL: https://www.dewharvest.com/uploads/3/4/5/4/34546214/oak-from-seed_orig.jpg
My browser (Firefox on macOS) thinks it's valid:
but when I run the stripped Java program below, this is what I get:
Name: CN=www.dewharvest.com
Not after: Tue Feb 09 00:59:59 CET 2021
Not before: Fri Feb 07 01:00:00 CET 2020
Serial #: 6f698f95c0f23b77fb181bf76141b080
Thumbprint: 580d0025564d9603ede46f6aba03dfc5045d207a
Name: CN=USERTrust RSA Certification Authority,O=The USERTRUST Network,L=Jersey City,ST=New Jersey,C=US
Not after: Sat May 30 12:48:38 CEST 2020
Not before: Tue May 30 12:48:38 CEST 2000
Serial #: 13ea28705bf4eced0c36630980614336
Thumbprint: eab040689a0d805b5d6fd654fc168cff00b78be3
Name: CN=Sectigo RSA Domain Validation Secure Server CA,O=Sectigo Limited,L=Salford,ST=Greater Manchester,C=GB
Not after: Wed Jan 01 00:59:59 CET 2031
Not before: Fri Nov 02 01:00:00 CET 2018
Serial #: 7d5b5126b476ba11db74160bbc530da7
Thumbprint: 33e4e80807204c2b6182a3a14b591acd25b5f0db
The order is different (end entity; root; intermediate) and you can see the thumbprint and serial number of the end entity and intermediate one match with my browser. The root certificate is different, the main problem being that this one is only valid until May last year. What's happening here? I checked my Java keystore (with keytool -list
) and it contains the same root certificate as my browser, but not the expired root certificate above:
usertrustrsaca [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): 2B:8F:1B:57:33:0D:BB:A2:D0:7A:6C:51:F7:0E:E9:0D:DA:B9:AD:8E
The Java program is here; I'm using version 4.5.11 of Apache HttpClient but updating to 4.5.13 doesn't help.
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.xml.bind.DatatypeConverter;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
public class Test {
public static void main(String[] args) throws Exception {
X509TrustManager x509TrustManager = getDefaultX509TrustManager();
SSLContext sslContext = SSLContext.getInstance("TLS");
final MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
sslContext.init(new KeyManager[0], new TrustManager[] { new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
x509TrustManager.checkClientTrusted(chain, authType);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
for (X509Certificate certificate : chain) {
System.out.println("Name: " + certificate.getSubjectX500Principal().getName());
System.out.println("Not after: " + certificate.getNotAfter());
System.out.println("Not before: " + certificate.getNotBefore());
System.out.println("Serial #: " + certificate.getSerialNumber().toString(16));
System.out.println("Thumbprint: " + DatatypeConverter
.printHexBinary(messageDigest.digest(certificate.getEncoded())).toLowerCase());
System.out.println();
}
x509TrustManager.checkServerTrusted(chain, authType);
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return x509TrustManager.getAcceptedIssuers();
}
} }, new SecureRandom());
final CloseableHttpClient client = HttpClients.custom().setSSLContext(sslContext).build();
client.execute(new HttpGet("https://www.dewharvest.com/"));
}
private static X509TrustManager getDefaultX509TrustManager() throws Exception {
TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore)null);
for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
if (trustManager instanceof X509TrustManager)
return (X509TrustManager)trustManager;
}
throw new Exception("No X509 trust manager found.");
}
}
There was a specific issue with Certigo, see: What happens if I have expired additional certificate in the chain with alternate trust path?
If I understand correctly, the owners of the website should have removed the old root certificate from their certificate chain, since the new root certificate is installed by default in browsers and also in Java's truststore as you saw.
There's a nice online tester for that by SSLMate: https://whatsmychaincert.com/?www.dewharvest.com
The difference between a browser and Java's TrustManager is that in Java, if the certificate in the certificate chain is expired, the alternative certificate (in the local truststore) is not checked anymore.
I remember that in Java 6 or 7, there was a different issue that the expiration date was only checked on the end certificate, and not on intermediate certificates, but I can't remember exactly when they fixed that.