Thursday 24 December 2015

Java SSL handshake with Server Name Identification (SNI)

SNI (Server Name Indication) was an extension added to TLS, to support multiple digital certificates per host name on a single IP. The SNI headers indicates which host is the client trying to connect as, allowing the server to return the appropriate digital certificate to the client.

Java handling of SNI 

 

Support for SNI extension was added in Java 7. With this addition, every time a SSL connection handshake was initiated, the server_name extension header was added by default, which ensure the client receives the correct server certificate(public key). This support exists in Java 8, however, there is a specific scenario where this header isn't sent.

When connecting to a server over a HTTPS url, the client performs a SSL handshake with the server, to validate the server's identity. The server sends a  server certificate (public key) and the client validates the certificate against an existing trust store (a storage for public keys of servers that the client trusts). The involves, apart from other things, matching the host name in the URL with the common name(CN) provided in the server certificate. If the host name in the URL does not match the server certificate common name (CN), you can provide a host name verifier implementation to perform custom validation and accordingly accept or deny the connection.

HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
   @Override
   public boolean verify(String hostName, SSLSession session) {
      return true;
   }
});

This is where things start to go wrong. Whenever a custom HostNameVerifier is provided, java 8 fails to add the SNI extension header and throws the following exception.

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
    at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1949)
    at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:302)
    at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:296)
    at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1506)
    at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:216)
    at sun.security.ssl.Handshaker.processLoop(Handshaker.java:979)
    at sun.security.ssl.Handshaker.process_record(Handshaker.java:914)
    at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1062)
    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387)
    at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559)
    at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
    at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1512)
    at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1440)
    at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254)

Note that this problem does not occur if the domain name is the only name mapped to the IP.

 

Solution 

 

To fix the problem, we need to ensure that each handshake includes the SNI header. This can be accomplished by implementing a SSLSocketFactory wrapper that adds supports adding custom parameters for every socket created

import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

public class SSLSocketFactoryWrapper extends SSLSocketFactory {

    private final SSLSocketFactory wrappedFactory;
    private final SSLParameters sslParameters;

    public SSLSocketFactoryWrapper(SSLSocketFactory factory, SSLParameters sslParameters) {
        this.wrappedFactory = factory;
        this.sslParameters = sslParameters;
    }

    @Override
    public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
        SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(host, port);
        setParameters(socket);
        return socket;
    }

    @Override
    public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
            throws IOException, UnknownHostException {
        SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(host, port, localHost, localPort);
        setParameters(socket);
        return socket;
    }


    @Override
    public Socket createSocket(InetAddress host, int port) throws IOException {
        SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(host, port);
        setParameters(socket);
        return socket;
    }

    @Override
    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
        SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(address, port, localAddress, localPort);
        setParameters(socket);
        return socket;

    }

    @Override
    public Socket createSocket() throws IOException {
        SSLSocket socket = (SSLSocket) wrappedFactory.createSocket();
        setParameters(socket);
        return socket;
    }

    @Override
    public String[] getDefaultCipherSuites() {
        return wrappedFactory.getDefaultCipherSuites();
    }

    @Override
    public String[] getSupportedCipherSuites() {
        return wrappedFactory.getSupportedCipherSuites();
    }

    @Override
    public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
        SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(s, host, port, autoClose);
        setParameters(socket);
        return socket;
    }

    private void setParameters(SSLSocket socket) {
        socket.setSSLParameters(sslParameters);
    }

}
This wrapper factory can be used in place of the default SSL Socket Factory as below

URL url = new URL("https://host.com");
SSLParameters sslParameters = new SSLParameters();
List sniHostNames = new ArrayList(1);
sniHostNames.add(new SNIHostName(url.getHost()));
sslParameters.setServerNames(sniHostNames);
SSLSocketFactory wrappedSSLSocketFactory = new SSLSocketFactoryWrapper(sslContext.getSocketFactory(), sslParameters);
HttpsURLConnection.setDefaultSSLSocketFactory(wrappedSSLSocketFactory); 
Any HTTPS connections from now on will have the server_name extension set to the host name. This can be verified by running the program with SSL debug flag on (-Djavax.net.debug=ssl:handshake)

Extension elliptic_curves, curve names: {secp256r1, sect163k1, sect163r2, secp192r1, secp224r1, sect233k1, sect233r1, sect283k1, sect283r1, secp384r1, sect409k1, sect409r1, secp521r1, sect571k1, sect571r1, secp160k1, secp160r1, secp160r2, sect163r1, secp192k1, sect193r1, sect193r2, secp224k1, sect239k1, secp256k1}
Extension ec_point_formats, formats: [uncompressed]
Extension signature_algorithms, signature_algorithms: SHA512withECDSA, SHA512withRSA, SHA384withECDSA, SHA384withRSA, SHA256withECDSA, SHA256withRSA, SHA224withECDSA, SHA224withRSA, SHA1withECDSA, SHA1withRSA, SHA1withDSA, MD5withRSA
Extension server_name, server_name: [type=host_name (0), value=host.com]

Note: I came across this error when creating a SOAP client using apache CXF SOAP module. The CXF SOAP client by default overrides the default host name verifier with its custom implementation.


24 comments:

  1. I was going mad on this til I found your post.
    Thank you.
    You're a genius! ;)

    ReplyDelete
  2. Thanks for this!

    Note that this bug has been reported on Java 8 (https://bugs.openjdk.java.net/browse/JDK-8159569) and fixed on Java 9 (https://bugs.openjdk.java.net/browse/JDK-8144566).

    ReplyDelete
  3. Thank you very much..its really helpful.

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. That was very helpful indeed.

    ReplyDelete
  6. SSLSocketFactory wrappedSSLSocketFactory = new SSLSocketFactoryWrapper(sslContext.getSocketFactory(), sslParameters);

    Where did you define sslContext ?

    ReplyDelete
    Replies
    1. I was wondering the same thing, but got it working based off of an example i found here: http://www.programcreek.com/java-api-examples/javax.net.ssl.SSLContext

      Girish, thank you for the example, it helped me out with a piece of code i was struggling on. I would also be curious how you defined sslContent to see if it was better than the example that i found.

      Delete
    2. SSLContext sslContext = SSLContext.getInstance("TLSv1.1");

      Delete
  7. Hi, thanks for your explanation.
    Anyone would find a solution using Java 7 ?

    Maybe I'm wrong, but the object SNIHostName is only available since java 8, is not it?

    ReplyDelete
    Replies
    1. Did you find a solution? I am struggling with the same and I think even though Java 7 support is there for SNI, we can't explicitly set it and can only enable/disable it during SSL Handshake.

      Delete
  8. Seems that finally Oracle corrected this bug for Java8 :-)

    http://www.oracle.com/technetwork/java/javase/8u141-relnotes-3720385.html

    ReplyDelete
  9. Thanks a lot for the article.. makes son many things clear now :)

    ReplyDelete
  10. This comment has been removed by the author.

    ReplyDelete
  11. Hi I am get "java.net.SocketException: Connection reset by peer: socket write error" on JBOSS when trying to send HTTPS request.
    The http connector performs below steps:
    1. Open connection: success
    2. Check connection: success
    3. Close connection: success
    4. Open connection to send request: success
    5. write lines : success
    6. Send : java.net.SocketException: Connection reset by peer: socket write error

    I have installed the certificates in my trust store and I don't see any ssl errors.

    Any help will be great!

    Thanks,
    Kunal

    ReplyDelete
    Replies
    1. Forgot to mention: The resource adapter(connection) is managed by JBOSS

      Delete
  12. This comment has been removed by the author.

    ReplyDelete
  13. Hi All,
    I have to set SNI name while making rest call using CXF client. I searched alot on internet but didn't find any example. Can someone help me on how to set SNI hostname while making rest call using CXF client 3.1.2. I'm able to do the same thing using HTTP client but I have to use CXF client now.

    ReplyDelete
  14. This is soooooo helpfull! You've made my day!

    ReplyDelete
  15. Thanks for taking the time to post this. Help me allot.

    ReplyDelete
  16. Anyway to use this in Java7, i could see that setServer is available only from Java 8.

    ReplyDelete
  17. I am using java 1.8 in PROD and QA also.but RSS feed is working in QA but getting connection reset error on PROD .
    URL feed = new URL(rssURL);
    String httpsIrsURL = "https://taxpayeradvocate.irs.gov/rss?projection=162";
    if(rssURL.equals(irsURL) || rssURL.equals(httpsIrsURL)){
    rssURL = httpsIrsURL;
    feed = new URL(rssURL);
    HttpsURLConnection con = null;
    con = (HttpsURLConnection) feed.openConnection();
    con.setConnectTimeout(5000); // three second
    //con.setReadTimeout(5000); // three second
    // Create a SSL SocketFactory
    SSLContext context = SSLContext.getInstance("TLSv1.1"); //Try Changing to TLSv1 and TLSv1.1, we will get connection reset error.
    context.init(null, null, null);
    SSLSocketFactory sslSocketFactory = context.getSocketFactory();
    con.setSSLSocketFactory(sslSocketFactory);

    when i changed tlsv1.2 to tlsv1 i am getting connection reset error but i given tlsv1.2 but still getting same error on PROD

    ReplyDelete
  18. good article and sharing knowledge. Well done.

    ReplyDelete
  19. SNIHostName serverName1 = new SNIHostName("www.verisign.co.in");
    SNIHostName serverName2 = new SNIHostName("www.verisign.co.uk");

    I am trying to pass 2 host Names but I get the following exception. When I inspect the SNIServerName ArrayList it dispalys this
    [type=host_name (0), value=www.verisign.co.in, type=host_name (0), value=www.verisign.co.uk]

    java.lang.IllegalArgumentException: Duplicated server name of type 0
    at java.base/javax.net.ssl.SSLParameters.setServerNames(SSLParameters.java:343)
    at SSLSocketClient.main(SSLSocketClient.java:69)

    ReplyDelete