bigywater.tech
Java · 357

CertificateFactory:从字节流读取证书链

读证书看起来是一行代码,但格式容忍度、证书链文件的流读取、CertPath 编码与验证顺序都有不少细节。这篇用真实生成的根 → 中间 → 叶子证书链逐项验证。

4 次 → Empty input
对同一个流连续调用 generateCertificate,第 4 次(链里只有 3 张)的结果
[inter, leaf]
CertPath 经 PKCS7 编码往返之后的顺序(原来是 [leaf, inter]);PkiPath 往返保持顺序
同一个对象
同一份 PEM 解析两次(即使换行风格不同)得到的 X509Certificate 实例

PEM 与 DER 的容忍度与错误

实测 · 本机执行输出
  [formats]
    CertificateFactory.getInstance("X.509"):provider = SUN,type = X.509
    PEM(-----BEGIN CERTIFICATE-----)→ CN=www.example.test;颁发者 CN=Test Intermediate CA;签名算法 SHA384withECDSA;公钥 EC;SAN = [[2, www.example.test], [2, example.test]];BasicConstraints = -1
    DER(c.getEncoded(),长度 471)→ 1 张:[CN=www.example.test]
    PEM 前面加一段无关文字:1 张:[CN=www.example.test]
    PEM 用 CRLF 换行:1 张:[CN=www.example.test]
    PEM 每行缩进两个空格:CertificateException: No certificate data found
    BEGIN X509 CERTIFICATE(旧写法):1 张:[CN=www.example.test]
    BEGIN TRUSTED CERTIFICATE(OpenSSL 的信任格式):1 张:[CN=www.example.test]
    空输入:0 张:[]
    只有头尾没有内容:CertificateException: Unable to initialize, java.io.IOException: Too short ← IOException: Too short
    PEM 里 base64 被截断(去掉最后 30 个字符再补尾):CertificateException: Unable to initialize, java.io.EOFException: not enough content ← EOFException: not enough content
    第二行(base64 正文)中间混入非法字符 '!':CertificateException: java.io.IOException: java.lang.IllegalArgumentException: Illegal base64 character 21 ← IOException: java.lang.IllegalArgumentException: Illegal base64 character 21
    头尾标签换成别的名字(BEGIN FOO / END FOO):1 张:[CN=www.example.test];证书文件里的头是 “-----BEGIN CERTIFICATE-----”;头尾不一致(BEGIN CERTIFICATE / END FOO):CertificateException: java.io.IOException: Header and footer do not match: -----BEGIN CERTIFICATE----- -----END FOO----- ← IOException: Header and footer do not match: -----BEGIN CERTIFICATE----- 
    缺少 END 行:CertificateException: java.io.IOException: Incomplete data ← IOException: Incomplete data
    DER 后面带 20 个字节的垃圾:1 张:[CN=www.example.test]
    DER 被截掉最后 5 个字节:CertificateException: java.io.IOException: Incomplete BER/DER data ← IOException: Incomplete BER/DER data
    随机字节:CertificateException: No certificate data found

证书链文件与流语义

实测 · 本机执行输出
  [bundle]
    PEM 链文件(leaf + inter + root 依次拼接):generateCertificates → 3 张:[CN=www.example.test, CN=Test Intermediate CA, CN=Test Root CA]
    generateCertificate(单数)读同一个文件 → CN=www.example.test(只取第一张)
    对同一个 ByteArrayInputStream 连续调用 4 次 generateCertificate:[CN=www.example.test, CN=Test Intermediate CA, CN=Test Root CA, CertificateException: Could not parse certificate: java.io.IOException: Empty input ← IOException: Empty input]
    对同一个 FileInputStream(不支持 mark)连续调用 4 次:[CN=www.example.test, CN=Test Intermediate CA, CN=Test Root CA, CertificateException: Could not parse certificate: java.io.IOException: Empty input ← IOException: Empty input]
    FileInputStream.markSupported() = false;generateCertificates 读完后 available() = 0
    两张 DER 直接拼接:2 张:[CN=www.example.test, CN=Test Intermediate CA]
    PEM 之间夹着注释行(# comment):2 张:[CN=www.example.test, CN=Test Intermediate CA]
    PEM 之间夹着 openssl -text 风格的 Certificate: 文本块:2 张:[CN=www.example.test, CN=Test Intermediate CA]
    读 PEM 返回的集合类型:java.util.ArrayList;generateCertificates 对没有证书的输入 → CertificateException: No certificate data found

CertPath 编码与 PKIX 验证

实测 · 本机执行输出
  [path]
    generateCertPath([leaf, inter]) 长度 2;类型 X.509;编码格式 [PkiPath, PKCS7]
    PkiPath 编码长度 913,PKCS7 编码长度 956,默认编码长度 913
    从 PkiPath 字节读回:[CN=www.example.test, CN=Test Intermediate CA];equals 原路径? true
    从 PKCS7 字节读回:[CN=Test Intermediate CA, CN=www.example.test]
    验证 [leaf, inter](根作为信任锚):通过;信任锚 CN=Test Root CA,O=Demo;目标公钥 EC
    顺序反了 [inter, leaf]:CertPathValidatorException: Path does not chain with any of the trust anchors
    路径里带上根 [leaf, inter, root]:通过
    缺中间证书 [leaf]:CertPathValidatorException: Path does not chain with any of the trust anchors
    把中间证书当作信任锚验证 [leaf]:通过
    过期证书 checkValidity:CertificateExpiredException: NotAfter: Fri Jan 31 00:00:00 CST 2020;notBefore = 2019-12-31T16:00:00Z;notAfter = 2020-01-30T16:00:00Z
    验证 [expired, inter]:CertPathValidatorException: validity check failed ← CertificateExpiredException: NotAfter: Fri Jan 31 00:00:00 CST 2020
    验证 [leaf, inter],验证时间设为叶子 notAfter 之后一天(叶子有效期 90 天,notAfter = 2027-01-01T19:15:47Z):CertPathValidatorException: validity check failed ← CertificateExpiredException: NotAfter: Sat Jan 02 03:15:47 CST 2027
    验证时间设为叶子 notBefore 之前一天:CertPathValidatorException: validity check failed ← CertificateNotYetValidException: NotBefore: Sun Oct 04 03:15:46 CST 2026
    验证时间在有效期内(notAfter 之前一天):通过
    自签名根单独验证 [root] 以自己为锚:通过

类型名与对象相等

实测 · 本机执行输出
  [types]
        CertificateFactory.getInstance("X.509"  ) → X.509 / provider SUN
        CertificateFactory.getInstance("X509"   ) → X509 / provider SUN
        CertificateFactory.getInstance("x.509"  ) → x.509 / provider SUN
        CertificateFactory.getInstance("PKIX"   ) → CertificateException: PKIX not found ← NoSuchAlgorithmException: PKIX CertificateFactory not available
        CertificateFactory.getInstance("X.509-CRL") → CertificateException: X.509-CRL not found ← NoSuchAlgorithmException: X.509-CRL CertificateFactory not available
    支持的 CertPath 编码:[PkiPath, PKCS7]
        服务:SUN → X.509
    证书对象的类:sun.security.x509.X509CertImpl;toString 前 3 行:[[, [,   Version: V3]
    同一份 PEM 解析两次的两个对象 equals? true;hashCode 相同? true;是同一个对象? true
    getEncoded() 与 PEM 里的 base64 解码结果一致? true;长度 471
    换行风格不同(CRLF)但内容相同:equals true,同一对象 true

分步演示

CertificateFactory
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,SUN 提供者;证书由 keytool 现场生成(EC secp256r1,根 3650 天、中间 1800 天、叶子 90 天),keytool 的 gencert -rfc 输出里取第一张
    • 没有验证:吊销检查(CRL / OCSP,见下一篇)、RSA 证书、cross-signed 链与多信任锚、pathlen 约束与名称约束的违反、其它提供者(BouncyCastle)的容忍度差异、对象缓存的原因与上限(只观察到同一个对象)、JDK 17 / 27 的错误信息差异
    京ICP备2026067502号-1京公网安备11010502063487号