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.
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.
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.
I was going mad on this til I found your post.
ReplyDeleteThank you.
You're a genius! ;)
Thanks for this!
ReplyDeleteNote 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).
Thank you very much..its really helpful.
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteThat was very helpful indeed.
ReplyDeleteSSLSocketFactory wrappedSSLSocketFactory = new SSLSocketFactoryWrapper(sslContext.getSocketFactory(), sslParameters);
ReplyDeleteWhere did you define sslContext ?
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
DeleteGirish, 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.
SSLContext sslContext = SSLContext.getInstance("TLSv1.1");
DeleteHi, thanks for your explanation.
ReplyDeleteAnyone would find a solution using Java 7 ?
Maybe I'm wrong, but the object SNIHostName is only available since java 8, is not it?
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.
DeleteSeems that finally Oracle corrected this bug for Java8 :-)
ReplyDeletehttp://www.oracle.com/technetwork/java/javase/8u141-relnotes-3720385.html
Thanks a lot for the article.. makes son many things clear now :)
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteHi I am get "java.net.SocketException: Connection reset by peer: socket write error" on JBOSS when trying to send HTTPS request.
ReplyDeleteThe 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
Forgot to mention: The resource adapter(connection) is managed by JBOSS
DeleteThis comment has been removed by the author.
ReplyDeleteHi All,
ReplyDeleteI 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.
This is soooooo helpfull! You've made my day!
ReplyDeleteThanks for taking the time to post this. Help me allot.
ReplyDeleteAnyway to use this in Java7, i could see that setServer is available only from Java 8.
ReplyDeleteI am using java 1.8 in PROD and QA also.but RSS feed is working in QA but getting connection reset error on PROD .
ReplyDeleteURL 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
good article and sharing knowledge. Well done.
ReplyDeleteWow! great example for SNI
ReplyDeleteSNIHostName serverName1 = new SNIHostName("www.verisign.co.in");
ReplyDeleteSNIHostName 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)