Java 8 to Java 11 Hostname Verification Error
There is a ton of literature on the subject of a hostname verification error. Stackoverflow, blogs, on and on for a decade.
What there is not any information on, that I could find, is getting a hostname verification error in your program when porting to Java 11 from a working Java 8 baseline. What follows is for posterity and hopefully saves other people a ton of time.
For test or otherwise, you can set up an X509TrustManager to add to your SSLContext that will implicitly trust all host certificates. In our working Java 8 codebase, that looks like this:
public static X509TrustManager createTrustAllX509TrustManager() {
return new X509TrustManager() {
@Override
public void checkClientTrusted(final X509Certificate[] x509Certificates,final String authType) throws CertificateException {}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String authType) throws CertificateException {}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
}
Somewhere between 1.8.0.242 and 11.0.6, the underlying behavior in JDK began wrapping your X509TrustManager with AbstractTrustManagerWrapper that provided implementation for X509ExtendedTrustManager. This added a layer that will always do hostname verification even with your custom X509TrustManager. This is due to the four additional methods that came with the extended object. In our use case this was the trouble method:
sun/security/ssl/AbstractTrustManagerWrapper.java
public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException {
this.tm.checkServerTrusted(chain, authType);
this.checkAdditionalTrust(chain, authType, engine, false);
}
Egregiously, there is a check to see if the trust manager passed in is an instance of X509ExtendedTrustManager, but because the JDK wraps it, this is always true!
java.base/share/classes/sun/security/ssl/CertificateMessage.java
if (tm instanceof X509ExtendedTrustManager) {
if (chc.conContext.transport instanceof SSLEngine) {
SSLEngine engine = (SSLEngine)chc.conContext.transport;
((X509ExtendedTrustManager)tm).checkServerTrusted(
certs.clone(),
keyExchangeString,
engine);
} else {
SSLSocket socket = (SSLSocket)chc.conContext.transport;
((X509ExtendedTrustManager)tm).checkServerTrusted(
certs.clone(),
keyExchangeString,
socket);
}
} else {
// Unlikely to happen, because we have wrapped the old
// X509TrustManager with the new X509ExtendedTrustManager.
throw new CertificateException("Improper X509TrustManager implementation");
}
So rather than throw an error on our now less "good" TrustManager, we spend hours digging. This behavior effectively takes your intent and throws it away. Once we updated our TrustManger to be Extended we were good to go:
public static X509TrustManager createTrustAllX509TrustManager() {
return new X509ExtendedTrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException {}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException {}
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException {}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException {}
@Override
public void checkClientTrusted(final X509Certificate[] x509Certificates, final String authType) throws CertificateException {}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String authType) throws CertificateException {}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
}
Note, this could manifest as any number of certificate problems: SAN, hostname, et al. Nevertheless, the solution is the same. Provide an empty implementation or Java will fill one in for you and not tell.