Java 7(及以上)
您可以使用此隐式使用X509ExtendedTrustManager
Java 7 中引入的(请参阅此答案:
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.
}
其他
不幸的是,验证器需要手动实现。Oracle JRE 显然有一些主机名验证器实现,但据我所知,它不能通过公共 API 获得。
在这个最近的答案中有更多关于规则的细节。
这是我编写的一个实现。它当然可以与被审查有关...欢迎评论和反馈。
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,具体取决于可用的内容。
主要的微妙之处在于:
- 仅在 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
和 SAN 在不应该的情况下有效,www.example.com
但没有 SAN 。cn.example.com
cn.example.com
- 我对提取 CN 的方式有点怀疑。主题 DN 字符串序列化可能有点有趣,特别是如果某些 RDN 包含逗号,以及某些 RDN 可以有多个 AVA 的尴尬情况。
很难看到一个普遍的“更好的方法”。将此反馈提供给 Apache HttpComponents 库当然是一种方式。复制和粘贴我之前在上面编写的代码当然听起来也不是一个好方法(SO 上的代码片段通常不维护,没有 100% 测试并且可能容易出错)。
更好的方法可能是尝试说服 Android 开发团队支持相同的功能SSLParameters
,X509ExtendedTrustManager
就像它为 Java 7 所做的那样。这仍然会留下遗留设备的问题。