java.security.cert.CertificateException: Illegal given domain xxx_xx.test.com.cn

今天解決了一個因https url中存在不合法字符導致證書校驗失敗的問題,錯誤信息為java.security.cert.CertificateException: Illegal given domain xxx_xx.test.com.cn,網(wǎng)上對于這個問題的解決辦法一般都是通過向HttpsURLConnection設置一個自定義的HostnameVerifier禁用證書中的域名校驗即可,因為本來這中域名就不合法,如果對方不愿意配合修改域名的話,只能在我方這邊關閉域名校驗。
本文簡單記錄一下為什么這么設置可以禁用域名校驗,以及這么做的優(yōu)缺點。

問題現(xiàn)象

今天發(fā)現(xiàn)日志中出現(xiàn)大量調(diào)對方https服務失敗的情況,錯誤堆棧如下:

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、錯誤原因是什么

我們先從堆棧的最底層看起,先看最終出異常的地方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)方法的注釋和實現(xiàn)上可以看到,我們域名xxx_xx.test.com.cn里的_(ASCII碼:0x5F)是不符合這個校驗規(guī)則的。

2、為什么設置了HttpsURLConnectionHostnameVerifier就能解決這個問題

我們從剛才的錯誤堆棧處往上追溯,到這一個堆棧這里:

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()返回的值來決定要不要做域名校驗的。通過查找該方法內(nèi)的屬性值的set方法的調(diào)用方,發(fā)現(xiàn)在HttpsClientafterConnect方法中根據(jù)條件設置了該屬性的值(從錯誤堆棧上也可以看到有這個方法的堆棧記錄),代碼內(nèi)關于這部分設置還寫了很詳細的注釋,如下:

// 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;
    }
}

我把這部分代碼的邏輯以流程圖的形式表示,看起來可能會更清晰一點,重點過程以紅色字體表示:

SSL域名校驗流程圖

從圖上可以看出,當用戶為HttpsURLConnection設置了非默認的自定義hostnameVerifier,那么當SSL域名校驗失敗時,才會調(diào)用用戶自定義的hostnameVerifier執(zhí)行二次校驗,當且僅當自定義的hostnameVerifier返回true時,才會認為域名校驗成功。這也是為什么自定義HttpsURLConnection的hostnameVerifier為什么可以解決域名校驗失敗的原因。

解決方案

最簡單的解決方案:自定義javax.net.ssl.HostnameVerifier實現(xiàn),check方法直接返回true。

public class AcceptAllDomainHostnameVerifier implements HostnameVerifier {
  public boolean verify(String hostname, SSLSession session){
    return true;
  }
}

但是這種接受所有SSL校驗失敗的域名會有安全風險,相對安全點的做法建立一個SSL校驗失敗的域名白名單列表,只有配置在該列表種的域名,才算通過二次校驗。

public class AcceptAllDomainHostnameVerifier implements HostnameVerifier {
  public boolean verify(String hostname, SSLSession session){
    if (isInWhiteList(hostname)) {
      return true;
    }
    return false;
  }
}
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 自動化檢測360顯微鏡(完全免費) http://appscan.#/阿里聚安全(部分收費)https:...
    極客圈閱讀 8,917評論 0 18
  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 14,041評論 2 59
  • 隨著移動互聯(lián)網(wǎng)的發(fā)展,各大傳統(tǒng)保險公司和銀行金融公司都開發(fā)了自己的App,那么App的信息安全就變得非常重要了。如...
    碼農(nóng)一顆顆閱讀 3,113評論 1 6
  • 一、Java語言規(guī)范 詳見:Android開發(fā)java編寫規(guī)范 二、Android資源文件命名與使用 1. 【推薦...
    王朋6閱讀 1,043評論 0 0
  • 參考原文@ 我們不生產(chǎn)代碼, 只是Bug 的搬運工 摘要:漏洞描述 對于數(shù)字證書相關概念、Android 里 ht...
    紫虹載雪閱讀 670評論 0 0

友情鏈接更多精彩內(nèi)容