Ответ 1
Java 7 (и выше)
Вы можете неявно использовать X509ExtendedTrustManager
, введенный в Java 7, используя это (см. этот ответ:
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 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 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 only one AVA in this RDN.
cn = IETFUtils.valueToString(rdn.getFirst().getValue());
}
return cn;
}
Существуют две реализации getCommonName
: один использует javax.naming.ldap
и один использует BouncyCastle, в зависимости от того, что доступно.
Основные тонкости:
- Соответствие IP-адреса только в SAN (Этот вопрос посвящен сопоставлению IP-адресов и альтернативных имен объектов.). Возможно, что-то может быть сделано и для соответствия IPv6.
- Подстановочный знак.
- Только возврат на CN, если нет DNS SAN.
- Что означает "наиболее конкретный" CN. Я предположил, что это последний из них. (Я даже не рассматриваю ни одного CN RDN CN с несколькими утверждениями атрибутов (AVA): BouncyCastle может справиться с ними, но это очень редкий случай, насколько мне известно.)
- Я не проверял вообще, что должно произойти для интернационализированных (не ASCII) доменных имен (см. RFC 6125.)
ИЗМЕНИТЬ
Сделать предложение "заимствовать у Apache HttpComponents" больше бетон, я создал небольшую библиотеку, которая содержит Реализации HostnameVerifier (в первую очередь StrictHostnameVerifier и BrowserCompatHostnameVerifier), извлеченные из Apache HttpComponents. [...] Но, во-первых, есть ли какая-то причина, которую я не должен делать это так?
Да, есть причины не делать этого таким образом.
Во-первых, вы эффективно разветвляете библиотеку, и теперь вам придется ее поддерживать, в зависимости от дальнейших изменений, внесенных в эти классы в исходные Apache HttpComponents. Я не против создания библиотеки (я сделал это сам, и я не препятствую вам это делать), но вы должны принять это во внимание. Вы действительно пытаетесь сэкономить место? Разумеется, есть инструменты, которые могут удалить неиспользуемый код для вашего конечного продукта, если вам нужно освободить место (ProGuard приходит на ум).
Во-вторых, даже StrictHostnameVerifier не соответствует RFC 2818 или RFC 6125. Насколько я могу судить по его коду:
- Он будет принимать IP-адреса в CN, если это не так.
- Он не будет просто возвращаться к CN, если нет DNS SAN, но также рассматривают CN как первый выбор. Это может привести к сертификату с
CN=cn.example.com
и SAN дляwww.example.com
, но SAN дляcn.example.com
не будет действительным дляcn.example.com
, если это не так. - Я немного скептически отношусь к тому, как CN извлекается. Сериализация строки DN DN может быть немного забавной, особенно если некоторые RDN включают запятые и неудобный случай, когда некоторые RDN могут иметь несколько AVA.
Трудно увидеть общий "лучший способ". Предоставление этой обратной связи библиотеке Apache HttpComponents было бы одним из способов. Копирование и вставка кода, который я написал ранее, конечно, не очень хорошо звучит (фрагменты кода на SO обычно не поддерживаются, не проверяются на 100% и могут быть подвержены ошибкам).
Лучше всего попытаться попытаться убедить группу разработчиков Android поддерживать те же SSLParameters
и X509ExtendedTrustManager
, как это было сделано для Java 7. Это все еще оставляет проблему унаследованных устройств.