安全指南

如何读懂一张 TLS 证书:字段、证书链、文件格式与信任报错

X.509 证书各字段的含义、证书链如何构建又为何断裂、PEM / DER / PFX / P7B 里到底装了什么,以及随之而来的浏览器报错怎么读。

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 自动化。

openssl x509 -in leaf.crt -noout -text(节选)
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 / .cerPEM 或 DER——查开头几个字节一张证书,或拼接起来的捆绑包
.der二进制 DER恰好一张证书
.key通常是 PEM 包裹的 PKCS#8 或 PKCS#1一把私钥,可能带口令加密
.csrPEM,标签为 CERTIFICATE REQUEST公钥加申请的名字,自签以证明持有私钥
.p12 / .pfx二进制 PKCS#12,有口令保护叶子证书、它的链和私钥打包在一起
.p7b / .p7cPKCS#7,PEM 或 DER只有证书——通常是供导入的证书链
.jksJava 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 归档含私钥,绝不要粘进任何网页工具。

继续了解相关检查与工具