bigywater.tech
Java · 252

双向 TLS:客户端证书如何参与身份认证

双向 TLS 里,服务器怎么要求客户端证书?没有证书、证书不被信任时会怎样?客户端有多个证书怎么选?这篇用 JDK 的 SSLSocket 和真实的 nginx、curl 逐项验证,并看 TLS 1.3 与 1.2 的失败表现差异。

certificate_required
TLS 1.3 下 need + 客户端没有证书的 alert
第一次读写
TLS 1.3 客户端才发现握手被拒绝的时机
400
nginx ssl_verify_client on 时没有证书或证书不被信任的 HTTP 状态

need / want / none

实测 · 本机执行输出
  [TLSv1.3]
    服务器 need,客户端有 alice(ca1 签发)         → 客户端:startHandshake 返回(106 ms),本地证书 O=demo,CN=alice,读到 1 | 服务器:握手成功,客户端身份 = O=demo,CN=alice
    服务器 need,客户端没有证书                      → 客户端:startHandshake 返回(15 ms),但第一次读写失败 → SSLHandshakeException: (certificate_required) Received fatal alert: certificate_required | 服务器:SSLHandshakeException: (certificate_required) Empty client certificate chain
    服务器 want,客户端没有证书                      → 客户端:startHandshake 返回(12 ms),本地证书 无,读到 1 | 服务器:握手成功,客户端身份 = (没有客户端证书)
    服务器 none(不要求),客户端有 alice             → 客户端:startHandshake 返回(10 ms),本地证书 无,读到 1 | 服务器:握手成功,客户端身份 = (没有客户端证书)
  [TLSv1.2]
    服务器 need,客户端有 alice(ca1 签发)         → 客户端:startHandshake 返回(15 ms),本地证书 O=demo,CN=alice,读到 1 | 服务器:握手成功,客户端身份 = O=demo,CN=alice
    服务器 need,客户端没有证书                      → 客户端:startHandshake 失败 → SocketException: Broken pipe | 服务器:SSLHandshakeException: (handshake_failure) Empty client certificate chain
    服务器 want,客户端没有证书                      → 客户端:startHandshake 返回(6 ms),本地证书 无,读到 1 | 服务器:握手成功,客户端身份 = (没有客户端证书)
    服务器 none(不要求),客户端有 alice             → 客户端:startHandshake 返回(7 ms),本地证书 无,读到 1 | 服务器:握手成功,客户端身份 = (没有客户端证书)

证书不被信任

实测 · 本机执行输出
  服务器 need,客户端 mallory(ca2 签发,服务器不信任 ca2)    → 客户端:startHandshake 返回(92 ms),但第一次读写失败 → SSLHandshakeException: (certificate_required) Received fatal alert: certificate_required | 服务器:SSLHandshakeException: (certificate_required) Empty client certificate chain
  服务器 need 且信任 ca1+ca2,客户端 mallory                → 客户端:startHandshake 返回(25 ms),本地证书 O=demo,CN=mallory,读到 1 | 服务器:握手成功,客户端身份 = O=demo,CN=mallory
  服务器 need,客户端自签名证书(old_self,已过期)        → 客户端:startHandshake 返回(15 ms),但第一次读写失败 → SSLHandshakeException: (certificate_required) Received fatal alert: certificate_required | 服务器:SSLHandshakeException: (certificate_required) Empty client certificate chain
  服务器 want,客户端 mallory(不被信任)                    → 客户端:startHandshake 返回(12 ms),本地证书 无,读到 1 | 服务器:握手成功,客户端身份 = (没有客户端证书)

两个身份,选哪一个

实测 · 本机执行输出
  客户端 keystore 里有两个身份 [mallory, alice]
  服务器只信任 ca1(CertificateRequest 里的可接受签发者只有 ca1):客户端:startHandshake 返回(128 ms),本地证书 O=demo,CN=alice,读到 1 | 服务器:握手成功,客户端身份 = O=demo,CN=alice
  服务器信任 ca1+ca2:客户端:startHandshake 返回(35 ms),本地证书 O=demo,CN=mallory,读到 1 | 服务器:握手成功,客户端身份 = O=demo,CN=mallory
  X509KeyManager.chooseClientAlias(RSA, issuers=[CN=ca1]) = alice;issuers=[CN=ca2] = mallory;issuers=null(服务器没给列表)= mallory

真实的 nginx

实测 · 本机执行输出
  ssl_verify_client on        curl 没有客户端证书            → curl 返回码 0;HTTP 400,页面标题「400 No required SSL certificate was sent」
  ssl_verify_client on        curl alice(ca1 签发)      → curl 返回码 0;verify=SUCCESS dn=O=demo,CN=alice  [HTTP 200]
  ssl_verify_client on        curl mallory(ca2 签发)    → curl 返回码 0;HTTP 400,页面标题「400 The SSL certificate error」
  ssl_verify_client optional  curl 没有客户端证书            → curl 返回码 0;verify=NONE dn=  [HTTP 200]
  ssl_verify_client optional  curl alice(ca1 签发)      → curl 返回码 0;verify=SUCCESS dn=O=demo,CN=alice  [HTTP 200]
  ssl_verify_client optional  curl mallory(ca2 签发)    → curl 返回码 0;HTTP 400,页面标题「400 The SSL certificate error」
  openssl s_client 看到 nginx 在 CertificateRequest 里给出的可接受 CA 名字: CN=ca1
  ssl_verify_client on,TLS 1.2,curl 不带证书 → curl 返回码 0,HTTP 状态 400

分步演示

双向 TLS
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12 SSLSocket;nginx 1.31.6、OpenSSL 3.6.4、curl 8.7.1;两个自己用 openssl 创建的测试 CA,证书与密钥只用于本机
    • 没有验证:证书吊销(CRL/OCSP)、过期客户端证书单独的失败原因(这里的过期证书根本没有被发送)、客户端证书里 SAN/EKU 的检查、TLS 重新协商时请求客户端证书、HttpClient 与连接池里的客户端证书、HSM 里的私钥
    京ICP备2026067502号-1京公网安备11010502063487号