【问题标题】:SNI client-side mystery using Java8使用 Java8 的 SNI 客户端之谜
【发布时间】:2016-03-31 03:39:37
【问题描述】:

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

我可以使用 curl 访问各种虚拟主机(大概是 SNI 使其工作)。我还可以使用一个基本上只是在 URL 上打开连接()的小命令行 Java 程序来访问它们。

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

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

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

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

  2. 有什么想法可以调试吗?

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

【问题讨论】:

  • 您能否尝试捕获网络跟踪以查看应用程序是否在您的 tomcat 中发送 SNI 指示?你发出 https 请求的代码是什么?
  • SNI 不会神奇地添加到 SSL 连接,但负责建立 SSL 连接的代码必须使用它。并非所有 HTTP 库都这样做。我的猜测是,您在 tomcat 中使用不同的库或代码进行 URL 访问,而不是在您的小型测试程序中,因此 SNI 在一种情况下被使用,但在另一种情况下不使用。
  • 您可以启用 Java 对 SSL 消息的调试,例如在命令行中使用 -Djavax.net.debug=ssl,然后查看 Java 客户端发送/接收的具体内容...
  • @Castaglia:使用调试标志报告 Tomcat 应用程序(与命令行应用程序不同)不报告 Extension server_name, server_name: [type=host_name (0), value=example.com]

标签: java ssl sni


【解决方案1】:

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

它所说的一切似乎都是真的,但罪魁祸首不是默认的 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;
    }
}

如果您使用没有覆盖 createSocket() 的自定义 SSLSocketFactory(没有参数的方法),则会使用经过良好参数化的 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);
}

cmets 说了算。您需要在客户端握手之前调用 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 中按预期工作。但他们(甲骨文)似乎不想解决这个问题:

【讨论】:

【解决方案2】:

在调试 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,因为同样聪明的人还决定进行不区分大小写的名称比较。

'nuff 说。

【讨论】:

  • 您能否在HttpsURLConnection 上设置您自己的SSLSocketFactory 子类,以便您的工厂返回SSLSocket 之前对其执行socket.getSSLParameters().setEndpointIdentificationAlgorithm("HTTPS"),以解决上述代码?
【解决方案3】:

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

【讨论】:

    猜你喜欢
    • 2011-02-17
    • 2018-08-08
    • 1970-01-01
    • 2014-03-30
    • 2017-11-12
    • 1970-01-01
    • 1970-01-01
    • 2017-10-14
    • 1970-01-01
    相关资源
    最近更新 更多