- mongodb - 在 MongoDB mapreduce 中,如何展平值对象?
- javascript - 对象传播与 Object.assign
- html - 输入类型 ="submit"Vs 按钮标签它们可以互换吗?
- sql - 使用 MongoDB 而不是 MS SQL Server 的优缺点
我正在用 Java 编写一个客户端(需要在桌面 JRE 和 Android 上工作),用于通过 TLS 承载的专有协议(protocol)(特定于我的公司)。我试图找出用 Java 编写 TLS 客户端的最佳方法,特别是确保它正确地进行主机名验证。 (编辑:我的意思是检查主机名是否与 X.509 证书匹配,以避免中间人攻击。)
JSSE 是用于编写 TLS 客户端的明显 API,但我从“Most Dangerous Code in the World”论文(以及实验)中注意到,当使用 SSLSocketFactory API 时,JSSE 不会验证主机名。 (这是我必须使用的,因为我的协议(protocol)不是 HTTPS。)
所以,似乎在使用 JSSE 时,我必须自己进行主机名验证。而且,与其从头开始编写代码(因为我几乎肯定会弄错),我似乎应该“借用”一些现有的有效代码。所以,我发现的最有可能的候选者是使用 Apache HttpComponents 库(讽刺的是,因为我实际上并没有做 HTTP) 并使用 org.apache.http.conn.ssl.SSLSocketFactory 类代替标准的 javax.net.ssl.SSLSocketFactory 类.
我的问题是:这是一个合理的做法吗?还是我完全误解了一些事情,走得太远了,实际上有一种更简单的方法可以在 JSSE 中验证主机名,而无需引入像 HttpComponents 这样的第三方库?
我也查看了 BouncyCaSTLe,它具有用于 TLS 的非 JSSE API,但它似乎更加受限,因为它甚至不进行证书链验证,更不用说主机名验证,所以看起来像一个非首发。
编辑:这个问题has been answered对于 Java 7,但我仍然很好奇 Java 6 和 Android 的“最佳实践”是什么。 (特别是,我的应用程序必须支持 Android。)
再次编辑:为了使“从 Apache HttpComponents 借用”的建议更加具体,我创建了一个 small library其中包含从 Apache HttpComponents 中提取的 HostnameVerifier 实现(最值得注意的是 StrictHostnameVerifier 和 BrowserCompatHostnameVerifier)。 (我意识到我需要的只是 validator ,而且我不需要 Apache 的 SSLSocketFactory,因为我最初想的是。)如果留给我自己的设备,这就是我将使用的解决方案。但首先,我有什么理由不应该这样做吗? (假设我的目标是像 https 一样进行主机名验证。我意识到它本身是有争议的,并且已经在加密列表的线程中进行了讨论,但现在我坚持使用类似 HTTPS 的主机名验证,即使我没有使用 HTTPS。)
假设我的解决方案没有“错误”,我的问题是:有没有一种“更好”的方法来做到这一点,同时仍然可以跨 Java 6、Java 7 和 Android 移植? (“更好”意味着更惯用,已经广泛使用,和/或需要更少的外部代码。)
最佳答案
您可以隐式使用 X509ExtendedTrustManager
在 Java 7 中引入了这个(见 this answer:
SSLParameters sslParams = new SSLParameters();
sslParams.setEndpointIdentificationAlgorithm("HTTPS");
sslSocket.setSSLParameters(sslParams); // also works on SSLEngine
我对 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.
}
不幸的是, validator 需要手动实现。 Oracle JRE 显然有一些主机名 validator 实现,但据我所知,它不能通过公共(public) API 获得。
this recent answer 中有更多关于规则的详细信息.
这是我编写的一个实现。它当然可以与被审查有关...欢迎评论和反馈。
public void verifyHostname(SSLSession sslSession)
throws SSLPeerUnverifiedException {
try {
String hostname = sslSession.getPeerHost();
X509Certificate serverCertificate = (X509Certificate) sslSession
.getPeerCertificates()[0];
Collection<List<?>> subjectAltNames = serverCertificate
.getSubjectAlternativeNames();
if (isIpv4Address(hostname)) {
/*
* IP addresses are not handled as part of RFC 6125. We use the
* RFC 2818 (Section 3.1) behaviour: we try to find it in an IP
* address Subject Alt. Name.
*/
for (List<?> sanItem : subjectAltNames) {
/*
* Each item in the SAN collection is a 2-element list. See
* <a href=
* "http://docs.oracle.com/javase/7/docs/api/java/security/cert/X509Certificate.html#getSubjectAlternativeNames%28%29"
* >X509Certificate.getSubjectAlternativeNames()</a>. The
* first element in each list is a number indicating the
* type of entry. Type 7 is for IP addresses.
*/
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) {
/*
* Each item in the SAN collection is a 2-element list. See
* <a href=
* "http://docs.oracle.com/javase/7/docs/api/java/security/cert/X509Certificate.html#getSubjectAlternativeNames%28%29"
* >X509Certificate.getSubjectAlternativeNames()</a>. The
* first element in each list is a number indicating the
* type of entry. Type 2 is for DNS names.
*/
if ((sanItem.size() == 2)
&& ((Integer) sanItem.get(0) == 2)) {
anyDnsSan = true;
if (matchHostname(hostname, (String) sanItem.get(1))) {
return;
}
}
}
/*
* If there were not any DNS Subject Alternative Name entries,
* we fall back on the Common Name in the Subject DN.
*/
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) {
/*
* It's quite likely this exception would have been thrown in the
* trust manager before this point anyway.
*/
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;
}
/*
* Looking for wildcards, only on the left-most label.
*/
String[] certificateNameLabels = certificateName.split(".");
String[] hostnameLabels = certificateName.split(".");
if (certificateNameLabels.length != hostnameLabels.length) {
return false;
}
/*
* TODO: It could also be useful to check whether there is a minimum
* number of labels in the name, to protect against CAs that would issue
* wildcard certificates too loosely (e.g. *.com).
*/
/*
* We check that whatever is not in the first label matches exactly.
*/
for (int i = 1; i < certificateNameLabels.length; i++) {
if (!hostnameLabels[i].equalsIgnoreCase(certificateNameLabels[i])) {
return false;
}
}
/*
* We allow for a wildcard in the first label.
*/
if ("*".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());
/*
* Looking for the "most specific CN" (i.e. the last).
*/
String cn = null;
for (Rdn rdn : ldapName.getRdns()) {
if ("CN".equalsIgnoreCase(rdn.getType())) {
cn = rdn.getValue().toString();
}
}
return cn;
} catch (InvalidNameException e) {
return null;
}
}
/* BouncyCastle implementation, should work with Android. */
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,具体取决于可用的内容。
主要的微妙之处在于:
编辑:
To make my proposal of "borrow from Apache HttpComponents" moreconcrete, I've created a small library which contains theHostnameVerifier implementations (most notably StrictHostnameVerifierand BrowserCompatHostnameVerifier) extracted from ApacheHttpComponents. [...] But firstly, is there any reason I shouldn't doit this way?
是的,有理由不这样做。
首先,您已经有效地派生了一个库,现在您必须维护它,这取决于在原始 Apache HttpComponents 中对这些类所做的进一步更改。我不反对创建一个库(我自己已经这样做了,我并不劝阻你这样做),但你必须考虑到这一点。你真的想节省一些空间吗?当然,如果您需要回收空间,有些工具可以为您的最终产品删除未使用的代码(想到了 ProGuard)。
其次,即使是 StrictHostnameVerifier 也不符合 RFC 2818 或 RFC 6125。据我所知,它的代码:
CN=cn.example.com
的证书和 www.example.com
的 SAN但 cn.example.com
没有 SAN有效期为 cn.example.com
什么时候不应该。很难看到一般的“更好的方法”。将此反馈提供给 Apache HttpComponents 库当然是一种方式。复制和粘贴我之前在上面编写的代码当然听起来也不是一个好方法(SO 上的代码片段通常没有维护,没有 100% 测试并且可能容易出错)。
更好的方法可能是尝试说服 Android 开发团队支持相同的 SSLParameters
和 X509ExtendedTrustManager
就像在 Java 7 中所做的那样。这仍然会留下遗留设备的问题。
关于java - 使用 JSSE 时应该如何进行主机名验证?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18139448/
我在这里想做的是将所有连接转发到机器一上端口 3306 上的本地主机到本地主机上端口 3306 上的机器二。因此,如果您连接到机器一上的 mysql,它的行为就像您正在连接一样在二号机器上。 我认为
通过Kibana界面,如何获得 flex IP /主机? 我的意思是,与kibana连接的Elastic主机。 那有可能吗?我在这个上挣扎了好几个小时,却一无所获:( 附:不确定此问题是否是题外话,应
我知道这听起来很奇怪,但我有一个情况,Deno 需要关闭自己的主机(并因此杀死自己的进程)。这可能吗? 我特别需要这个用于 linux (lubuntu),如果相关的话。我想这需要 sudo 权限,这
我知道这听起来很奇怪,但我有一个情况,Deno 需要关闭自己的主机(并因此杀死自己的进程)。这可能吗? 我特别需要这个用于 linux (lubuntu),如果相关的话。我想这需要 sudo 权限,这
我有一个基本问题,但谷歌并没有为我产生很多结果(反正不是英文的)。基本上我想做的就是: 我有一个图形需要用作整个应用程序的持久 header ,例如:我不能让它在新的 Intent 调用时从屏幕上滑出
您好,我正在使用 xampp,我正在尝试使用 php 进行连接。 $sql_connections = mysql_connect("$server, $username, $password")
我目前正在尝试一些多人游戏的想法,并正在尝试创建一个 Java 应用程序来为基于网络浏览器的多人游戏提供服务。 我的开发环境是主机上的Eclipse, native 上的notepad + Googl
今天为大家分享一篇关于SSH 的介绍和使用方法的文章。本文从SSH是什么出发,讲述了SSH的基本用法,之后在远程登录、端口转发等多种场景下进行独立的讲述,希望能对大家有所帮助。 什么是SSH?
我已经完成了在裸机 Centos 7 上运行的测试 Kubernets 主机的设置。这将用作测试系统,因为我们将在 IBM Bluemix Kubernetes 服务中部署所有内容。 从 Bluemi
我正在尝试通过带有 4.2(果冻 bean )的 android 设备“nexus 7”通过 USB 与我的 freeduino 板进行通信,该板类似于 arduino uno。 几个月后,我使用开发
我正在使用 nginx,但在设置反向代理时遇到问题。 我的 nginx.conf 是默认的(没有对其进行任何更改),我的站点可用配置是: upstream backend_hosts { se
我在 projectlocker(免费 svn 主机)上有一个帐户,但我不知道如何将我的项目文件上传到它。 我在我的仪表板中找不到任何选项。 我在我的电脑上使用tortoiseSvn,那么如何上传文件
设置batchSize = 1有意义吗?如果我想一次处理一个文件? 尝试过batchSize = 1000和batchSize = 1 - 似乎具有相同的效果 { "version": "2.0"
我只想知道.. docker中现在有任何可用的工具吗?我已经阅读了Docker中有关多主机功能的一些文档,例如, Docker群 Docker服务(带有副本) 我也知道群模式下的volume问题,容器
我想将文件从 Docker 的容器挂载到我的 docker 主机。 数据卷不是我的解决方案,因为它们是从 docker 主机到 docker 容器的装载,我需要相反的方法。 谢谢 最佳答案 当 doc
我是新手。我无法正确理解RMI。互联网上有大量教程,但据我所知,它们都是针对本地主机的。服务器和客户端都运行在同一台机器上。 我想在任何计算机上运行客户端,并且主机将位于一台计算机上,让我们考虑IP
我无法从客户端“A”SSH 到服务器“B”(但我可以从同一子网上的许多其他 ssh 客户端而不是“A”——所有都是 *nux 机器) serverA>ssh -v -p 端口用户@serverB Op
设置batchSize = 1有意义吗?如果我想一次处理一个文件? 尝试过batchSize = 1000和batchSize = 1 - 似乎具有相同的效果 { "version": "2.0"
由于我不是天生的编码员,请多多包涵。 这是我尝试使用HAproxy来实现的目标,但是经过数小时的检查后,我无法以某种方式使其工作。 从 domain.com/alpha domain.com/beta
我正在使用 tomcat 运行 Java Web 应用程序,通过电子邮件将生成的报告发送给用户。我可以发送电子邮件,但几个小时后服务器停止发送电子邮件,并出现以下错误。 javax.mail.Mess
我是一名优秀的程序员,十分优秀!