HTTPS/TLS 证书:是什么、怎么签、怎么验
三件事:证书里装了什么;根 → 中间 → 叶子怎么签出来;客户端怎么验证。本文只讨论公开信任的 TLS 服务器证书。验证服务端证书,—— 确认你正在通信的对象,就是你以为的那个对象。
三件事:证书里装了什么;根 → 中间 → 叶子怎么签出来;客户端怎么验证。本文只讨论公开信任的 TLS 服务器证书。
验证服务端证书,最终是在验证 “身份” —— 确认你正在通信的对象,就是你以为的那个对象。
一、证书是什么
一张 X.509 v3 证书就是三段:
Certificate = {
tbsCertificate, // 证书正文(待签名内容)
signatureAlgorithm, // 签名算法
signatureValue // 上级 CA 用其私钥对 DER(tbsCertificate) 的签名
}
正文 tbsCertificate 里 HTTPS 真正关心的字段:
| 字段 | 含义 |
|---|---|
issuer |
签发者 DN(指向上级证书的 subject) |
subject |
本证书主体 DN |
validity |
notBefore / notAfter |
subjectPublicKeyInfo |
本证书绑定的公钥 |
subjectAltName (SAN) |
覆盖的域名 / IP,唯一的身份匹配依据 |
basicConstraints |
CA:TRUE/FALSE,能否再签下级 |
keyUsage / extendedKeyUsage |
公钥用途;TLS 服务端必须有 serverAuth |
authorityKeyIdentifier |
上级公钥的标识符;公开 TLS 证书里通常要求它与上级证书的 subjectKeyIdentifier 对应,用来快速找到上级证书 |
| 嵌入的 SCT | CT 日志的签名时间戳;公开 TLS 证书通常用它满足浏览器 Certificate Transparency 政策 |
关键直觉:签发者真正签的是 DER 编码后的 tbsCertificate 字节。改一位,验签就失败。
附带概念:
- DER:证书的二进制 ASN.1 编码;PEM:DER 的 Base64 文本外壳,编码格式本身不提供信任。
- CSR:申请材料,不是证书。它带申请者公钥,并由申请者用私钥对申请内容签名(仅证明"我持有这对私钥",不证明"我拥有这个域名")。
二、三级签发:根 → 中间 → 叶子(自己业务的服务端)
1. 层次与谁信任谁
要点:
- 客户端预存信任根,不是因为根能自验签,而是因为根被本地信任库收录。
- 根私钥离线保管,日常签发交给中间 CA;中间挂了能吊销重发,根不至于被牵连。
- 服务端只发"叶子 + 中间",不发根。
2. 每级签的是什么
signatureValue_I = Sign( RootPrivateKey, DER(tbsCertificate_I) )
signatureValue_L = Sign( IntermediatePrivateKey, DER(tbsCertificate_L) )
完全对称:上级的私钥签下级的正文字节。
3. 每级长什么样(举例)
假设有这样一条链:api.example.com 的证书由 Example TLS Intermediate CA 签发,后者由 Example Root CA 签发。
根证书 R(自签名,已在客户端信任库中)
Certificate_R {
tbsCertificate {
subject = "CN=Example Root CA"
issuer = "CN=Example Root CA" // 自签:issuer == subject
validity = 2020-01-01 .. 2040-01-01 // 常见多年,公开信任根通常远长于叶子
subjectPublicKeyInfo = RootPublicKey // ECDSA P-384
extensions {
basicConstraints = CA:TRUE
keyUsage = keyCertSign, cRLSign
}
}
signatureValue = Sign(RootPrivateKey, DER(tbs_R)) // 自己签自己
}
中间证书 I(由 R 签发,日常签发用)
Certificate_I {
tbsCertificate {
subject = "CN=Example TLS Intermediate CA"
issuer = "CN=Example Root CA" // 指向 R
validity = 2024-01-01 .. 2029-01-01 // 通常 5~10 年
subjectPublicKeyInfo = IntermediatePublicKey // ECDSA P-256
extensions {
basicConstraints = CA:TRUE, pathLenConstraint=0 // 下级不能再是 CA
keyUsage = keyCertSign, cRLSign
extendedKeyUsage = serverAuth
authorityKeyId = <Root 的 subjectKeyIdentifier> // 指认上级
}
}
signatureValue = Sign(RootPrivateKey, DER(tbs_I)) // R 用其私钥签 I
}
叶子证书 L(由 I 签发,部署到服务器)
Certificate_L {
tbsCertificate {
subject = "CN=api.example.com"
issuer = "CN=Example TLS Intermediate CA" // 指向 I
validity = 2026-04-01 .. 2026-08-01 // 当前新签发上限内(≤200 天)
subjectPublicKeyInfo = ServerPublicKey // ECDSA P-256
extensions {
subjectAltName = DNS:api.example.com, DNS:www.example.com
basicConstraints = CA:FALSE
keyUsage = digitalSignature
extendedKeyUsage = serverAuth
authorityKeyId = <Intermediate 的 subjectKeyIdentifier>
SCTList = { SCT_log1, SCT_log2 }
}
}
signatureValue = Sign(IntermediatePrivateKey, DER(tbs_L)) // I 用其私钥签 L
}
把三张证书串起来:
[ R: subject=Root, pubkey=RootPubKey, issuer=Root ] ← 自签,本地信任锚
[ I: subject=Inter, pubkey=IntermediatePubKey, issuer=Root ] ← 被 R 的私钥签
[ L: subject=api, pubkey=ServerPubKey, issuer=Inter ] ← 被 I 的私钥签
注意:每张证书里携带的是该证书主体的公钥。上级的公钥要去上级证书里拿——这是后面验签的关键。
4. 叶子证书是怎么诞生的(ACME 流程)
截至 2026-05-15,MPIC 已进入至少 3 个远端网络视角的阶段;2026-06-15 起提高到至少 4 个,2026-12-15 起提高到至少 5 个。
CA/Browser Forum 新签发 Subscriber Certificate 最大有效期(Ballot SC-081 v3,当前日期 2026-05-15):
| 签发时间 | 最大有效期 |
|---|---|
| 2026-03-15 之前 | 398 天 |
| 2026-03-15 至 2027-03-14(当前) | 200 天 |
| 2027-03-15 至 2029-03-14 | 100 天 |
| 2029-03-15 起 | 47 天 |
注:这是新签发上限。2026-03-15 前已签发、notAfter 仍在未来的 398 天证书,不会因为新上限生效而立刻失效。
三、客户端怎么验证
1. 总流程
2. 逐级验签到底是什么逻辑
核心一句话:用上级证书里的公钥,去验下级证书的 signatureValue。
为什么这样能行?签发时正是反过来:上级 CA 用自己的私钥给下级的正文签名。公私钥成对,所以拿上级的公钥就能验证这个签名。而"上级的公钥"就放在上级自己的证书里(subjectPublicKeyInfo 字段)——所以"验下级"必须"先有上级的证书"。
信任沿着链自上而下传递:
本地信任库 ──授信──▶ R(信任锚,直接被信任,不靠上级验签建立信任)
│ R 的私钥签过 I
▼ 用 R.公钥 验 I.signatureValue
I(验签通过 → I 可信)
│ I 的私钥签过 L
▼ 用 I.公钥 验 L.signatureValue
L(验签通过 → L 可信,其中的 ServerPublicKey
也可信地代表 api.example.com)
两点提醒:
- R 不是靠自验签才可信。自签最多证明它的内容完整,不能凭空产生身份信任;身份信任来自本地信任库。
- 链断在哪儿就停在哪儿。拼链时找不到上级证书,或任意一级验签失败,连接立即中止。
"验签通过"到底保证了什么
验签是一次单向数学校验,公开 TLS 证书里常见的是 RSA / ECDSA 系列签名算法。以 R 用 SHA-256 签 I 为例:
签发侧(R 持有 RootPrivateKey):
hash_I = SHA-256( DER(I.tbsCertificate) )
sig_I = Sign( RootPrivateKey, hash_I )
把 sig_I 写进 I.signatureValue
验签侧(客户端已信任 R):
hash_I' = SHA-256( DER(I.tbsCertificate) ) // 对收到的正文重新哈希
ok = Verify( RootPublicKey, hash_I', sig_I) // 用 R 证书里的公钥校验
在签名算法安全成立时,Verify 返回 true 能让客户端得到两个结论:
sig_I必须由与 RootPublicKey 配对的那把私钥生成;hash_I'必须与签发时的hash_I逐位相等。
于是一次性同时锁住了身份与内容一致:
| 攻击者想做 | 为什么做不到 |
|---|---|
不知道 R 的私钥,凑一个能验过的 sig_I |
没私钥就无法生成对应公钥能验过的签名(RSA / 离散对数难题) |
改 tbsCertificate 任一位(比如换 subject) |
改了字节 → hash_I' 变了 → 与原 sig_I 对不上 → Verify 失败 |
| 改正文 + 重算哈希 + 重新签 | 重签需要 R 的私钥,回到第 1 行 |
| 找一份"不同正文但哈希相同"的伪造正文 | 需破坏 SHA-256 抗碰撞性;SHA-1 被禁用正是因为这条防线已破 |
所以"验签通过"等价于:手上这份 tbsCertificate 字节,确实是当年握有 RootPrivateKey 的人(即 R)盖章认可的原始字节,一位没改。 正文里写着 subject = Example TLS Intermediate CA、subjectPublicKeyInfo = IntermediatePublicKey,于是这把中间公钥的身份就被钉死了,可以拿去验下一级 L。
常见误解:不要把验签理解成把
signatureValue解开后再逐字比较正文。不同算法内部细节不同,但对调用方来说都是Verify(公钥, 消息或消息哈希, 签名) → 布尔值的校验;攻击者看到全部signatureValue字节也无法反推私钥。
3. 合规检查与主机名匹配
验签只能证明"没被改、确实由上级签出"。还要检查:
| 项 | 内容 |
|---|---|
| 有效期 | 叶子和中间必须在 notBefore/notAfter 之内;信任锚的有效期处理取决于客户端实现和根库策略 |
basicConstraints |
中间必须 CA:TRUE;叶子不得是 CA(若有该扩展,应为 CA:FALSE) |
keyUsage / extendedKeyUsage |
中间需 keyCertSign;现代 TLS 叶子通常需 digitalSignature + serverAuth(兼容旧 TLS 1.2 RSA 密钥交换时可能还会有 keyEncipherment) |
nameConstraints |
中间 CA 不能越权签限定外的域名 |
| 算法强度 | 拒绝 SHA-1、过短 RSA 等 |
| SCT | Chrome、Safari 等浏览器要求公开 TLS 证书满足 CT 政策;所需 SCT 数量和日志要求随浏览器政策变化 |
| 吊销状态 | 取决于客户端策略;浏览器通常结合 CRL/OCSP/厂商维护的吊销列表等机制,不同客户端是否硬失败并不一致 |
主机名匹配应看 SAN,现代规范不再依赖 CN(RFC 9525;少数旧客户端可能仍有兼容回退):
| 访问目标 | 证书 SAN | 结果 |
|---|---|---|
api.example.com |
DNS:api.example.com |
通过 |
api.example.com |
DNS:*.example.com |
通过 |
example.com |
DNS:*.example.com |
不通过(不含裸域) |
a.b.example.com |
DNS:*.example.com |
不通过(* 只代表一个 DNS 标签,不跨点号) |
192.0.2.10 |
iPAddress:192.0.2.10 |
通过 |
192.0.2.10 |
DNS:192.0.2.10 |
不通过(须用 iPAddress) |
4. 私钥归属:CertificateVerify
证书是公开的,攻击者能复制。要证明"对面这台机器真有 ServerPrivateKey",TLS 1.3 让服务端用对应私钥做一次握手签名:
// RFC 8446 §4.4.3:64 个 0x20 + 上下文串 + 0x00 + 握手哈希
toBeSigned = 0x20 × 64
|| "TLS 1.3, server CertificateVerify"
|| 0x00
|| TranscriptHash(ClientHello ... Server Certificate)
certificateVerify = Sign(ServerPrivateKey, toBeSigned)
客户端:Verify(ServerPublicKey from L, toBeSigned, certificateVerify)
前缀那 64 个 0x20 是为了防止跨协议碰撞(其他场景下不可能产生同样前缀的待签数据)。
这把"证书里的身份"绑到了"本次连接对面那台机器"。最后双方再交换 Finished(基于握手密钥的 HMAC),把整段握手钉死,防降级、防拼接。
四、延伸:嵌入式客户端把"中间 CA"作为信任锚
本节是嵌入式工程里的特殊信任锚模式,不是公开 WebPKI 客户端的默认模型。
1. 代码里如何处理
底层(mbedTLS / cm_ssl)做证书链验证时,需要一张本地"信任"的证书作为信任锚(trust anchor)。嵌入式工程把中间 CA 证书塞进信任列表,链验证在中间处即停止向上追溯:中间本身由本地配置直接授信,不再验证它是否被某个根正确签过,也不再需要根证书参与。
本地信任库 ──授信──▶ I(中间 CA,本工程的信任锚)
│ I 的私钥签过 L
▼ 用 I.公钥 验 L.signatureValue
L(验签通过 → L 可信)
后果:服务端链中"中间以上"的部分完全不参与(根是否过期、是否在公共信任库都无关);叶子的验签只做一次;只信任由这张特定中间签出的叶子,自带一层 CA 钉扎(CA pinning)效果。主机名匹配仍看叶子证书的 SAN;叶子的有效期、KU/EKU、basicConstraints 等检查仍应照常开启。至于信任锚证书自身的有效期、keyUsage、nameConstraints 等是否以及如何检查,取决于具体 TLS 库和配置,工程上应按"会检查"来设计,并在目标固件里实测。
注:本节以 mbedTLS 类实现作参考。若使用 cm_ssl 或其他库,应单独确认信任锚加载、多信任锚选择顺序、时间校验、CRL/OCSP、basicConstraints、keyUsage、nameConstraints 的实际行为。
2. 为什么会这样选
- 省 flash:一张中间证书 ≈ 1–2 KB;完整公共根列表几十到上百 KB。
- 省内存与启动开销:mbedTLS 的加载、解析、构链开销随信任锚数量线性增长。
- 业务面对单一服务:设备只连一类后端,没必要让它能信"整个互联网"。
- 顺带 pinning:即使设备厂的根列表被污染,也不影响这条链路。
3. 代价与风险
| 风险 | 说明 | 触发条件 |
|---|---|---|
| 中间证书到期(最大) | 很多库会检查信任锚自身的 notAfter;即使库不查,服务端也可能已切到新中间 |
到期日;若 OTA 也依赖同一信任锚,会被锁死 |
| 中间证书被服务商轮换 | 服务端叶子改用另一张中间 CA 签发 | 任意时间,CA 自己决定 |
| 中间证书被吊销 | 私钥泄漏等事件触发 CA 紧急吊销;设备若检查吊销会失败,若不检查则有安全风险 | 罕见但发生过 |
| 服务器迁移到其他 CA | 叶子完全不在这条链下 | 业务方决策 |
| 无法接公共 CA 的别的服务 | 同一份 SSL 配置只能信这一张中间签出的服务 | 复用配置时易踩坑 |
上述任一情况发生时,若设备的业务连接和 OTA 更新都依赖同一份信任锚,设备可能无法自我修复,只能通过已预留的备用信任锚或线下/旁路升级换 PEM。
4. 缓解建议
- 监控到期日:把内置 CA 的
notAfter写进 CI/构建产物清单,提前至少 12 个月开始告警和切换演练。 - 预留多张信任锚:把当前在用 + 备用的中间 CA 一起编进固件,并实测目标 TLS 库的匹配顺序和失败回退;服务商轮换时无需紧急 OTA。
- 做过渡发布:先发布包含"旧中间 + 新中间"的固件,确认服务端完成换链后,再在后续版本移除旧中间。
- 必要时下沉到根:若 flash 允许,直接把对应的根 CA(选剩余有效期足够长的版本,例如 ECC 根 GlobalSign Root R6、ISRG Root X2 等)编进信任锚,可一次覆盖该 CA 旗下多张中间,进一步降低轮换风险。
五、常见错误速查
| 现象 | 原因 | 处理 |
|---|---|---|
certificate expired / not yet valid |
证书过期或客户端时钟错 | 续期;校时 |
hostname mismatch |
SAN 不含访问域名 | 重签覆盖正确域名 |
unable to get local issuer certificate |
服务端漏发中间证书,或客户端缺少对应上级/信任锚 | 补齐中间链;确认客户端信任库 |
untrusted root |
根不在客户端信任库(如企业自签根),或信任锚不符合本地策略 | 安装企业根或换公开 CA 签的链 |
self-signed certificate in certificate chain |
链中含未被信任的自签证书(多为配置错误) | 改用由上级 CA 正常签发的证书 |
| 算法被拒 | SHA-1、RSA < 2048 位等弱算法/弱参数 | 换 SHA-256+ / RSA >= 2048 / ECDSA |
| 缺 SCT 被拒 | Chrome/Safari 等浏览器要求公开 TLS 证书满足 CT 政策 | 用支持 CT 的公开 CA 重签 |
服务端链文件只放:叶子 + 必要中间(不放根)。
排查命令:
openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts
openssl x509 -in cert.pem -noout -subject -issuer -dates \
-ext subjectAltName,extendedKeyUsage,basicConstraints
openssl verify -purpose sslserver -verify_hostname api.example.com \
-CAfile root.pem -untrusted intermediate.pem cert.pem
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)