While testing my client-server distributed system, I was surprised at first to learn that the default JSSE implementation of TLS doesn't do hostname verification. I tried the accepted answer in this question, but my use case is a bit different. I use RabbitMQ's connection factory, which abstracts the SSLSocket's construction. I just provide the connection factory with an SSLContext. I did find a lot about HTTPS and even some other protocols, but not something general that can always be used, even with custom protocols.
There's not really much to be found about creating a domain-verifying SSLContext though, except for using the X509ExtendedTrustManager
. While debugging, I can see that
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init((KeyStore) null);
tmf.getTrustManagers();
returns one TrustManager
, a X509TrustManagerImpl
, which according to this page extends X509ExtendedTrustManager
. This one does not, however, reject a faulty certificate (and by 'faulty' I mean that the certificate does not match the server's hostname).
So I then resorted to writing my own X509ExtendedTrustManager
, which delegates to the trust managers from my TrustManagerFactory
:
final TrustManager[] trustManagers = tmf.getTrustManagers();
X509ExtendedTrustManager x509ExtendedTrustManager = new X509ExtendedTrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException {
checkClientTrusted(x509Certificates, s);
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException {
checkServerTrusted(x509Certificates, s);
}
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException {
checkClientTrusted(x509Certificates, s);
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException {
checkServerTrusted(x509Certificates, s);
}
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
for (TrustManager trustManager : trustManagers)
((X509TrustManager)trustManager).checkClientTrusted(x509Certificates, s);
}
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
for (TrustManager trustManager : trustManagers)
((X509TrustManager)trustManager).checkServerTrusted(x509Certificates, s);
for (X509Certificate x509Certificate : x509Certificates) {
Collection<List<?>> alternativeNameCollection = x509Certificate.getSubjectAlternativeNames();
if (alternativeNameCollection != null) {
for (List alternativeNames : alternativeNameCollection) {
if (alternativeNames.get(1).equals(host))
return;
}
}
}
throw new CertificateException("Certificate hostname and requested hostname don't match");
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
This does finally work. But I cannot believe there wouldn't be a cleaner way to do this. Basically what I'm looking for is something standard, because I think what I'm doing is pretty standard too. I read about HostnameVerifier
but is there a way to use it with SSLContext without using HTTPS? Is there a hostname-verifying implementation of X509ExtendedTrustManager
somewhere? I'm sure I'm reinventing things that have already been written.
EDIT: this is a good, working example. Still custom code though.
EDIT 2: The problem still remains with RabbitMQ, because RabbitMQ resolves the DNS and passes the IP address to the verifier, which of course always fails. So the X509ExtendedTrustManager
still seems the way to go.