使用Java8的SNI客户端之谜

时间:2016-03-31 03:39:38

标签: java ssl sni

我有一个Apache Web服务器,它运行多个具有不同证书和SNI的TLS虚拟主机。

我可以使用curl访问各种虚拟主机(假设SNI使其工作)。我也可以使用一个基本上只是在URL上的openConnection()的命令行Java程序来访问它们。

在我的Tomcat应用程序中,基本相同的客户端代码访问与客户端相同的Apache服务器,但总是以默认证书(defaulthost.defaultdomain)结束,而不是使用默认证书中指定的虚拟主机的证书。它尝试访问的URL。 (这会产生一个SunCertPathBuilderException - 基本上它无法验证证书的证书路径,当然这是真的,因为它是非官方证书。但是不管怎么说都不应该使用默认证书。)

就像我的应用程序/ Tomcat中的SNI已在客户端停用一样。我不知道为什么我的应用程序和命令行之间应该有不同的行为;相同的JDK,相同的主机等。

我找到了属性jsse.enableSNIExtension,但我确认两种情况都设置为true。问题:

  1. 任何想法,甚至是狂野的想法,为什么这两个程序的行为不同?

  2. 我想如何调试这个?

  3. 这是Arch Linux on 86_64,JDK 8u77,Tomcat 8.0.32。

3 个答案:

答案 0 :(得分:8)

这个答案来得太晚了,但我们遇到了问题(我不敢相信,这似乎是一个非常大的错误)。

它所说的一切似乎都是正确的,但它不是默认的HostnameVerifier的罪魁祸首,而是故障排除者。当HttpsClient执行afterConnect时首先尝试建立setHost(仅当socket是SSLSocketImpl时):

SSLSocketFactory factory = sslSocketFactory;
try {
    if (!(serverSocket instanceof SSLSocket)) {
        s = (SSLSocket)factory.createSocket(serverSocket,
                                            host, port, true);
    } else {
        s = (SSLSocket)serverSocket;
        if (s instanceof SSLSocketImpl) {
            ((SSLSocketImpl)s).setHost(host);
        }
    }
} catch (IOException ex) {
    // If we fail to connect through the tunnel, try it
    // locally, as a last resort.  If this doesn't work,
    // throw the original exception.
    try {
        s = (SSLSocket)factory.createSocket(host, port);
    } catch (IOException ignored) {
        throw ex;
    }
}

如果使用自定义SSLSocketFactory而不覆盖createSocket()(不带参数的方法),则使用完全参数化的createSocket,并且所有都按预期工作(使用客户端sni扩展)。但是当第二种方式使用时(尝试setHost和SSLSocketImpl)执行的代码是:

// ONLY used by HttpsClient to setup the URI specified hostname
//
// Please NOTE that this method MUST be called before calling to
// SSLSocket.setSSLParameters(). Otherwise, the {@code host} parameter
// may override SNIHostName in the customized server name indication.
synchronized public void setHost(String host) {
    this.host = host;
    this.serverNames =
        Utilities.addToSNIServerNameList(this.serverNames, this.host);
}

评论说全部。您需要在客户端握手之前调用setSSLParameters。如果使用默认的HostnameVerifier,HttpsClient将调用setSSLParameters。但是没有相反的方式执行setSSLParameters。对于Oracle来说,修复应该非常简单:

SSLParameters paramaters = s.getSSLParameters();
if (isDefaultHostnameVerifier) {
    // If the HNV is the default from HttpsURLConnection, we
    // will do the spoof checks in SSLSocket.
    paramaters.setEndpointIdentificationAlgorithm("HTTPS");

    needToCheckSpoofing = false;
}
s.setSSLParameters(paramaters);

Java 9在SNI中按预期工作。但他们(甲骨文)似乎不想解决这个问题:

答案 1 :(得分:6)

经过几个小时的JDK调试,这是一个不幸的结果。这有效:

URLConnection c = new URL("https://example.com/").openConnection();
InputStream i = c.getInputStream();
...

这失败了:

URLConnection c = new URL("https://example.com/").openConnection();
((HttpsURLConnection)c).setHostnameVerifier( new HostnameVerifier() {
        public boolean verify( String s, SSLSession sess ) {
            return false; // or true, won't matter for this
        }
});
InputStream i = c.getInputStream(); // Exception thrown here
...

添加setHostnameVerifier调用会导致禁用SNI,但自定义HostnameVerifier 永远不会被调用

罪魁祸首似乎是sun.net.www.protocol.https.HttpsClient中的代码:

            if (hv != null) {
                String canonicalName = hv.getClass().getCanonicalName();
                if (canonicalName != null &&
                canonicalName.equalsIgnoreCase(defaultHVCanonicalName)) {
                    isDefaultHostnameVerifier = true;
                }
            } else {
                // Unlikely to happen! As the behavior is the same as the
                // default hostname verifier, so we prefer to let the
                // SSLSocket do the spoof checks.
                isDefaultHostnameVerifier = true;
            }
            if (isDefaultHostnameVerifier) {
                // If the HNV is the default from HttpsURLConnection, we
                // will do the spoof checks in SSLSocket.
                SSLParameters paramaters = s.getSSLParameters();
                paramaters.setEndpointIdentificationAlgorithm("HTTPS");
                s.setSSLParameters(paramaters);

                needToCheckSpoofing = false;
            }

其中一些聪明的头脑检查配置的HostnameVerifier的类是否是默认的JDK类(当调用时,只返回false,就像我上面的代码一样)并基于此,更改SSL的参数连接 - 作为副作用,关闭SNI。

如何检查一个类的名称并使一些逻辑依赖于它是一个好主意逃避我。 (“妈妈!我们不需要虚拟方法,我们只需要查看类名并发送即可!”)但更糟糕的是,SNI首先与HostnameVerifier有什么关系呢?

或许解决方法是使用具有相同名称但大小写不同的自定义HostnameVerifier,因为同样聪明的头脑也决定进行不区分大小写的名称比较。

'努夫说。

答案 2 :(得分:0)

这是由JDK-8144566修复的Java 8错误(8u141)。有关更多信息,请参见Extended server_name (SNI Extension) not sent with jdk1.8.0 but send with jdk1.7.0