bigywater.tech
Java · 262

证书固定 Pinning:额外信任约束如何维护

证书固定(pinning)在 CA 校验之上再限定服务器只能用哪个公钥。pin 怎么算、轮换时怎么不出事故、不验证只 pin 有什么风险?这篇用本机自建的 CA 与 TLS 服务逐项真实验证,并用 openssl 与 curl 交叉核对 pin 值。

返回码 90
curl 在 pin 不匹配时的返回码
同一个密钥
续期证书只要公钥不变,pin 仍然通过
PKIX + pin
只 pin 会放过过期证书,必须叠加

pin 的计算与交叉验证

实测 · 本机执行输出
  openssl x509 -pubkey | openssl pkey -pubin -outform der | sha256 → base64:zSW3rbSEHe8FBvmi+f3kZnZesyS6CSfanYpxyQSFI/E=(叶子 A 的 pin)
  curl --pinnedpubkey sha256//…(正确的 pin(A 的公钥))→ curl 返回码 0,HTTP 200
  curl --pinnedpubkey sha256//…(另一个证书 B 的 pin)→ curl 返回码 90,HTTP 000;
  curl --pinnedpubkey sha256//…(根证书的 pin)→ curl 返回码 90,HTTP 000;

Java 里的 pin 校验与轮换

实测 · 本机执行输出
  Java 里按 SubjectPublicKeyInfo 算出的 pin(A.crt 的叶子)= zSW3rbSEHe8FBvmi+f3kZnZesyS6CSfanYpxyQSFI/E=,与 openssl 算出的一致 = true
  只做普通 PKIX 校验(信任 root,不 pin),服务器证书 A → 连接成功
  pin = A 的公钥(叶子),服务器证书 A → 连接成功
  pin = A 的公钥,服务器换成证书 B(新密钥,同一个 CA 签发,PKIX 本身可以通过)→ 失败:CertificateException: 证书链里没有任何一个公钥匹配 pin(链长 2)
  pin = A 的公钥,服务器换成证书 A2(同一个密钥重新签发了新证书)→ 连接成功
  pin = {A, B}(当前 + 备用),服务器换成 B → 连接成功
  pin = 根 CA 的公钥,服务器 B(链里含根)→ 连接成功;服务器 A → 连接成功
  pin 集合为空 → 失败:CertificateException: 证书链里没有任何一个公钥匹配 pin(链长 2)

只 pin 不验证的风险

实测 · 本机执行输出
  已过期的证书 X(PKIX 可以识别)且 pin 匹配:只 pin、不做 PKIX → 连接成功
  同一个过期证书,PKIX + pin → 失败:CertificateExpiredException: NotAfter: Wed Aug 05 08:00:00 CST 2026
  主机名不符的证书 H(CN=wrong.example,CA 签发),连接 localhost:PKIX + pin + 主机名检查(HTTPS)→ 失败:CertificateException: No subject alternative DNS name matching localhost found.
  同一个 H 证书,不设 endpointIdentificationAlgorithm(不检查主机名)→ 连接成功(pin 只比较公钥,不检查证书里的名字)

分步演示

证书固定
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12;OpenSSL 3.6.4;curl 8.7.1;证书由本机 openssl 生成
    • 没有验证:Android/浏览器的 pin 机制(HPKP 已废弃)、pin 失败后的上报与回退策略、证书透明度(见相关内容)、HttpClient 与 OkHttp 的内置 pin API、pin 一个中间 CA 的取舍(这里只测了根与叶子)、pin 配置分发与过期的运维流程
    京ICP备2026067502号-1京公网安备11010502063487号