java 使用Java8的SNI客户端之谜

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/36323704/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-03 01:16:18  来源:igfitidea点击:

SNI client-side mystery using Java8

javasslsni

提问by Johannes Ernst

I have an Apache web server that runs several TLS virtualhosts with different certs and SNI.

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

I can access the various virtual hosts just fine using curl (presumably SNI makes it work). I can also access them fine with a little command-line Java program that basically just openConnection()s on a URL.

我可以使用 curl 很好地访问各种虚拟主机(大概是 SNI 使它工作)。我还可以使用一个小的命令行 Java 程序很好地访问它们,该程序基本上只是 URL 上的 openConnection()s。

In my Tomcat application, the basic same client-side code accesses the same Apache server as a client, but always ends up with the default cert (defaulthost.defaultdomain) instead of the cert of the virtual host that was specified in the URL that it attempts to access. (This produces a SunCertPathBuilderException -- basically it can't verify the certificate path to the cert, which of course is true as it is a non-official cert. But then the default cert should not be used anyway.)

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

It's just as if SNI had been deactivated client-side in my application / Tomcat. I am at a loss why it should behave differently between my app and the command-line; same JDK, same host etc.

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

I found property jsse.enableSNIExtension, but I verified that it is set to true for both cases. Questions:

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

  1. Any ideas, even wild ones, why these two programs behave differently?

  2. Any ideas how I would debug this?

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

  2. 任何想法我将如何调试?

This is Arch Linux on 86_64, JDK 8u77, Tomcat 8.0.32.

这是 86_64、JDK 8u77、Tomcat 8.0.32 上的 Arch Linux。

回答by Dawuid

This answer comes late, but we just have hit the problem (I can't believe it, it seems a very big bug).

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

All what it said seems true, but it's not default HostnameVerifier the culprit but the troubleshooter. When HttpsClient do afterConnect first try to establish setHost (only when socket is SSLSocketImpl):

它所说的一切似乎都是正确的,但它不是默认的 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;
    }
}

If you use a custom SSLSocketFactory without override createSocket() (the method without parameters), the createSocket well parametrized is used and all works as expected (with client sni extension). But when second way it's used (try to setHost en SSLSocketImpl) the code executed is:

如果您使用自定义 SSLSocketFactory 而不覆盖 createSocket()(没有参数的方法),则使用参数化良好的 createSocket 并且一切按预期工作(使用客户端 sni 扩展)。但是当使用第二种方式时(尝试 setHost en 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);
}

The comments say all. You need to call setSSLParameters before client handshake. If you use default HostnameVerifier, HttpsClient will call setSSLParameters. But there is no setSSLParameters execution in the opposite way. The fix should be very easy for Oracle:

评论说明了一切。您需要在客户端握手之前调用 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 is working as expected in SNI. But they (Oracle) seem not to want fix this:

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

回答by Johannes Ernst

After some hours of debugging the JDK, here is the unfortunate result. This works:

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

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

This fails:

这失败了:

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
...

Adding the setHostnameVerifiercall has the consequence of disabling SNI, although the custom HostnameVerifieris never invoked.

添加setHostnameVerifier调用有禁用SNI的结果,虽然定制HostnameVerifier永远不会被调用

The culprit seems to be this code in sun.net.www.protocol.https.HttpsClient:

罪魁祸首似乎是以下代码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;
            }

where some bright mind checks whether the configured HostnameVerifier's class is the default JDK class (which, when invoked, just returns false, like my code above) and based on that, changes the parameters for the SSL connection -- which, as a side effect, turns off SNI.

一些聪明的头脑检查配置HostnameVerifier的类是否是默认的 JDK 类(它在调用时只返回 false,就像我上面的代码一样),并在此基础上更改 SSL 连接的参数——作为一个方面效果,关闭 SNI。

How checking the name of a class and making some logic depend on it is ever a good idea escapes me. ("Mom! We don't need virtual methods, we can just check the class name and dispatch on that!") But worse, what in the world does SNI have to do with the HostnameVerifier in the first place?

如何检查一个类的名称并使其依赖于它的一些逻辑是一个好主意,这让我无法理解。(“妈妈!我们不需要虚方法,我们只需检查类名并对其进行调度即可!”)但更糟糕的是,SNI 与 HostnameVerifier 到底有什么关系?

Perhaps the workaround is to use a custom HostnameVerifierwith the same name, but different capitalization, because that same bright mind also decided to do case-insensitive name comparison.

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

'nuff said.

'纳夫说。

回答by Florent Guillaume