When performing https communication, SNI (see Wikipedia) should be enabled by server-side settings, and conversely, SNI should be disabled. There is. If the connection fails due to this, the following exception will be thrown.
javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
It is enabled by default in Java7 or later, and can be disabled by specifying "-Djsse.enableSNIExtension = false" in the JVM startup option. (Reference article) However, this method sets the entire system to either enable or disable, so it does not work if you want to enable or disable it depending on the connection destination. Note that in versions less than 1.8.0_141, SNI could be disabled secondarily by calling connection.setHostnameVerifier, but in 1.8.0_141 this behavior was fixed to always follow the boot options.
In the startup option, with SNI enabled, by passing an empty list to the setServerNames method when creating a Socket with SocketFactory, it was possible to disable it individually for each communication.
Main.java
import java.io.*;
import java.net.*;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import javax.net.ssl.*;
import sun.security.ssl.SSLSocketImpl;
public class Main {
public static void connect(String url, boolean disableSni, boolean disableHostnameCheck) throws IOException, GeneralSecurityException {
HttpsURLConnection connection;
connection = (HttpsURLConnection)(new URL(url)).openConnection();
connection.setRequestMethod("GET");
//Disable host name check
if (disableHostnameCheck) {
connection.setHostnameVerifier(new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
}
//Certificate check invalidation
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, new X509TrustManager[] {new RelaxedX509TrustManager()}, new java.security.SecureRandom());
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
//SNI disabled
if (disableSni) {
socketFactory = new SniDisabledSSLSocketFactory(socketFactory);
}
//Reflect settings in connection
connection.setSSLSocketFactory(socketFactory);
//Get and output results
connection.connect();
int responseCode = connection.getResponseCode();
System.out.println("ResponseCode : " + responseCode);
InputStream input = null;
try {
input = connection.getInputStream();
} catch (IOException e) {
input = connection.getErrorStream();
}
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
System.out.println(line);
}
}
public static void main(String[] args) throws Exception {
connect("https://www.example.com/", false, true);
connect("https://www.example.com/", true, true);
connect("https://www.example.com/", false, false);
connect("https://www.example.com/", true, false); //Only this pattern has not been tested
}
}
/**
* jsse.SSLSocketFactory that disables SNI regardless of the enableSNIExtension setting
*/
class SniDisabledSSLSocketFactory extends SSLSocketFactory {
private SSLSocketFactory baseSocketFactory;
public SniDisabledSSLSocketFactory(SSLSocketFactory baseSocketFactory) {
this.baseSocketFactory = baseSocketFactory;
}
private Socket setSni(Socket socket) {
SSLParameters params = ((SSLSocketImpl)socket).getSSLParameters();
params.setServerNames(new ArrayList<SNIServerName>()); //Disable SNI by emptying the host name
((SSLSocketImpl)socket).setSSLParameters(params);
return socket;
}
@Override
public String[] getDefaultCipherSuites() {
return baseSocketFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return baseSocketFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket paramSocket, String paramString, int paramInt, boolean paramBoolean) throws IOException {
return setSni(baseSocketFactory.createSocket(paramSocket, paramString, paramInt, paramBoolean));
}
@Override
public Socket createSocket(String paramString, int paramInt) throws IOException, UnknownHostException {
return setSni(baseSocketFactory.createSocket(paramString, paramInt));
}
@Override
public Socket createSocket(String paramString, int paramInt1, InetAddress paramInetAddress, int paramInt2) throws IOException, UnknownHostException {
return setSni(baseSocketFactory.createSocket(paramString, paramInt1, paramInetAddress, paramInt2));
}
@Override
public Socket createSocket(InetAddress paramInetAddress, int paramInt) throws IOException {
return setSni(baseSocketFactory.createSocket(paramInetAddress, paramInt));
}
@Override
public Socket createSocket(InetAddress paramInetAddress1, int paramInt1, InetAddress paramInetAddress2, int paramInt2) throws IOException {
return setSni(baseSocketFactory.createSocket(paramInetAddress1, paramInt1, paramInetAddress2, paramInt2));
}
}
/**
*TrustManager to allow everything
*/
class RelaxedX509TrustManager implements javax.net.ssl.X509TrustManager {
public boolean isClientTrusted( java.security.cert.X509Certificate[] chain ){
return true;
}
public boolean isServerTrusted( java.security.cert.X509Certificate[] chain ){
return true;
}
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {}
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {}
}
It has been confirmed that it works as intended by connecting to each of the server that actually accepts only SNI enabled and the server that accepts only disabled. However, I couldn't prepare a server that "can connect with host name check enabled" and "accepts only SNI disabled", so I haven't been able to test that pattern. Even with that pattern, we have confirmed that packet capture does not send SNI information.
When I deleted the logic of certificate check invalidation and created SniDisabledSSLSocketFactory based on SocketFactory obtained by connection.getSSLSocketFactory (), it didn't work for some reason (= SNI was always enabled). It is unconfirmed whether SNI can be invalidated while properly checking the certificate using SSLContext generated by SSLContext.getInstance ("SSL").
I think the only effect of SNI enablement is to send the host name in the SSL / TSL Client Hello packet, but I'm not sure why some "servers will fail when SNI is enabled". Is SNI enabled in the server-side middleware, but the settings are incomplete?
As mentioned at the beginning, SNI is enabled by default in Java 7 and later, but in versions less than 1.8.0_141, SNI is disabled if you call setHostnameVerifier (HostnameVerifier v) and register your own Verifier. It had been. It has been enabled since 1.8.0_141. (See Release Note) This caused me to get hooked when updating Java, which inspired me to write this article.
Recommended Posts