bigywater.tech
Java · 413

证书主机名验证:公钥可信不等于目标可信

证书链验证通过只说明“这是某个受信任 CA 签发的证书”,并不说明你连上的就是你想连的主机——后者要靠主机名验证。这篇用 15 种证书与主机名的组合、四种 Java 客户端量出:哪里会检查、匹配规则是什么、以及怎样的写法让它悄悄失效。

15 / 15
默认 SSLSocket 对所有证书与主机名的组合握手成功(不检查主机名);加 HTTPS 端点标识后只有 9 / 15
HTTP 200
证书是给 other.test 的,用 right.test 去连,HttpsURLConnection + (h, s) -> true 的结果
一级
通配符 *.wild.test 能匹配的 DNS 层数(a.wild.test 成功,wild.test 与 a.b.wild.test 失败)

四种客户端的结果

实测 · 本机执行输出
  四种客户端:①SSLSocket 默认设置;②SSLSocket + setEndpointIdentificationAlgorithm("HTTPS");③HttpsURLConnection;④java.net.http.HttpClient
  [A SAN=DNS:localhost]
      连接 localhost:①握手成功;②握手成功
          ③HTTP 200;④HTTP 200
      连接 LOCALHOST:①握手成功;②握手成功
          ③HTTP 200;④HTTP 200
      连接 localhost.:①握手成功;②握手成功
          ③HTTP 200;④失败 IllegalArgumentException:Server name value of host_name cannot have the trailing dot
  [B SAN=DNS:other.test(证书是给另一个域名的)]
      连接 right.test:①握手成功;②失败 CertificateException:No subject alternative DNS name matching right.test found.
          ③失败 CertificateException:No subject alternative DNS name matching right.test found.;④失败 CertificateException:No subject alternative DNS name matching right.test found.
  [C 只有 CN=cn.test,没有 SAN]
      连接 cn.test:①握手成功;②握手成功
          ③HTTP 200;④HTTP 200
  [D SAN=DNS:*.wild.test]
      连接 a.wild.test:①握手成功;②握手成功
          ③HTTP 200;④HTTP 200
      连接 wild.test:①握手成功;②失败 CertificateException:No subject alternative DNS name matching wild.test found.
          ③失败 CertificateException:No subject alternative DNS name matching wild.test found.;④失败 CertificateException:No subject alternative DNS name matching wild.test found.
      连接 a.b.wild.test:①握手成功;②失败 CertificateException:No subject alternative DNS name matching a.b.wild.test found.
          ③失败 CertificateException:No subject alternative DNS name matching a.b.wild.test found.;④失败 CertificateException:No subject alternative DNS name matching a.b.wild.test found.
  [E SAN=IP:127.0.0.1]
      连接 127.0.0.1:①握手成功;②握手成功
          ③HTTP 200;④HTTP 200
      连接 localhost:①握手成功;②失败 CertificateException:No name matching localhost found
          ③失败 CertificateException:No name matching localhost found;④失败 CertificateException:No name matching localhost found
  [F SAN=DNS:127.0.0.1(把 IP 写成 DNS 名)]
      连接 127.0.0.1:①握手成功;②失败 CertificateException:No subject alternative names matching IP address 127.0.0.1 found
          ③失败 CertificateException:No subject alternative names matching IP address 127.0.0.1 found;④失败 CertificateException:No subject alternative names matching IP address 127.0.0.1 found
  [G SAN=DNS:w*.part.test(部分通配)]
      连接 w1.part.test:①握手成功;②握手成功
          ③HTTP 200;④HTTP 200
  [H SAN=DNS:UPPER.TEST(大写)]
      连接 upper.test:①握手成功;②握手成功
          ③HTTP 200;④HTTP 200
  [I SAN=DNS:dot.test + CN=wrong.test(SAN 与 CN 不一致)]
      连接 dot.test:①握手成功;②握手成功
          ③HTTP 200;④HTTP 200
      连接 wrong.test:①握手成功;②失败 CertificateException:No subject alternative DNS name matching wrong.test found.
          ③失败 CertificateException:No subject alternative DNS name matching wrong.test found.;④失败 CertificateException:No subject alternative DNS name matching wrong.test found.
  汇总:共 15 种(证书, 主机名)组合;①默认 SSLSocket 成功但②加上端点标识后失败的有 6 种;①成功的共 15 种,②成功的共 9 种,③成功的共 9 种,④成功的共 8 种

匹配规则

实测 · 本机执行输出
  大小写:SAN=localhost,连接 LOCALHOST → 成功
  没有 SAN 时回退到 CN:证书只有 CN=cn.test,连接 cn.test → 成功
  有 SAN 时 CN 被忽略:SAN=dot.test、CN=wrong.test,连接 dot.test → 成功;连接 wrong.test → 失败(CertificateException:No subject alternative DNS name matchin)
  通配符 *.wild.test:连接 a.wild.test → 成功;连接 wild.test → 失败(CertificateException:No subject alternative DNS name matchin);连接 a.b.wild.test → 失败(CertificateException:No subject alternative DNS name matchin)
  部分通配 w*.part.test:连接 w1.part.test → 成功
  IP 地址:SAN=IP:127.0.0.1,连接 127.0.0.1 → 成功;连接 localhost → 失败(CertificateException:No name matching localhost found);SAN=DNS:127.0.0.1,连接 127.0.0.1 → 失败(CertificateException:No subject alternative names matching I)
  主机名带结尾的点(localhost.):①成功;②成功;③成功;④失败(IllegalArgumentException:Server name value of host_name cann)

绕过写法

实测 · 本机执行输出
  [默认] HttpsURLConnection + setHostnameVerifier((h, s) -> true):HTTP 200;HttpsURLConnection + 返回 false 的验证器:失败 IOException:HTTPS hostname wrong: should be <right.test>;(自定义 HostnameVerifier 被调用:right.test);HttpClient:失败 CertificateException:No subject alternative DNS name matching right.test found.
      SSLContext 的默认 SSLParameters.endpointIdentificationAlgorithm = null
  [-Djdk.internal.httpclient.disableHostnameVerification=true] HttpsURLConnection + setHostnameVerifier((h, s) -> true):HTTP 200;HttpsURLConnection + 返回 false 的验证器:失败 IOException:HTTPS hostname wrong: should be <right.test>;(自定义 HostnameVerifier 被调用:right.test);HttpClient:HTTP 200
  [-Dcom.sun.net.ssl.checkRevocation=false(与主机名无关,对照)] HttpsURLConnection + setHostnameVerifier((h, s) -> true):HTTP 200;HttpsURLConnection + 返回 false 的验证器:失败 IOException:HTTPS hostname wrong: should be <right.test>;(自定义 HostnameVerifier 被调用:right.test);HttpClient:失败 CertificateException:No subject alternative DNS name matching right.test found.

分步演示

主机名验证
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12;服务端是 openssl 4.0.3 s_server -www(一次只处理一个连接,所以客户端关闭了 keep-alive);主机名用 -Djdk.net.hosts.file 解析,证书用本地 CA 签发(有效期 2 天)
    • 没有验证:国际化域名(IDN / punycode)、SAN 里的 URI / email 类型、OCSP / 吊销检查、SNI 与证书选择、Android / 其它 JDK 版本的匹配差异、Apache HttpClient / OkHttp 的默认主机名验证器、SSLEngine 的 setSSLParameters 用法
    京ICP备2026067502号-1京公网安备11010502063487号