使用JSSE时如何进行主机名validation?

我正在编写一个Java客户端(需要在桌面JRE和Android上工作),以获得通过TLS承载的专有协议(特定于我的公司)。 我正在尝试找出用Java编写TLS客户端的最佳方法,特别是确保它正确地进行主机名validation。 ( 编辑:通过它,我的意思是检查主机名是否与X.509证书匹配,以避免中间人攻击。)

JSSE是编写TLS客户端的明显API,但我从“ 世界上最危险的代码 ”论文(以及实验)中注意到,JSSE在使用SSLSocketFactory API时不validation主机名。 (这是我必须使用的,因为我的协议不是HTTPS。)

因此,似乎在使用JSSE时,我必须自己进行主机名validation。 而且,不是从头开始编写代码(因为我几乎肯定会弄错),似乎我应该“借用”一些有效的现有代码。 所以,我发现的最有可能的候选者是使用Apache HttpComponents库(具有讽刺意味的是,因为我实际上并不在做HTTP) 并使用org.apache.http.conn.ssl.SSLSocketFactory类代替标准的javax .net.ssl.SSLSocketFactory类

我的问题是:这是一个合理的行动方案吗? 或者我完全误解了事情,走出了深层,实际上有一种更简单的方法来获取JSSE中的主机名validation,而不需要像HttpComponents那样引入第三方库?

我也查看了BouncyCastle,它有一个非JSSE API用于TLS,但它似乎更有限,因为它甚至没有进行证书链validation,更不用说主机名validation,所以它看起来像一个非-起动机。

编辑:这个问题已经回答了Java 7,但我仍然很好奇Java 6和Android的“最佳实践”是什么。 (特别是,我必须为我的应用程序支持Android。)

再次编辑:为了使我的“借用Apache HttpComponents”的提议更加具体,我创建了一个小型库 ,其中包含从Apache HttpComponents中提取的HostnameVerifier实现(最值得注意的是StrictHostnameVerifier和BrowserCompatHostnameVerifier)。 (我意识到我需要的只是validation器,我不需要像我原先想的那样使用Apache的SSLSocketFactory。)如果留给我自己的设备,这就是我将使用的解决方案。 但首先,我有什么理由这样做吗? (假设我的目标是以与https相同的方式进行主机名validation。我意识到它本身可以辩论,并且已经在加密列表的线程中进行了讨论,但是现在我坚持使用类似HTTPS的主机名validation,即使我没有使用HTTPS。)

假设我的解决方案没有“错误”,我的问题是:是否有一种“更好”的方式来实现它,同时仍然可以在Java 6,Java 7和Android中保持可移植性? (“更好”意味着更惯用,已经广泛使用,和/或需要更少的外部代码。)

Java 7(及以上)

您可以使用此隐式使用Java 7中引入的X509ExtendedTrustManager (请参阅此答案 :

 SSLParameters sslParams = new SSLParameters(); sslParams.setEndpointIdentificationAlgorithm("HTTPS"); sslSocket.setSSLParameters(sslParams); // also works on SSLEngine 

Android的

我对Android不熟悉,但是Apache HTTP Client应该与它捆绑在一起,所以它不是一个额外的库。 因此,您应该能够使用org.apache.http.conn.ssl.StrictHostnameVerifier 。 (我没试过这段代码。)

 SSLSocketFactory ssf = (SSLSocketFactory) SSLSocketFactory.getDefault(); // It's important NOT to resolve the IP address first, but to use the intended name. SSLSocket socket = (SSLSocket) ssf.createSocket("my.host.name", 443); socket.startHandshake(); SSLSession session = socket.getSession(); StrictHostnameVerifier verifier = new StrictHostnameVerifier(); if (!verifier.verify(session.getPeerHost(), session)) { // throw some exception or do something similar. } 

其他

不幸的是,validation者需要手动实现。 Oracle JRE显然有一些主机名validation器实现,但据我所知,它不能通过公共API获得。

在最近的答案中有关于规则的更多细节。

这是我写的一个实现。 它肯定可以通过审查…评论和反馈欢迎。

 public void verifyHostname(SSLSession sslSession) throws SSLPeerUnverifiedException { try { String hostname = sslSession.getPeerHost(); X509Certificate serverCertificate = (X509Certificate) sslSession .getPeerCertificates()[0]; Collection> subjectAltNames = serverCertificate .getSubjectAlternativeNames(); if (isIpv4Address(hostname)) { /https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* IP addresses are not handled as part of RFC 6125. We use the https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* RFC 2818 (Section 3.1) behaviour: we try to find it in an IP https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* address Subject Alt. Name. https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/*/ for (List sanItem : subjectAltNames) { /https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* Each item in the SAN collection is a 2-element list. See https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* X509Certificate.getSubjectAlternativeNames(). The https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* first element in each list is a number indicating the https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* type of entry. Type 7 is for IP addresses. https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/*/ if ((sanItem.size() == 2) && ((Integer) sanItem.get(0) == 7) && (hostname.equalsIgnoreCase((String) sanItem .get(1)))) { return; } } throw new SSLPeerUnverifiedException( "No IP address in the certificate did not match the requested host name."); } else { boolean anyDnsSan = false; for (List sanItem : subjectAltNames) { /https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* Each item in the SAN collection is a 2-element list. See https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* X509Certificate.getSubjectAlternativeNames(). The https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* first element in each list is a number indicating the https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* type of entry. Type 2 is for DNS names. https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/*/ if ((sanItem.size() == 2) && ((Integer) sanItem.get(0) == 2)) { anyDnsSan = true; if (matchHostname(hostname, (String) sanItem.get(1))) { return; } } } /https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* If there were not any DNS Subject Alternative Name entries, https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* we fall back on the Common Name in the Subject DN. https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/*/ if (!anyDnsSan) { String commonName = getCommonName(serverCertificate); if (commonName != null && matchHostname(hostname, commonName)) { return; } } throw new SSLPeerUnverifiedException( "No host name in the certificate did not match the requested host name."); } } catch (CertificateParsingException e) { /https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* It's quite likely this exception would have been thrown in the https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* trust manager before this point anyway. https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/*/ throw new SSLPeerUnverifiedException( "Unable to parse the remote certificate to verify its host name: " + e.getMessage()); } } public boolean isIpv4Address(String hostname) { String[] ipSections = hostname.split("\\."); if (ipSections.length != 4) { return false; } for (String ipSection : ipSections) { try { int num = Integer.parseInt(ipSection); if (num < 0 || num > 255) { return false; } } catch (NumberFormatException e) { return false; } } return true; } public boolean matchHostname(String hostname, String certificateName) { if (hostname.equalsIgnoreCase(certificateName)) { return true; } /https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* Looking for wildcards, only on the left-most label. https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/*/ String[] certificateNameLabels = certificateName.split("."); String[] hostnameLabels = certificateName.split("."); if (certificateNameLabels.length != hostnameLabels.length) { return false; } /https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* TODO: It could also be useful to check whether there is a minimum https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* number of labels in the name, to protect against CAs that would issue https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* wildcard certificates too loosely (eg https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/*.com). https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/*/ /https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* We check that whatever is not in the first label matches exactly. https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/*/ for (int i = 1; i < certificateNameLabels.length; i++) { if (!hostnameLabels[i].equalsIgnoreCase(certificateNameLabels[i])) { return false; } } /https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* We allow for a wildcard in the first label. https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/*/ if ("https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/*".equals(certificateNameLabels[0])) { // TODO match wildcard that are only part of the label. return true; } return false; } public String getCommonName(X509Certificate cert) { try { LdapName ldapName = new LdapName(cert.getSubjectX500Principal() .getName()); /https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* Looking for the "most specific CN" (ie the last). https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/*/ String cn = null; for (Rdn rdn : ldapName.getRdns()) { if ("CN".equalsIgnoreCase(rdn.getType())) { cn = rdn.getValue().toString(); } } return cn; } catch (InvalidNameException e) { return null; } } /https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/* BouncyCastle implementation, should work with Android. https://stackoverflow.com/questions/18139448/how-should-i-do-hostname-validation-when-using-jsse/*/ public String getCommonName(X509Certificate cert) { String cn = null; X500Name x500name = X500Name.getInstance(cert.getSubjectX500Principal() .getEncoded()); for (RDN rdn : x500name.getRDNs(BCStyle.CN)) { // We'll assume there's only one AVA in this RDN. cn = IETFUtils.valueToString(rdn.getFirst().getValue()); } return cn; } 

有两个getCommonName实现:一个使用javax.naming.ldap ,一个使用BouncyCastle,具体取决于可用的内容。

主要的细微之处在于:

  • 仅在SAN中匹配IP地址( 此问题与IP地址匹配和主题备用名称有关。)。 或许也可以做一些关于IPv6匹配的事情。
  • 通配符匹配。
  • 如果没有DNS SAN,则只返回CN。
  • “最具体”的CN真正意味着什么。 我以为这是最后一个。 (我甚至没有考虑具有多个属性值断言(AVA)的单个CN RDN:BouncyCastle可以处理它们,但据我所知,这是极其罕见的情况。)
  • 我还没有检查国际化(非ASCII)域名应该发生什么(参见RFC 6125。 )

编辑

为了使我的“借用Apache HttpComponents”的提议更加具体,我创建了一个小型库,其中包含从Apache HttpComponents中提取的HostnameVerifier实现(最值得注意的是StrictHostnameVerifier和BrowserCompatHostnameVerifier)。 [...]但首先,我有什么理由不这样做吗?

是的,有理由不这样做。

首先,你已经有效地分叉了一个库,你现在必须维护它,这取决于对原始Apache HttpComponents中这些类的进一步更改。 我不反对创建一个图书馆(我自己这样做了,我并不劝你这样做),但你必须考虑到这一点。 你真的想节省一些空间吗? 当然,如果您需要回收空间,有一些工具可以删除最终产品的未使用代码(ProGuard会浮现在脑海中)。

其次,即使StrictHostnameVerifier也不符合RFC 2818或RFC 6125.据我所知,它的代码:

  • 它将接受CN中的IP地址,而不应该接受。
  • 当没有DNS SAN存在时,它不仅会回到CN上,而且还将CN视为首选。 这可能会导致CN=cn.example.com的证书和www.example.com的SAN,但CN=cn.example.com的SAN不应该对cn.example.com有效。
  • 我对CN的提取方式持怀疑态度。 主题DN字符串序列化可能有点滑稽,特别是如果某些RDN包含逗号,以及某些RDN可能具有多个AVA的尴尬情况。

很难看到一般的“更好的方式”。 将此反馈提供给Apache HttpComponents库当然是一种方法。 复制和粘贴我上面写的代码肯定听起来不是一个好方法(SO上的代码片段一般不会被维护,不是100%经过测试而且可能容易出错)。

一种更好的方法可能是尝试说服Android开发团队支持与Java 7相同的SSLParametersX509ExtendedTrustManager 。这仍然留下遗留设备的问题。

要求jsse客户端(您)提供自己的StrictHostnameVerifier有很多充分的理由。 如果您信任您公司的名称服务器,那么编写一个应该非常简单。

  1. 从主机配置使用的DNS提供程序获取主机名的IP。
  2. 返回匹配名称的结果。
  3. (可选)validationIP的反向查找是否返回正确的名称。

如果您需要,我会为您提供validation。 如果你想让我提供’好理由’,我也可以这样做。