Search code examples
spring-amqpspring-rabbit

Performing TLS hostname validation with the Spring AMQP client


Use case: As a user of the Spring AMQP client connecting to RabbitMQ brokers over TLS, I want to verify that the hostname(s) either in the X.509 certificate Common Name field or in one of the Subject Alternative Names in the certificate X.509 extensions matches the hostname I used to connect to the broker.

One possible solution: The Spring Rabbit connection factory bean org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean has a setSocketConfigurator(com.rabbitmq.client.SocketConfigurator) method on it that can be used configure the SSLSocket with a javax.net.ssl.HandshakeCompletedListener as follows in this simple SocketConfigurator


static class MySocketConfigurator implements SocketConfigurator {

  private final String[] validHostnames;

  public MySocketConfigurator(String[] validHostnames) {
      this.validHostnames = validHostnames;
  }     

  @Override
  public void configure(final Socket socket) throws IOException {
      if (socket instanceof SSLSocket) {
          SSLSocket sslSocket = (SSLSocket) socket;
          sslSocket.addHandshakeCompletedListener(new HandshakeCompletedListener() {
              @Override
              public void handshakeCompleted(final HandshakeCompletedEvent event) {
                  try { 
                      if (event.getPeerCertificates()[0] instanceof X509Certificate) {
                          X509Certificate x509Certificate = (X509Certificate) event.getPeerCertificates()[0];
                          boolean verified = verifyHost(validHostnames, x509Certificate);
                          if (!verified) {
                              event.getSocket().close();
                          }     
                      } else {
                          event.getSocket().close();
                      }     
                  } catch (SSLPeerUnverifiedException e) {
                      throw new RuntimeException(e);
                  } catch (CertificateParsingException e) {
                      throw new RuntimeException(e);
                  } catch (IOException e) {
                      throw new RuntimeException(e);
                  }     
              }     
          });   
      }     
  }     

  // verify that one of the validHostname items matches a host found in the broker certificate
  boolean verifyHost(String[] validHostnames, X509Certificate serverCertificate) throws CertificateParsingException {
      ...   
  }     

}   

Closing the socket referenced in event.getSocket().close() seems a bit heavy-handed, but does suffice to shut the connection down if the application deems the hostnames in the certificate are not a close enough match. I had originally conceived to throw a RuntimeException upon determining that the hostnames did not match, but it appears the Spring stack swallows those and does not induce the desired application-induced connection setup failure.

Is the SocketConfigurator approach shown above, with its direct call to socket.close(), the recommended way to fail TLS connection setup if the certificate hostnames are deemed an insufficient match?


Solution

  • I am not sure what you mean by "spring stack swallows the exception"; doesn't sound right; if you can point me to the code that does that I can take a look.

    The spring connection factory just delegates to the rabbit connection factory.

    I don't know the answer to your basic question about best practices; you might want to ping the rabbit guys on the rabbitmq-users google group.