Anthony Sorvari

HostnameVerifier.verify not called with actual hostname

Discussion created by Anthony Sorvari on Feb 18, 2015
Latest reply on Aug 9, 2017 by Kirill Maximov

I have an XMPP client using Smack 4.0 which connects to a server using TLS and uses EXTERNAL SASL authentication to log in using a client certificate.  This works fine, but as a client, I want to verify the identity of the server that I am connecting to, much like a web browser verifies a website's host name against the server certificate when connecting over SSL/TLS.  Smack 4.0 provides the method ConnectionConfiguration.setHostnameVerifier, which allows me to provide my own class for validating the host name. However, it seems that I am not given sufficient information to validate the host name.

 

Suppose my XMPP server is at goodhost.com, with a server certificate whose Common Name is goodhost.com. The HostnameVerifier should protect against a Man-in-the-Middle attack where the attacker has obtained a valid certificate for a different host name and is trying to use that to impersonate my XMPP server. This is easily tested in reverse: add a hosts file entry for wronghost.com pointing to the IP address of my XMPP server, and try to connect to wronghost.com. I will connect to my XMPP server, which presents a certificate with a CN of goodhost.com, and I should be able to see that this does not match my expected host name of wronghost.com.

 

To my surprise, I see that my HostnameVerifier's verify method is called with a hostname of goodhost.com. Naturally, this matches the server certificate. It seems that "hostname" in this context is actually the Service Name which is set during the connection process.

 

In XMPPTCPConnection.login:

 

  // Set the user.
  String response = bindResourceAndEstablishSession(resource);
  if (response != null) {
   this.user = response;
   // Update the serviceName with the one returned by the server
   setServiceName(StringUtils.parseServer(response));
  }

 

And in XMPPTCPConnection.proceedTLSReceived:

 

  final HostnameVerifier verifier = getConfiguration().getHostnameVerifier();
  if (verifier != null && !verifier.verify(getServiceName(), sslSocket.getSession())) {
   throw new CertificateException("Hostname verification of certificate failed. Certificate does not authenticate " + getServiceName());
  }

 

Note that the login code has changed in 4.1, but the result appears to be the same, to take the Service Name from the server instead of using the configured host name.

 

        // Set the connections user to the result of resource binding. It is important that we don't infer the user
        // from the login() arguments and the configurations service name, as, for example, when SASL External is used,
        // the username is not given to login but taken from the 'external' certificate.
        user = response.getJid();
        serviceName = XmppStringUtils.parseDomain(user);

 

This is all well and good if I use a custom HostnameVerifier that knows the "hostname" is actually the XMPP Service Name and verifies it against the hostname that I actually wanted, but a standard implementation of HostnameVerifier that follows the HostnameVerifier interface documentation, which specifically states "hostname" throughout, won't work here.

What about the peer host name on the SSLSession? HostnameVerifier can check that, too. It turns out that is actually the IP address.

 

In XMPPTCPConnection.proceedTLSReceived:

 

        Socket plain = socket;
        // Secure the plain connection
        socket = context.getSocketFactory().createSocket(plain,
                plain.getInetAddress().getHostAddress(), plain.getPort(), true);

 

Socket.getInetAddress returns an InetAddress representing the IP address of the plain socket, and getHostAddress converts this to a String. This is not the host name that was used to open the plain socket. That information has been lost - it's as if I had connected to the server directly by IP address. (Fun fact - SSLParameters.setEndpointIdentificationAlgorithm lets you enforce HTTPS-style hostname validation on the SSLSocket itself, but if you give Smack an SSLContext which generates SSLSockets with this property set, the connection is rejected because the "host name" (IP address) does not match.)

 

My workaround is to validate the "hostname" passed to HostnameVerifier against my actual desired hostname, which is not a bad idea anyway. But, if you intended this "hostname" to be the actual hostname, as per the HostnameVerifier interface, then this needs to be fixed. If you do want to pass in the XMPP Service Name as the "hostname", then I strongly think this should be documented, for example in the Javadoc for ConnectionConfiguration.setHostnameVerifier. I understand not wanting to change this functionality due to the possibility of breaking existing HostnameVerifiers, especially since doing so has critical security implications. Fixing the SSLSocket hostname, on the other hand, should hopefully not break anything. In connectUsingConfiguration, sockets are opened by FQDN. It would make sense to set the SSLSocket's hostname to the same FQDN used to open the underlying socket so that this information can be available to a HostnameVerifier.


Outcomes