跳到主要内容
ecdsa.com
← 全部文章

X.509 证书字段详解:以真实证书为例

我们于2026年10月20日获取了 letsencrypt.org、github.com 和 one.one.one.one 的证书链,用 OpenSSL 3.6.5 逐个字段解读,同时对照 RFC 5280 和 CA/Browser Forum《基线要求》2.3.1,看每个字段必须包含什么。

简短回答:TLS 客户端只根据主题备用名称(SAN)识别服务器,即其中的 dNSName 和 iPAddress 条目,从不依据主体的通用名称(CN)。密钥用法说明密钥在数学上可以做什么(签名、加密、签发证书),扩展密钥用法(EKU)说明它可以用于哪些应用(serverAuth、clientAuth、代码签名)。一张证书能否签发其他证书,取决于基本约束中的 CA:TRUE;叶子证书为 CA:FALSE。颁发机构密钥标识符(AKI)和主体密钥标识符(SKI)只是帮助客户端找到颁发者的线索,而不是安全检查。CRL 分发点和颁发机构信息访问(AIA)告诉客户端去哪里检查吊销状态、获取缺失的中间证书;Let's Encrypt 的证书已不再带有 OCSP URL。SCT 证明证书已记录到证书透明度(CT)日志中,这是 Chrome 和 Safari 的要求。

获取一张真实证书

openssl s_client -showcerts 会打印服务器发送的每一张证书;openssl x509 -text 则解码其中一张。实验环境为 debian:forky-20261005-slim,使用 OpenSSL 3.6.5 和 pyca/cryptography 49.0.0:

保存服务器发送的证书链,每张证书一个 PEM 文件
openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org -showcerts </dev/null 2>/dev/null \
  | awk '/-----BEGIN CERTIFICATE-----/{n++; f=sprintf("letsencrypt.org-%d.pem", n); p=1} p{print >> f} /-----END CERTIFICATE-----/{p=0; close(f)}'
openssl x509 -in letsencrypt.org-1.pem -noout -text

-servername 用于发送 SNI;不发送 SNI 时,许多服务器会返回另一个名称的默认证书。下面是 letsencrypt.org 当天提供的叶子证书(公钥完整显示,两个 SCT 签名已截断):

openssl x509 -text,letsencrypt.org 叶子证书(SCT 签名已截断)
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            05:43:93:3b:e3:86:b3:a2:b3:ad:d5:34:f2:10:3b:c8:b6:5f
        Signature Algorithm: ecdsa-with-SHA384
        Issuer: C=US, O=Let's Encrypt, CN=YE2
        Validity
            Not Before: Sep  4 14:34:32 2026 GMT
            Not After : Dec  3 14:34:31 2026 GMT
        Subject: CN=letsencrypt.org
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:e9:e5:40:7b:43:1e:46:b9:8e:45:e4:aa:cb:40:
                    ab:e6:3f:99:27:b8:b7:81:e3:32:56:e7:14:74:5f:
                    fa:43:5b:52:c6:a6:b5:66:c5:56:0e:07:21:56:e3:
                    4c:fe:35:55:4f:f0:92:2f:eb:22:4a:f1:7a:39:51:
                    39:04:c2:b3:0d
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Subject Key Identifier: 
                16:68:8D:1E:2A:32:48:8C:B5:F1:10:0A:16:08:37:08:3B:9D:30:23
            X509v3 Authority Key Identifier: 
                B9:59:F2:8E:CF:22:F0:86:D3:37:48:FF:76:14:18:BA:82:D8:55:87
            Authority Information Access: 
                CA Issuers - URI:http://ye2.i.lencr.org/
            X509v3 Subject Alternative Name: 
                DNS:cp.letsencrypt.org, DNS:cp.root-x1.letsencrypt.org, DNS:cps.letsencrypt.org, DNS:cps.root-x1.letsencrypt.org, DNS:lencr.org, DNS:letsencrypt.com, DNS:letsencrypt.org, DNS:www.lencr.org, DNS:www.letsencrypt.com, DNS:www.letsencrypt.org
            X509v3 Certificate Policies: 
                Policy: 2.23.140.1.2.1
            X509v3 CRL Distribution Points: 
                Full Name:
                  URI:http://ye2.c.lencr.org/96.crl

            CT Precertificate SCTs: 
                Signed Certificate Timestamp:
                    Version   : v1 (0x0)
                    Log ID    : D8:09:55:3B:94:4F:7A:FF:C8:16:19:6F:94:4F:85:AB:
                                B0:F8:FC:5E:87:55:26:0F:15:D1:2E:72:BB:45:4B:14
                    Timestamp : Sep  4 15:33:02.377 2026 GMT
                    Extensions: none
                    Signature : ecdsa-with-SHA256
                                30:46:02:21:00:D0:2E:25:A4:9D:7F:C8:18:D6:7B:5E:...
                Signed Certificate Timestamp:
                    Version   : v1 (0x0)
                    Log ID    : 46:AF:86:3D:3B:3E:E5:9F:A5:77:DE:A8:24:5D:36:B0:
                                D9:ED:22:A2:23:F4:61:77:41:22:94:52:EE:95:50:5F
                    Timestamp : Sep  4 15:33:02.552 2026 GMT
                    Extensions: 00:00:05:00:24:DF:DB:0C
                    Signature : ecdsa-with-SHA256
                                30:44:02:20:31:2F:83:D6:42:65:26:0D:58:D7:DD:C3:...
    Signature Algorithm: ecdsa-with-SHA384
    Signature Value:
        30:65:02:31:00:ee:1d:b0:7c:35:88:98:15:57:b8:88:73:23:
        92:a9:bf:40:bf:e8:ae:02:48:e6:f4:7a:2d:18:fc:43:52:d3:
        80:9a:ff:ce:e0:2a:0d:4a:29:e6:80:a2:94:f5:4f:14:d9:02:
        30:35:08:99:3b:bc:89:5b:23:a0:ef:64:24:e2:b2:b3:b8:c2:
        d8:d5:95:45:de:f3:03:e8:e7:82:78:a5:9e:c2:1f:ad:eb:14:
        fc:39:51:c7:e1:18:e9:16:b4:a5:48:de:09

本文其余部分按从上到下的顺序逐项讲解。结构本身由 RFC 5280 定义(§4.1 为字段,§4.2 为扩展);公开受信任的 TLS 证书可以包含哪些内容,则由 CA/Browser Forum 的《基线要求》(Baseline Requirements,BR)§7.1 规定,本文依据的是2026年10月4日发布的 2.3.1 版。

版本、序列号、签名算法、有效期

  • 版本 3 是唯一包含扩展的版本,《基线要求》规定必须使用它。
  • 序列号:在同一颁发者范围内唯一;对于公共 CA,它还必须是“大于零(0)且小于 2¹⁵⁹ 的非连续数,并包含至少 64 位来自 CSPRNG 的输出”(《基线要求》§7.1.2.7)。这里的随机性用于抵御针对签名哈希的选择前缀碰撞攻击,序列号看起来像随机噪声也正是这个原因。CRL 中列出的就是序列号。
  • 签名算法出现两次:一次在被签名的部分中,一次在签名旁边,两处必须逐字节相同。这里的 ecdsa-with-SHA384 反映的是颁发者的密钥(YE2 是 P-384 密钥),而不是主体的密钥。末尾的签名值是 DER 编码的 ECDSA 签名,这种格式在 ECDSA 签名格式一文中有详细说明。
  • 有效期:notBefore 和 notAfter 这两个时刻本身都包含在有效期内。这张证书的有效期为 90 天;《基线要求》规定的上限为:2026年3月15日起签发的证书 200 天,2027年3月15日起 100 天,2029年3月15日起 47 天(《基线要求》§6.3.2)。Let's Encrypt 已在需主动选用的 tlsserver 配置文件(profile)中签发 45 天证书,并计划于2027年2月10日把默认有效期改为 64 天,于2028年2月16日改为 45 天。

主体与主题备用名称

主体是一个可分辨名称:这里只有 CN=letsencrypt.org。域名验证型证书不包含其他任何属性,因为 CA 除此之外什么也没有验证。客户端检查的名称位于主题备用名称(SAN)中:这张证书有十个 DNS: 条目。

通用名称早就不再起作用了。2023年发布的 RFC 9525 取代了 RFC 6125,其中写道:“通用名称 RDN 不得(MUST NOT)用于标识服务”。Chrome 从 58 版(2017年)起不再匹配通用名称;Go 遇到只有 CN 的证书会拒绝,并报错 x509: certificate relies on legacy Common Name field。《基线要求》只把 CN 保留为一份副本:如果存在,它“必须(MUST)恰好包含一个条目,且该条目是证书 subjectAltName 扩展所含的值之一”(§7.1.4.3)。

SAN 中也可以包含 IP 地址。下面是 Cloudflare 解析器的证书:

openssl x509 -noout -subject -ext subjectAltName(one.one.one.one 叶子证书)
subject=C=US, ST=California, L=San Francisco, O=Cloudflare, Inc., CN=cloudflare-dns.com
X509v3 Subject Alternative Name: 
    DNS:cloudflare-dns.com, DNS:*.cloudflare-dns.com, IP Address:1.0.0.1, IP Address:1.1.1.1, IP Address:162.159.36.1, IP Address:162.159.46.1, IP Address:2606:4700:4700:0:0:0:0:1001, IP Address:2606:4700:4700:0:0:0:0:1111, IP Address:2606:4700:4700:0:0:0:0:64, IP Address:2606:4700:4700:0:0:0:0:6400, DNS:one.one.one.one

https://1.1.1.1 之所以能用,是因为 1.1.1.1 是一个 iPAddress 条目;DNS:1.1.1.1 条目无法匹配 IP 地址,Go 的报错 cannot validate certificate for … because it doesn't contain any IP SANs 说的就是这种情况。这里的主体还包含组织名称和所在地,证书也声明了 OV 策略 2.23.140.1.2.2(参见 DV vs OV vs EV);但浏览器不会显示其中任何一项。这张证书于2025年12月31日签发,有效期 355 天,当时 200 天的上限尚未生效。

像 *.cloudflare-dns.com 这样的通配符只匹配一个标签:它匹配 a.cloudflare-dns.com,但不匹配 cloudflare-dns.com 本身,也不匹配 a.b.cloudflare-dns.com(RFC 9525 §6.3)。OpenSSL 也按同样的方式检查名称:

openssl x509 -noout -checkhost <name>(letsencrypt.org 叶子证书)
Hostname www.lencr.org does match certificate
Hostname lencr.org.evil.test does NOT match certificate
Hostname mail.letsencrypt.org does NOT match certificate

主体公钥信息

subjectPublicKeyInfo(SPKI)包含算法和密钥本身:一个 P-256 未压缩点(04 后接 32 字节的 x 和 y)。证书中只有这一部分与主体的私钥相关,所以公钥固定计算的是 SPKI 的哈希,而不是整张证书的哈希:用同一密钥续期后,固定值保持不变(参见2026年的证书固定)。

密钥用法与扩展密钥用法

这是两个不同的问题:

  • 密钥用法(RFC 5280 §4.2.1.3)限制密钥可执行的密码运算:digitalSignature、keyEncipherment、keyAgreement、keyCertSign、cRLSign 以及几个较少见的位。对于 ECDSA 叶子证书,《基线要求》要求设置 digitalSignature,并禁止设置 keyEncipherment(EC 密钥不能用于加密)。RSA 叶子证书可以额外设置 keyEncipherment,以支持旧式的 TLS 1.2 RSA 密钥交换,《基线要求》称之为对“不安全密码套件”的支持。《基线要求》还规定,该扩展应当(SHOULD)存在并标记为关键(critical),本例正是如此。
  • 扩展密钥用法(§4.2.1.12)列出可用的应用:serverAuth(1.3.6.1.5.5.7.3.1)、clientAuth(…3.2)、codeSigning(…3.3)、emailProtection(…3.4)、timeStamping(…3.8)。TLS 服务器证书必须(MUST)包含 serverAuth,不得(MUST NOT)包含代码签名、电子邮件、时间戳、OCSP 签名或 anyExtendedKeyUsage(《基线要求》§7.1.2.7.10)。

发生变化的是 clientAuth。《基线要求》仍然写的是“可以”(MAY),但 Chrome 根证书计划策略 1.8 要求证书层级“专用于 TLS 服务器身份验证”:自2026年6月15日起披露的下级 CA 只能声明 serverAuth,自2027年3月15日起签发的订户证书也必须如此。Let's Encrypt 于2026年2月11日从默认配置文件中移除了 clientAuth,并于2026年7月8日彻底停止签发带此用途的证书;上面的 letsencrypt.org 叶子证书只有 serverAuth。如果你的服务曾用公共 TLS 证书对客户端做身份验证,现在就需要私有 CA(参见 mTLS 详解)。

验证器会强制检查 EKU。下面用一个临时 CA 签发一张 EKU 只含 clientAuth 的证书:

openssl verify -CAfile root.pem -purpose sslserver worker-7.pem,然后改用 -purpose sslclient
CN=worker-7
error 26 at 0 depth lookup: unsuitable certificate purpose
error worker-7.pem: verification failed
worker-7.pem: OK

这就是 OpenSSL 的错误 26;Go 把同样的情况报告为 x509: certificate specifies an incompatible key usage。

基本约束:谁可以签发证书

基本约束包含一个布尔值和一个数字。CA:TRUE 允许用该证书的密钥签发其他证书;pathlen 限制其下最多还能有多少级中间 CA。RFC 5280 §4.2.1.9 明确规定,在未声明 cA 的情况下,“被认证的公钥不得(MUST NOT)用于验证证书签名”。沿 letsencrypt.org 的证书链向上看:

证书基本约束密钥用法
CN=letsencrypt.org关键,CA:FALSEDigital Signature
CN=YE2(中间证书)关键,CA:TRUE, pathlen:0Digital Signature、Certificate Sign、CRL Sign
CN=Root YE(由 ISRG Root X2 交叉签名)关键,CA:TRUECertificate Sign、CRL Sign

YE2 上的 pathlen:0 表示它只能签发叶子证书。为了观察这些规则的实际执行,实验用一张普通服务器证书(CA:FALSE,密钥用法为 digitalSignature)的密钥签发了另一张证书:

openssl verify -CAfile root.pem -untrusted api.example.test.pem forged.example.test.pem
CN=api.example.test
error 79 at 1 depth lookup: invalid CA certificate
CN=api.example.test
error 32 at 1 depth lookup: key usage does not include certificate signing
error forged.example.test.pem: verification failed

两条规则都生效了:缺少 CA:TRUE 触发错误 79,缺少 keyCertSign 触发错误 32。2002年,Internet Explorer 正是因为跳过了这项检查,才会把任意叶子证书当作 CA 接受。Python 3.13 及更高版本还会启用 OpenSSL 的严格模式,它会额外拒绝基本约束未标记为关键的 CA 证书。

颁发机构密钥标识符与主体密钥标识符

主体密钥标识符(SKI)标识证书自身的密钥;颁发机构密钥标识符(AKI)标识为该证书签名的密钥。每张证书的 AKI 都等于其上一级证书的 SKI:

letsencrypt.org 发送的每张证书的主体、SKI 和 AKI
  subject=CN=letsencrypt.org
  16:68:8D:1E:2A:32:48:8C:B5:F1:10:0A:16:08:37:08:3B:9D:30:23
  B9:59:F2:8E:CF:22:F0:86:D3:37:48:FF:76:14:18:BA:82:D8:55:87
  subject=C=US, O=Let's Encrypt, CN=YE2
  B9:59:F2:8E:CF:22:F0:86:D3:37:48:FF:76:14:18:BA:82:D8:55:87
  A3:C8:26:5A:8E:A1:4C:D0:35:63:FC:9B:23:C8:3A:AE:56:F3:4F:56
  subject=C=US, O=ISRG, CN=Root YE
  A3:C8:26:5A:8E:A1:4C:D0:35:63:FC:9B:23:C8:3A:AE:56:F3:4F:56
  7C:42:96:AE:DE:4B:48:3B:FA:92:F8:9E:8C:CF:6D:8B:A9:72:37:95
  subject=C=US, O=Internet Security Research Group, CN=ISRG Root X2
  7C:42:96:AE:DE:4B:48:3B:FA:92:F8:9E:8C:CF:6D:8B:A9:72:37:95
  79:B4:59:E6:7B:B6:E5:E4:01:73:80:08:88:C8:1A:58:F6:E9:9B:6E

当多张 CA 证书同名时(例如更换过密钥或经过交叉签名的 CA,像现存的两张 ISRG Root X2 证书),密钥标识符可以帮助验证器选出正确的颁发者。它们本身什么也证明不了,起证明作用的是签名检查。RFC 5280 要求除自签名根证书以外的所有证书都包含 AKI,所有 CA 证书都包含 SKI;RFC 5280 建议的一种 SKI 计算方法是对密钥做 SHA-1 哈希,但颁发者可以使用任何唯一值。《基线要求》把叶子证书中的 SKI 列为“不推荐”(NOT RECOMMENDED);Let's Encrypt 和 Sectigo 目前仍然都会包含它。遇到不含 AKI 的 CA 证书,严格的验证器会验证失败,Python 的 Missing Authority Key Identifier 报错就是一例。

这条证书链还说明,你验证的路径并不总是服务器发送的路径。letsencrypt.org 发送了四张证书,最后一张是由 ISRG Root X1 交叉签名的 ISRG Root X2;而使用 Debian 信任库的 OpenSSL 会在它已经信任的 X2 根证书处停止:

openssl verify -show_chain -untrusted untrusted.pem letsencrypt.org-1.pem
letsencrypt.org-1.pem: OK
Chain:
depth=0: CN=letsencrypt.org (untrusted)
depth=1: C=US, O=Let's Encrypt, CN=YE2 (untrusted)
depth=2: C=US, O=ISRG, CN=Root YE (untrusted)
depth=3: C=US, O=Internet Security Research Group, CN=ISRG Root X2

CRL 分发点、AIA 与 OCSP

有两个扩展指向 URL:

  • CRL 分发点(RFC 5280 §4.2.1.13):颁发者发布其 CRL 的位置。除非证书属于短期证书(自2026年3月15日起指有效期不超过 7 天的证书)或带有 OCSP URL,否则《基线要求》要求包含此扩展;而且该 URL 必须是普通的 http:CRL 本身带有签名,TLS 不会带来额外保障,反而会造成循环。
  • 颁发机构信息访问(§4.2.2.1):CA Issuers 指明从哪里下载签发 CA 的证书,OCSP 则是颁发者的 OCSP 响应器。

Let's Encrypt 的叶子证书有 CRL URL,没有 OCSP URL:在《基线要求》把 OCSP 改为可选之后,它于2025年5月从证书中去掉了 OCSP URL,并于2025年8月6日关闭了 OCSP 响应器。Sectigo 仍在运行 OCSP;github.com 的叶子证书同时具有这两种访问方式,并带有两项策略:

openssl x509 -text,github.com 叶子证书(节选)
            X509v3 Certificate Policies: 
                Policy: 1.3.6.1.4.1.6449.1.2.2.7
                  CPS: https://sectigo.com/CPS
                Policy: 2.23.140.1.2.1
            Authority Information Access: 
                CA Issuers - URI:http://crt.sectigo.com/SectigoPublicServerAuthenticationCADVE36.crt
                OCSP - URI:http://ocsp.sectigo.com

下载 letsencrypt.org 叶子证书所指向的 CRL,并用 YE2 证书检查:

下载证书中指定的 CRL 并检查其签名
crl=$(openssl x509 -in letsencrypt.org-1.pem -noout -ext crlDistributionPoints | grep -o 'http://[^ ]*')
curl -sS -o leaf.crl "$crl"
openssl crl -inform DER -in leaf.crl -noout -issuer -lastupdate -nextupdate
openssl crl -inform DER -in leaf.crl -noout -text | grep -c 'Serial Number:'
openssl crl -inform DER -in leaf.crl -noout -CAfile letsencrypt.org-2.pem
输出
issuer=C=US, O=Let's Encrypt, CN=YE2
lastUpdate=Oct 20 15:03:57 2026 GMT
nextUpdate=Oct 29 15:03:56 2026 GMT
1590
verify OK

URL 中的 96.crl 是一个分片:CA 把吊销记录分散到许多小型 CRL 中,并在每张证书中写明对应的那一个,因此客户端只需下载 1,590 条记录,而不是全部吊销记录。浏览器很少实时获取 CRL;Chrome 和 Firefox 会自行把吊销数据推送给客户端。相关取舍见 CRL vs OCSP。

服务器忘记发送中间证书时,CA Issuers 就派上了用场。浏览器以及 Windows 和 macOS 的平台验证器会沿着它获取中间证书(即“AIA chasing”);OpenSSL(以及 Linux 上依赖它的 curl、Python 和 PHP)、Linux 上的 Go,以及默认设置下的 Java 则不会这样做。因此,同一台配置错误的服务器在浏览器中能正常访问,在代码中却会报错“unable to get local issuer certificate”。

证书策略

每张公共 TLS 证书都恰好声明一个 CA/Browser Forum 策略标识符,说明主体经过了哪种验证:2.23.140.1.2.1 为域名验证,2.23.140.1.2.2 为组织验证,2.23.140.1.2.3 为个人验证,2.23.140.1.1 为扩展验证(《基线要求》§7.1.6.1)。CA 还会添加自己的策略 OID,以及指向其认证业务声明(CPS)的链接,上面的 Sectigo 就是如此。显示验证级别的客户端读取的是这个扩展,而不是主体。

签名证书时间戳

签发之前,CA 会把一张预证书(内容相同,外加一个毒化扩展)提交到证书透明度日志,每个日志都会返回一份带签名的承诺,保证会发布它,这就是 SCT。CA 把这些 SCT 嵌入最终证书中(OID 1.3.6.1.4.1.11129.2.4.2,RFC 6962 §3.3)。日志 ID 是日志密钥的哈希;Chrome 的日志列表把它对应到日志名称:

scts.py:列出每个嵌入的 SCT 所对应的 CT 日志名称(cryptography 49.0.0)
"""List the SCTs embedded in a certificate and name their logs from Chrome's CT log list."""
import base64
import json
import sys

from cryptography import x509

log_list = json.load(open(sys.argv[1]))
logs = {}
for operator in log_list["operators"]:
    for log in operator["logs"] + operator.get("tiled_logs", []):
        logs[base64.b64decode(log["log_id"])] = (operator["name"], log["description"])

cert = x509.load_pem_x509_certificate(open(sys.argv[2], "rb").read())
scts = cert.extensions.get_extension_for_class(x509.PrecertificateSignedCertificateTimestamps).value
for sct in scts:
    operator, name = logs.get(sct.log_id, ("unknown operator", "unknown log"))
    print(f"{sct.timestamp:%Y-%m-%d %H:%M:%S} UTC  {operator}: {name}  ({sct.entry_type.name})")
python3 scts.py log_list.json letsencrypt.org-1.pem,然后是 github.com-1.pem
2026-09-04 15:33:02 UTC  Google: Google Xenon2026h2  (PRE_CERTIFICATE)
2026-09-04 15:33:02 UTC  Geomys: Geomys Tuscolo2026h2  (PRE_CERTIFICATE)
2026-09-01 00:10:32 UTC  Google: Google Argon2026h2  (PRE_CERTIFICATE)
2026-09-01 00:10:31 UTC  Sectigo: Sectigo Tiger2026h2  (PRE_CERTIFICATE)

日志列表是同一天从 https://www.gstatic.com/ct/log_list/v3/log_list.json 下载的。每张证书都有两个 SCT,分别来自两个不同的运营方,这正是 Chrome 的 CT 策略对有效期不超过 180 天的证书的要求(有效期更长的证书需要三个)。Tuscolo SCT 上的 Extensions: 00:00:05:00:24:DF:DB:0C 是 leaf_index 扩展,实现静态 CT API 的日志必须添加它:类型 0,长度 5,外加一个 40 位的索引,这里是该日志中索引为 618,650,380 的条目。正是因为有了 SCT,任何人都能搜索为自己的域名签发的所有证书,错误签发的证书也无法隐瞒。SCT 解码器无需命令行即可解码 SCT。

常见错误

  • 名称只写在 CN 中的证书。浏览器会报 NET::ERR_CERT_COMMON_NAME_INVALID,Java 会报 No subject alternative names present。每个名称(包括 CN 中的那个)都要写进 SAN。这个问题最常见于内部 CA,以及只设置了 -subj 的 openssl req 单行命令。
  • 缺少中间证书。在浏览器以及 Windows 和 macOS 的平台验证器中,借助 AIA chasing 可以正常工作;在基于 OpenSSL 的客户端、Linux 上的 Go 和 Java 中则会验证失败。服务器应发送完整的证书链(叶子证书在前,根证书可选)。
  • 依赖公共证书中的 clientAuth。公共 TLS 证书体系正在逐步取消它;合作方集成如果固定接受“任何带 clientAuth 的公共证书”,就需要改用私有 CA。
  • CA 证书设置了 CA:TRUE,却缺少 keyCertSign,或者没有标记为关键。严格的验证器(Python 3.13+,以及针对部分字段的 Go)会拒绝这种证书;Go 的报错是 parent certificate cannot sign this kind of certificate。
  • 把 SKI 当作指纹。SKI 的值完全由 CA 决定。要标识一张证书,应对整个 DER 计算哈希(openssl x509 -fingerprint -sha256);要标识一个密钥,应对 SPKI 计算哈希。
  • 按本地时间解读有效期。notBefore 和 notAfter 都是 UTC 时间。服务器时钟只要慢了几个小时,就会以尚未生效为由拒绝刚签发的证书。

参考资料