发布于
TLS 证书是一份经过签名的声明:这把公钥属于这些名字,由这个签发方背书,有效期到这一天为止。你会遇到的几乎每一个证书问题,都是这四句话中某一句不成立;而只要知道该看哪里,读一遍证书大约三十秒就能定位是哪一句。
证书到底声明了什么
剥掉编码,一张 X.509 证书里装的是一把公钥、一组名字、一个有效期窗口、若干用途约束,以及签发方对上述全部内容的签名。私钥不在里面,也从不随证书一起传输,所以证书是公开信息——而且事实上已经公开在证书透明度日志里。
浏览器做出的信任判断,就是把这同一份声明重复若干次形成的链条。你服务器的证书由中间 CA 签名;中间 CA 的证书由根 CA 签名;根 CA 的证书位于操作系统或浏览器的信任库中,由一个负责审计 CA 的项目放进去。只要每一环都验签通过、名字匹配上、且没有任何一环过期或被吊销,这条连接就是受信任的。
读字段
下面是对一张为本文在本地生成的证书执行 openssl x509 -text 的真实输出,只保留了要紧的部分。你可以对磁盘上任何一张证书执行同样的命令,或者用 openssl s_client -connect example.com:443 -servername example.com 抓一张线上的再管道进去。
主体里的 Common Name 是所有人第一眼会看、却最不重要的字段。多年以来,主机名匹配只针对 Subject Alternative Name 扩展进行——Chrome 在 2017 年停止回退到 CN,其他主流浏览器随后跟进。一张 CN 写着你的主机名、但 SAN 列表里没有它的证书,一定会被拒绝。排查名字不匹配时,请读 SAN,忽略 CN。
证书有效期也比资深工程师印象中短得多。CA/Browser Forum 的基线要求在 2020 年把公共证书上限压到 398 天,而新一轮缩短已经生效:2026 年 3 月 15 日起签发的证书上限为 200 天,2027 年 3 月降至 100 天,2029 年 3 月降至 47 天。任何依赖「某个人记得去续期」的流程都已经过时,请用 ACME 自动化。
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
28:c5:03:7d:36:fe:f0:59:70:25:8c:88:ad:46:36:dd:2c:9f:04:5d
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=US, O=Example Demo CA, CN=Example Demo Root CA
Validity
Not Before: Sep 21 07:06:49 2026 GMT
Not After : Dec 20 07:06:49 2026 GMT
Subject: C=US, O=Example Ltd, CN=www.example.com
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:www.example.com, DNS:example.com
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Subject Key Identifier:
0E:7D:D3:4E:51:37:FB:5A:28:E3:BE:BC:1B:53:18:8A:CC:E4:B5:D2
X509v3 Authority Key Identifier:
FC:D1:70:93:E7:33:87:D7:E2:87:45:2D:53:7D:77:A4:FD:8F:E8:96
# 这张证书及签发它的 CA 都是撰写本文时在本地生成的。
# 字段值是真实的,但这个 CA 不被任何信任库承认——
# 这也正是下面证书链那一节重要的原因。SAN、通配符与名字匹配
Subject Alternative Name 扩展是一个列表,只有当某个主机名出现在其中时,证书对它才有效。条目通常是 DNS 名;也可以是 IP 地址,这就是「给裸 IP 签证书」的做法;在非 Web 用途中还可以是邮箱地址或 URI。
通配符条目有一条规则反复把人绊倒:通配符只替换恰好一个标签,而且只能是最左边那个。*.example.com 覆盖 www.example.com 和 api.example.com;它不覆盖 example.com 本身——顶级域名需要自己的 SAN 条目,这也是几乎所有真实证书都会把两者都列上的原因;它同样不覆盖 a.b.example.com,因为那深了两个标签。如果某个「子域的子域」失败而它的兄弟域正常,原因通常就在这里。
证书链的构建,以及为什么问题多半出在这里
客户端连上来时,服务器会发送自己的证书,并且应当把抵达受信根所需的每一张中间证书一起发出去。根证书本身不该发——客户端本来就有。
漏发中间证书是最常见的单一 TLS 配置错误,而且有一个特征鲜明的表现:站点在你的浏览器里正常,在 curl、移动 App 或服务端到服务端调用里失败。原因是浏览器会悄悄替你把错误抹平:多数桌面浏览器会缓存在别处见过的中间证书,还会顺着 Authority Information Access 扩展通过 HTTP 去抓缺失的签发方证书。命令行客户端和许多语言运行时两者都不做,于是它们看到一条到不了根的链,直接拒绝。如果同事说站点没问题而你的部署脚本不同意,先查链,别的都放后面。
修法是把完整链发出去:把叶子证书和各级中间证书按「叶子在前」的顺序拼进一个 PEM 文件,让服务器指向它。多数 ACME 客户端正是为此生成 fullchain.pem,而出错通常是配置指向了 cert.pem。
$ openssl s_client -connect example.com:443 -servername example.com \
-showcerts < /dev/null
# 看输出顶部的 "Certificate chain" 块。你想看到的是:
# 0 s:CN=www.example.com i:CN=Some Intermediate CA
# 1 s:CN=Some Intermediate CA i:CN=Some Root CA
#
# 只有 depth 0 就说明中间证书缺失。浏览器可能把这个问题
# 掩盖过去,curl 和多数 SDK 不会。
$ openssl verify -untrusted chain.pem leaf.crt
leaf.crt: OK文件格式:别人发给你的这个文件里装了什么
证书的文件扩展名近乎没有意义——.cer 既可能是 PEM 也可能是 DER,.crt 既用于叶子证书也用于 CA 捆绑包——所以请按内容而不是按文件名判断。文件开头是五个连字符加 BEGIN,那就是 PEM:DER 字节外面套了一层 Base64,标签会告诉你里面是什么。第一个字节是 0x30,那就是裸 DER。
真正要紧的区分只有一条:这个文件里有没有私钥。PEM 证书和 PKCS#7 捆绑包没有;PKCS#8 密钥文件和 PKCS#12 归档有。后两者请按对待任何其他秘密的方式对待:不要附在工单里,不要提交进仓库,也不要粘进网页。
| 扩展名 | 编码 | 内容 | 是否含私钥 |
|---|---|---|---|
| .pem | 带 BEGIN/END 标签的 Base64 文本 | 一张或多张证书,和/或一把密钥——看标签 | 只有出现 KEY 标签时才有 |
| .crt / .cer | PEM 或 DER——查开头几个字节 | 一张证书,或拼接起来的捆绑包 | 否 |
| .der | 二进制 DER | 恰好一张证书 | 否 |
| .key | 通常是 PEM 包裹的 PKCS#8 或 PKCS#1 | 一把私钥,可能带口令加密 | 是 |
| .csr | PEM,标签为 CERTIFICATE REQUEST | 公钥加申请的名字,自签以证明持有私钥 | 否 |
| .p12 / .pfx | 二进制 PKCS#12,有口令保护 | 叶子证书、它的链和私钥打包在一起 | 是 |
| .p7b / .p7c | PKCS#7,PEM 或 DER | 只有证书——通常是供导入的证书链 | 否 |
| .jks | Java KeyStore,有口令保护 | Java 专用容器里的证书与密钥 | 通常有 |
指纹,以及它标识的到底是什么
证书指纹就是一个哈希——通常是 SHA-256——对证书完整的 DER 编码计算得出。它不是证书里的某个字段,而是从证书派生出来的;正因如此,不同工具算出的指纹总是一致,而任何字段的任何改动都会产生一个完全不同的指纹。
这个派生关系带来一个常被搞错的实际后果:续期会产生新的指纹,哪怕密钥、名字和 CA 都没变,因为序列号和有效期变了。于是「固定证书指纹」的系统会在每次续期时失效——在 200 天、很快 100 天的有效期下,这意味着一年好几次。
如果确实需要固定(pinning),请改为固定 Subject Public Key Info 的哈希。只要你复用同一对密钥,SPKI 就能跨越续期存活,这才是你真正想要的行为。并且要带备份固定:至少准备一把离线保管的备用密钥的 SPKI,这样紧急轮换密钥时不会把所有客户端一起锁在门外。指纹仍然适合它擅长的那件事——通过带外渠道确认,你看到的这张证书和别人看到的是同一张。
读懂报错,并合理使用解析工具
浏览器的证书报错比它们吓人的外观要具体得多,每一条都对应一个你可以去读的字段。ERR_CERT_DATE_INVALID 表示当前时间落在有效期窗口之外——先看 Not After,然后确认客户端自己的时钟是对的,因为日期设错的设备会在所有站点上都报这个。ERR_CERT_COMMON_NAME_INVALID 表示这个主机名不在 SAN 列表里。ERR_CERT_AUTHORITY_INVALID,也就是 Firefox 的 SEC_ERROR_UNKNOWN_ISSUER,表示链没有抵达受信根——在生产环境里这几乎总是中间证书缺失,在企业网络里则通常是某个做 TLS 解密的代理,其根证书没有装进这个特定的信任库。ERR_CERT_REVOKED 表示 CA 已公告该证书不应再被使用。
本站的证书解析工具在页面内解析 PEM 或 DER 证书,把主体、SAN 列表、签发方、有效期、密钥用途和指纹排开展示,不上传任何内容。
两条要说在前面的边界。第一,解析工具读的是一张证书,它不构建证书链,因为它没有你的信任库、抓不到签发方证书,也无从知道你的服务器实际发出了什么。当症状是信任失败而不是某个字段写错时,你要的工具是 openssl s_client 或在线的证书链检查器。第二,有一类文件永远不该粘进它、也不该粘进任何网页:.key 文件和 .p12 归档。它们携带私钥。解析证书是在读公开信息;解析密钥文件,则是把证书存在的意义所要保护的那个东西直接交出去。如果别人发给你一个包而你不确定是哪一种,先用文本编辑器打开,搜一下有没有 PRIVATE 这个词,再决定它能不能靠近浏览器。
要点回顾
- 主机名匹配只看 Subject Alternative Name 列表——排查名字不匹配时读 SAN,忽略 Common Name。
- 通配符只覆盖恰好一个标签且永不覆盖顶级域,所以 *.example.com 匹配不了 example.com,也匹配不了 a.b.example.com。
- 「浏览器正常、curl 失败」几乎总是中间证书缺失;请按叶子在前的顺序下发完整链。
- 证书指纹每次续期都会变,因此要固定就固定 SPKI 哈希,并准备一把离线备用密钥。
- 证书是公开信息,可以随便解析;但 .key 文件和 .p12 归档含私钥,绝不要粘进任何网页工具。