bigywater.tech
Java · 181

JSSE TLS 握手:Java 如何验证证书和协商密钥

Java 的 TLS 握手怎么协商算法、怎么验证证书?出错时的报错分别对应哪一步?这篇用 keytool 生成真实的证书和信任库,在回环地址上跑服务端与客户端,逐项验证(会话复用与 mTLS 在后面的篇目)。

TLSv1.3
JDK 21 默认协商出的协议(套件 TLS_AES_256_GCM_SHA384)
certificate_expired
CA 签发的过期证书的握手失败原因
protocol_version
双方协议版本没有交集时的告警

协商结果

实测 · 本机执行输出
  成功:协议 TLSv1.3,套件 TLS_AES_256_GCM_SHA384,服务端证书 CN=localhost
  JDK 默认开启的协议:[TLSv1.3, TLSv1.2];默认开启的 TLS 1.3 套件:[TLS_AES_256_GCM_SHA384, TLS_AES_128_GCM_SHA256, TLS_CHACHA20_POLY1305_SHA256]
  默认开启的套件一共 31 个

证书校验失败

实测 · 本机执行输出
  默认信任库(不含这个自签名证书):失败:SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path t ← 根因 SunCertPathBuilderException: unable to find valid certification path to requested target
  信任库里导入了服务端证书:成功:协议 TLSv1.3,套件 TLS_AES_256_GCM_SHA384,服务端证书 CN=localhost
  信任了证书,但证书是签给 example.test 的(SAN 只有 example.test),却用 localhost 连接:失败:SSLHandshakeException: (certificate_unknown) No subject alternative DNS name matching localhost found. ← 根因 CertificateException: No subject alternative DNS name matching localhost found.
  同样的连接,但不做主机名校验:成功:协议 TLSv1.3,套件 TLS_AES_256_GCM_SHA384,服务端证书 CN=example.test
  CA 签发的服务端证书(有效期内),客户端只信任 CA:成功:协议 TLSv1.3,套件 TLS_AES_256_GCM_SHA384,服务端证书 CN=localhost
  CA 签发、但已过期 8 天的服务端证书:失败:SSLHandshakeException: (certificate_expired) PKIX path validation failed: java.security.cert.CertPathValidatorException: validity check failed ← 根因 CertificateExpiredException: NotAfter: Thu Sep 24 23:08:13 CST 2026

协议版本

实测 · 本机执行输出
  服务端只开 [TLSv1.3],客户端只开 [TLSv1.3] → 成功:协议 TLSv1.3,套件 TLS_AES_256_GCM_SHA384,服务端证书 CN=localhost
  服务端只开 [TLSv1.2],客户端只开 [TLSv1.2] → 成功:协议 TLSv1.2,套件 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,服务端证书 CN=localhost
  服务端只开 [TLSv1.2],客户端只开 [TLSv1.3] → 失败:SSLHandshakeException: (protocol_version) Received fatal alert: protocol_version
  服务端只开 [TLSv1.3, TLSv1.2],客户端只开 [TLSv1.2, TLSv1.1] → 成功:协议 TLSv1.2,套件 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,服务端证书 CN=localhost

会话复用

实测 · 本机执行输出
  第 1 次连接:session 创建时间 96661,session id 长度 32,协议 TLSv1.3
  第 2 次连接:session 创建时间 96661,session id 长度 32,协议 TLSv1.3
  同一个 SSLContext 的 6 次连接耗时(µs):[45681, 6815, 4812, 4356, 4386, 3930](第一次包含类加载与完整握手)

握手消息

实测 · 本机执行输出
  客户端线程:产生 ClientHello
  服务端线程:处理 ClientHello
  服务端线程:产生 ServerHello
  客户端线程:处理 ServerHello
  客户端线程:处理 EncryptedExtensions
  客户端线程:处理 Certificate
  服务端线程:产生 CertificateVerify
  服务端线程:产生 Finished
  客户端线程:处理 CertificateVerify
  客户端线程:处理 Finished
  客户端线程:产生 Finished
  服务端线程:处理 Finished

分步演示

JSSE TLS
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12(JSSE、keytool),回环地址,自己生成的证书(EC secp256r1)
    • 没有验证:自签名且已过期的证书作为信任锚时是否检查有效期(我先试过一个自签名过期证书直接导入信任库,握手竟然成功,没有深入原因,所以改用 CA 签发的过期证书)、证书吊销检查(CRL/OCSP,见后面的篇目)、客户端证书(双向 TLS)、SNI、ALPN、TLS 1.3 的 0-RTT、与真实公网服务器的握手;会话复用的耗时与类加载预热混在一起
    京ICP备2026067502号-1京公网安备11010502063487号