使用Java8的SNI客户端之谜

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

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

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

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

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

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

  2. 有什么想法我会如何调试这个?

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

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

它所说的一切似乎都是正确的,但它不是默认的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中正如预期的那样工作。 但他们(甲骨文)似乎不想解决这个问题:

经过几个小时的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 ... 

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

罪魁祸首似乎是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 ,因为同样聪明的头脑也决定进行不区分大小写的名称比较。

‘努夫说。

不确定这是否是同一个问题。 我的JVM配置为信任私有签名证书以连接到SNI服务器。 证书可以使用一段时间(几个小时),然后使用SunCertPathBuilderException: unable to find valid certification path to requested target开始失败SunCertPathBuilderException: unable to find valid certification path to requested target错误的SunCertPathBuilderException: unable to find valid certification path to requested target 。 一旦错误开始,我的Java应用程序就无法连接到HTTPS服务器,除非我以完全相同的配置重新启动JVM – 然后一切都重新开始工作。 这不断重复。

这是上面描述的相同问题吗?

谢谢