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,网上对于这个问题的解决办法一般都是通过向HttpsURLConnection设置一个自定义的HostnameVerifier禁用证书中的域名校验即可,因为本来这中域名就不合法,如果对方不愿意配合修改域名的话,只能在我方这边关闭域名校验。
本文简单记录一下为什么这么设置可以禁用域名校验,以及这么做的优缺点。

问题现象

今天发现日志中出现大量调对方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)方法的注释和实现上可以看到,我们域名xxx_xx.test.com.cn里的_(ASCII码:0x5F)是不符合这个校验规则的。

2、为什么设置了HttpsURLConnectionHostnameVerifier就能解决这个问题

我们从刚才的错误堆栈处往上追溯,到这一个堆栈这里:

at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:200)

这一步的下一步就开始调用checkIdentity来检查域名了,这一步的代码片段如下:

// check endpoint identity
String identityAlg = sslSocket.getSSLParameters().
                       getEndpointIdentificationAlgorithm();
if (identityAlg != null && identityAlg.length() != 0) {
     checkIdentity(session, chain, identityAlg, checkClientTrusted);
}

可以看出这里是根据SSLParameters里的getEndpointIdentificationAlgorithm()返回的值来决定要不要做域名校验的。通过查找该方法内的属性值的set方法的调用方,发现在HttpsClientafterConnect方法中根据条件设置了该属性的值(从错误堆栈上也可以看到有这个方法的堆栈记录),代码内关于这部分设置还写了很详细的注释,如下:

// 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域名校验失败时,才会调用用户自定义的hostnameVerifier执行二次校验,当且仅当自定义的hostnameVerifier返回true时,才会认为域名校验成功。这也是为什么自定义HttpsURLConnection的hostnameVerifier为什么可以解决域名校验失败的原因。

解决方案

最简单的解决方案:自定义javax.net.ssl.HostnameVerifier实现,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;
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 自动化检测360显微镜(完全免费) http://appscan.360.cn/阿里聚安全(部分收费)https:...
    极客圈阅读 8,877评论 0 18
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 13,562评论 2 59
  • 随着移动互联网的发展,各大传统保险公司和银行金融公司都开发了自己的App,那么App的信息安全就变得非常重要了。如...
    码农一颗颗阅读 3,082评论 1 6
  • 一、Java语言规范 详见:Android开发java编写规范 二、Android资源文件命名与使用 1. 【推荐...
    王朋6阅读 1,034评论 0 0
  • 参考原文@ 我们不生产代码, 只是Bug 的搬运工 摘要:漏洞描述 对于数字证书相关概念、Android 里 ht...
    紫虹载雪阅读 657评论 0 0

友情链接更多精彩内容