今天解決了一個(gè)因https url中存在不合法字符導(dǎo)致證書校驗(yàn)失敗的問題钮热,錯(cuò)誤信息為java.security.cert.CertificateException: Illegal given domain xxx_xx.test.com.cn
,網(wǎng)上對(duì)于這個(gè)問題的解決辦法一般都是通過向HttpsURLConnection
設(shè)置一個(gè)自定義的HostnameVerifier
禁用證書中的域名校驗(yàn)即可烛芬,因?yàn)楸緛磉@中域名就不合法隧期,如果對(duì)方不愿意配合修改域名的話,只能在我方這邊關(guān)閉域名校驗(yàn)赘娄。
本文簡(jiǎn)單記錄一下為什么這么設(shè)置可以禁用域名校驗(yàn)仆潮,以及這么做的優(yōu)缺點(diǎn)。
問題現(xiàn)象
今天發(fā)現(xiàn)日志中出現(xiàn)大量調(diào)對(duì)方https服務(wù)失敗的情況遣臼,錯(cuò)誤堆棧如下:
Caused by: javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: Illegal given domain name: xxx_xx.test.com.cn
at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1946)
at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:316)
at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:310)
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1639)
at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:223)
at sun.security.ssl.Handshaker.processLoop(Handshaker.java:1037)
at sun.security.ssl.Handshaker.process_record(Handshaker.java:965)
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1064)
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379)
at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559)
at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
at sun.net.www.protocol.http.HttpURLConnection.getOutputStream0(HttpURLConnection.java:1334)
at sun.net.www.protocol.http.HttpURLConnection.getOutputStream(HttpURLConnection.java:1309)
at sun.net.www.protocol.https.HttpsURLConnectionImpl.getOutputStream(HttpsURLConnectionImpl.java:259)
at com.xxx.utils.http.SimpleHttpClient.doRequest(SimpleHttpClient.java:57)
... 91 more
Caused by: java.security.cert.CertificateException: Illegal given domain name: xxx_xx.test.com.cn
at sun.security.util.HostnameChecker.matchDNS(HostnameChecker.java:195)
at sun.security.util.HostnameChecker.match(HostnameChecker.java:96)
at sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:455)
at sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:436)
at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:200)
at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:124)
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1621)
... 107 more
Caused by: java.lang.IllegalArgumentException: Contains non-LDH ASCII characters
at java.net.IDN.toASCIIInternal(IDN.java:296)
at java.net.IDN.toASCII(IDN.java:122)
at javax.net.ssl.SNIHostName.<init>(SNIHostName.java:99)
at sun.security.util.HostnameChecker.matchDNS(HostnameChecker.java:193)
... 113 more
問題分析
1性置、錯(cuò)誤原因是什么
我們先從堆棧的最底層看起,先看最終出異常的地方java.net.IDN.toASCIIInternal()
:
for (int i = 0; i < dest.length(); i++) {
int c = dest.charAt(i);
if (isNonLDHAsciiCodePoint(c)) {
throw new IllegalArgumentException(
"Contains non-LDH ASCII characters");
}
}
//
// LDH stands for "letter/digit/hyphen", with characters restricted to the
// 26-letter Latin alphabet <A-Z a-z>, the digits <0-9>, and the hyphen
// <->.
// Non LDH refers to characters in the ASCII range, but which are not
// letters, digits or the hypen.
//
// non-LDH = 0..0x2C, 0x2E..0x2F, 0x3A..0x40, 0x5B..0x60, 0x7B..0x7F
//
private static boolean isNonLDHAsciiCodePoint(int ch){
return (0x0000 <= ch && ch <= 0x002C) ||
(0x002E <= ch && ch <= 0x002F) ||
(0x003A <= ch && ch <= 0x0040) ||
(0x005B <= ch && ch <= 0x0060) ||
(0x007B <= ch && ch <= 0x007F);
}
從isNonLDHAsciiCodePoint(int ch)
方法的注釋和實(shí)現(xiàn)上可以看到揍堰,我們域名xxx_xx.test.com.cn
里的_(ASCII碼:0x5F)
是不符合這個(gè)校驗(yàn)規(guī)則的蚌讼。
2、為什么設(shè)置了HttpsURLConnection
的HostnameVerifier
就能解決這個(gè)問題
我們從剛才的錯(cuò)誤堆棧處往上追溯个榕,到這一個(gè)堆棧這里:
at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:200)
這一步的下一步就開始調(diào)用checkIdentity
來檢查域名了篡石,這一步的代碼片段如下:
// check endpoint identity
String identityAlg = sslSocket.getSSLParameters().
getEndpointIdentificationAlgorithm();
if (identityAlg != null && identityAlg.length() != 0) {
checkIdentity(session, chain, identityAlg, checkClientTrusted);
}
可以看出這里是根據(jù)SSLParameters
里的getEndpointIdentificationAlgorithm()
返回的值來決定要不要做域名校驗(yàn)的。通過查找該方法內(nèi)的屬性值的set方法的調(diào)用方西采,發(fā)現(xiàn)在HttpsClient
的afterConnect
方法中根據(jù)條件設(shè)置了該屬性的值(從錯(cuò)誤堆棧上也可以看到有這個(gè)方法的堆棧記錄)凰萨,代碼內(nèi)關(guān)于這部分設(shè)置還寫了很詳細(xì)的注釋,如下:
// We have two hostname verification approaches. One is in
// SSL/TLS socket layer, where the algorithm is configured with
// SSLParameters.setEndpointIdentificationAlgorithm(), and the
// hostname verification is done by X509ExtendedTrustManager when
// the algorithm is "HTTPS". The other one is in HTTPS layer,
// where the algorithm is customized by
// HttpsURLConnection.setHostnameVerifier(), and the hostname
// verification is done by HostnameVerifier when the default
// rules for hostname verification fail.
//
// The relationship between two hostname verification approaches
// likes the following:
//
// | EIA algorithm
// +----------------------------------------------
// | null | HTTPS | LDAP/other |
// -------------------------------------------------------------
// | |1 |2 |3 |
// HNV | default | Set HTTPS EIA | use EIA | HTTPS |
// |--------------------------------------------------------
// | non - |4 |5 |6 |
// | default | HTTPS/HNV | use EIA | HTTPS/HNV |
// -------------------------------------------------------------
//
// Abbreviation:
// EIA: the endpoint identification algorithm in SSL/TLS
// socket layer
// HNV: the hostname verification object in HTTPS layer
// Notes:
// case 1. default HNV and EIA is null
// Set EIA as HTTPS, hostname check done in SSL/TLS
// layer.
// case 2. default HNV and EIA is HTTPS
// Use existing EIA, hostname check done in SSL/TLS
// layer.
// case 3. default HNV and EIA is other than HTTPS
// Use existing EIA, EIA check done in SSL/TLS
// layer, then do HTTPS check in HTTPS layer.
// case 4. non-default HNV and EIA is null
// No EIA, no EIA check done in SSL/TLS layer, then do
// HTTPS check in HTTPS layer using HNV as override.
// case 5. non-default HNV and EIA is HTTPS
// Use existing EIA, hostname check done in SSL/TLS
// layer. No HNV override possible. We will review this
// decision and may update the architecture for JDK 7.
// case 6. non-default HNV and EIA is other than HTTPS
// Use existing EIA, EIA check done in SSL/TLS layer,
// then do HTTPS check in HTTPS layer as override.
boolean needToCheckSpoofing = true;
String identification =
s.getSSLParameters().getEndpointIdentificationAlgorithm();
if (identification != null && identification.length() != 0) {
if (identification.equalsIgnoreCase("HTTPS")) {
// Do not check server identity again out of SSLSocket,
// the endpoint will be identified during TLS handshaking
// in SSLSocket.
needToCheckSpoofing = false;
} // else, we don't understand the identification algorithm,
// need to check URL spoofing here.
} else {
boolean isDefaultHostnameVerifier = false;
// We prefer to let the SSLSocket do the spoof checks, but if
// the application has specified a HostnameVerifier (HNV),
// we will always use that.
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;
}
}
我把這部分代碼的邏輯以流程圖的形式表示,看起來可能會(huì)更清晰一點(diǎn)胖眷,重點(diǎn)過程以紅色字體表示:
從圖上可以看出武通,當(dāng)用戶為
HttpsURLConnection
設(shè)置了非默認(rèn)的自定義hostnameVerifier,那么當(dāng)SSL域名校驗(yàn)失敗時(shí)珊搀,才會(huì)調(diào)用用戶自定義的hostnameVerifier執(zhí)行二次校驗(yàn)冶忱,當(dāng)且僅當(dāng)自定義的hostnameVerifier返回true時(shí),才會(huì)認(rèn)為域名校驗(yàn)成功境析。這也是為什么自定義HttpsURLConnection
的hostnameVerifier為什么可以解決域名校驗(yàn)失敗的原因囚枪。
解決方案
最簡(jiǎn)單的解決方案:自定義javax.net.ssl.HostnameVerifier
實(shí)現(xiàn),check方法直接返回true劳淆。
public class AcceptAllDomainHostnameVerifier implements HostnameVerifier {
public boolean verify(String hostname, SSLSession session){
return true;
}
}
但是這種接受所有SSL校驗(yàn)失敗的域名會(huì)有安全風(fēng)險(xiǎn)链沼,相對(duì)安全點(diǎn)的做法建立一個(gè)SSL校驗(yàn)失敗的域名白名單列表,只有配置在該列表種的域名沛鸵,才算通過二次校驗(yàn)括勺。
public class AcceptAllDomainHostnameVerifier implements HostnameVerifier {
public boolean verify(String hostname, SSLSession session){
if (isInWhiteList(hostname)) {
return true;
}
return false;
}
}