证书与 PKI

本页目录

证书把一个公钥与身份、用途、有效期绑定起来,由签发者签名。客户端接受证书,还要有可信路径和应用身份校验;TLS 握手再证明对端掌握相应私钥。能读出一张证书,甚至能验证它的自签名,都不等于已经知道对方是谁。

以 https://api.example.com:8443/orders 为贯穿例子:连接目的地址来自解析,参考身份来自应用访问目标,证书提供可核验的身份声明。DNS 返回了哪个 IP、服务端发来了哪个名字,都不能替客户端决定应该信任谁。

要解决的问题查阅位置
SAN、CN、端口和通配符怎么匹配服务身份
已有根证书为何还报链不完整信任路径
DNS-01 到底写什么 TXTACME 三种挑战
证书没过期为什么仍应拒绝吊销、用途和客户端策略
CT 能否阻止错误签发公开审计
续期成功但线上仍过期发布闭环

先分清四种对象

对象携带什么不能替代什么
私钥签名或其他私钥运算所需秘密不能随证书公开分发
证书公钥、身份、约束、签发者签名不证明当前对端持有私钥
CSR请求签发的信息与申请密钥签名不是 CA 已认可的证书
信任库客户端认可的锚点及相关策略不是服务端随意提供的证书集合

证书常以 DER 二进制编码,PEM 是带边界标记的文本包装。文件叫 .pem 不足以判断里面是证书、私钥还是多张证书;应读取实际对象。证书自身的签名算法由签发者使用,SubjectPublicKeyInfo 描述的是持有者公钥算法,这两者也不必相同。

服务身份:先确定要访问谁,再比较 SAN

对本篇 HTTPS 场景,匹配 SAN 的 dNSName 或 iPAddress。不要回退用 Subject CN 冒充 DNS 身份。域名按适用规则处理大小写与国际化名称;通配符只能占最左侧完整标签,并且只覆盖一级。

证书 SAN访问目标名称结果
DNS:api.example.comhttps://api.example.com:8443/匹配;端口不写入 DNS SAN
DNS:*.example.comhttps://api.example.com/匹配
DNS:*.example.comhttps://example.com/不匹配裸域
DNS:*.example.comhttps://v2.api.example.com/不匹配多级
DNS:api.example.comhttps://192.0.2.10/不匹配;需适当 IP SAN

SNI 帮服务器选择虚拟主机证书,不替客户端执行名称验证。DNS CNAME 指向托管商,也不意味着可以拿托管商任意名字代替原始服务身份。这里是名称匹配结果,最终认证还取决于链与其他检查。RFC 9525:服务身份

信任路径:签名有效只是其中一个条件

信任从本地锚点开始;服务端发送的根证书不能自行建立信任 本地信任锚 由客户端策略配置 中间 CA 受上级约束 服务证书 api.example.com 验签之外,还要检查时间、CA 资格、用途、名称约束等 服务身份匹配 + TLS 私钥持有证明 链可信、名字正确、握手证明,承担不同职责 构建路径可有多个候选;同一公钥的交叉签名证书可能通向不同信任锚。

典型服务端发送叶证书和必要中间证书,通常不需要发送根。客户端用本地信任材料构建候选路径,再验证签名、有效期、关键扩展及约束。不能因为 Issuer 文本等于另一个证书 Subject 就跳过验签;AKI/SKI 可帮助选择候选,也不是独立信任来源。

字段或扩展阅读重点
serialNumber与签发者共同标识证书,不是全网唯一 ID
BasicConstraintsCA 资格;pathLenConstraint 限制路径中的非自颁发中间 CA 数量
KeyUsage如 digitalSignature、keyCertSign,不能按名称随意通用
ExtendedKeyUsage如 serverAuth、clientAuth;与 KU 同时存在时需同时满足
NameConstraints限制下级可使用的名字范围
critical 扩展无法处理的关键约束不能直接忽略

中间 CA 的 pathLenConstraint=0 仍可签发叶证书;它不是“不能签任何证书”。根证书常自签,但信任来自外部配置,不来自循环验证自己的签名。交叉签名会让路径构建出现多个候选,因此一台客户端可用而另一台失败,可能源于锚点、链构建能力或策略差异。RFC 5280:证书与路径

例如服务端只发送叶证书:浏览器可能已有中间证书缓存而连接成功,新容器却失败。应补齐服务链,不能把“这台浏览器能打开”当作链完整的证明。公开 Web PKI 与自建企业 PKI 的信任范围也不同;安装企业根会扩大该客户端可接受的签发来源。

链能连上,为什么仍要拒绝?

先输入裸域example.com或多级v2.api.example.com,观察通配符不能覆盖它们。恢复名称后,pathLen=0仍允许直接签叶证书;只有再插入一个CA才触发这项约束。其他检查仍独立保留。

正在呈现知识画面
链能连上,为什么仍要拒绝?

路径构建找到候选关系,不等于路径验证成功。名称匹配、用途、时间和CA约束是同时需要满足的条件,不由一个有效签名代替。

ACME:证明域名控制权,再为申请公钥签发

ACME 账户密钥用于认证账户请求,证书密钥用于服务握手;它们承担不同职责。订单描述标识符,authorization/challenge 记录控制权验证,CSR 提交待签公钥。订单 ready 后 finalize,处理完成到 valid 才能下载证书;一次 POST 成功不代表所有步骤结束。

令 keyAuthorization = token + "." + accountKeyThumbprint,其中 thumbprint 是账户公钥 JWK 的规范化指纹。两种基础挑战的承载如下:

挑战CA 检查的位置放置的值
HTTP-01目标域名的 HTTP 80 端口、规定 challenge 路径keyAuthorization 原文
DNS-01_acme-challenge.<域名> 的 TXTbase64url(SHA256(keyAuthorization)),无填充

申请 *.example.com 时 DNS 验证使用 _acme-challenge.example.com。同名可能同时存在多个挑战 TXT,自动化应按各自值维护,避免并发订单互相覆盖。发布后应确认权威应答与验证可见性,不能把本地递归缓存命中视作全局就绪。RFC 8555:订单与挑战

TLS-ALPN-01 使用 443 端口并协商 acme-tls/1;证明材料放在专用自签证书的关键 acmeIdentifier 扩展里,是 keyAuthorization 的 SHA256 摘要。ALPN 字符串本身并不携带挑战值。该专用证书还需相应单一 DNS SAN,不能拿普通服务证书直接替代。RFC 8737

可运行算例:核对挑战字节而不是猜 TXT

下面使用教学 token 和已给定的指纹,演示三种承载差别;不实现 JWK 规范化、签名请求或真实 CA 验证。

import base64
import hashlib

def b64url(data):
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode('ascii')

token = 'teaching-token'
thumbprint = b64url(hashlib.sha256(b'teaching-account-key').digest())
key_auth = (token + '.' + thumbprint).encode('ascii')
digest = hashlib.sha256(key_auth).digest()
dns_txt = b64url(digest)
# acmeIdentifier 的 extnValue 内层是 DER OCTET STRING:tag + length + 32 B。
acme_identifier_der = bytes([0x04, 0x20]) + digest
assert len(digest) == 32 and len(dns_txt) == 43
assert base64.urlsafe_b64decode(dns_txt + '=') == digest
assert acme_identifier_der[:2] == bytes([4, 32])
assert dns_txt != key_auth.decode('ascii')
assert hashlib.sha256(key_auth + b' ').digest() != digest
print('HTTP 原文、DNS 摘要编码、TLS 扩展内层长度:', len(key_auth), len(dns_txt), len(acme_identifier_der))

有效期、吊销与状态新鲜度

证书未过期不等于私钥从未泄露。吊销表达签发后撤回认可;客户端如何获取和强制执行状态,由协议能力及产品策略共同决定。

机制获取和验证什么需要留意的边界
CRL签名列表及其范围、更新时间可分区或使用增量,不一定是整个 CA 的单一巨表
OCSP对指定签发者/序列号的签名状态good 不是完整的证书有效性证明
OCSP Stapling服务端附带缓存的 OCSP 响应要验证签名、证书关联与新鲜度
短有效期缩短未更新证书的存活窗口不会在泄露瞬间撤销现有凭据

OCSP 的 good/revoked/unknown 不能简化成“安全/不安全/网络异常”。good 至少表达未被认定吊销,不保证名称、用途和链都有效。缓存响应也有时间边界:假设 thisUpdate 为 10:00、nextUpdate 为 12:00,11:00 拿到它,并不代表状态在 11:00 重新生成;12:30 不能当作仍然新鲜的肯定证据。RFC 6960

TLS 1.2 的 stapling 常见于 CertificateStatus 消息;TLS 1.3 使用 CertificateEntry 的扩展,不能照搬同一消息位置。Must-Staple 属于 TLS Feature 扩展的使用,不是 CT for code signing;其实际约束效果还取决于客户端支持。RFC 7633、TLS 1.3 证书消息

截至 2026 年 9 月,不能再把 Let's Encrypt 的旧 OCSP 地址当通用部署示例:其 OCSP 服务已于 2025 年 8 月结束,转向 CRL 发布。具体 CA 支持何种状态机制,应读当前证书扩展及其政策。Let's Encrypt 公告

CT:让签发可被审计,不代替域名验证

日志返回 SCT,承诺在规定期限内把提交项纳入日志;拿到 SCT 不等于已经拿到 inclusion proof。Merkle inclusion proof 证明某项属于特定树头,consistency proof 用于证明日志的追加一致性。监控者据此发现异常签发,再走调查、吊销或信任策略处理;日志本身不会替域名持有人授权签发。RFC 9162:CT v2

协议版本与浏览器部署策略要分开查。不能写成“所有证书固定两张 SCT”:例如当前 Chrome 的嵌入式 SCT 路径,生命周期不超过 180 天要求至少两个不同日志,超过 180 天为三个,并有运营者多样性和日志状态条件;TLS 承载路径另有条件。企业私有信任和公开 Web PKI 也不能直接套同一结论。Chrome CT Policy

CT 日志是公开信息来源,申请的内部命名若进入公开证书,会形成可见记录。它提升可发现性,不隐藏域名,也不能单独阻止恶意证书被短暂使用。

续期是一个发布闭环

续期完成的标志:每个服务入口实际送出新证书 签发成功 订单 valid,下载证书 校验待发布材料 名称 / 有效期 / 密钥匹配 分发并重载实例 处理失败、回滚和权限 验证实际握手 逐入口检查序列号与到期时间 CA 返回成功仅完成第一步;负载均衡、边缘节点和长连接会让新旧状态并存。

有效期不是 X.509 永久固定常量,也不能把某个 CA 某个产品的时长写成协议上限。截至 2026 年 9 月,公开 TLS Subscriber Certificate 的基线要求对 2026-03-15 至 2027-03-15 之前签发的证书规定最多 200 天;后续阶段还有收紧计划,具体产品可以更短。自建 PKI 不应未经区分套用该数值。CA/B Forum:6.3.2

设一个教学证书剩余 20 天,自动任务每 24 小时运行一次,正常分发需 2 小时。可用恢复窗口不是“20 次肯定成功的续期”:挑战系统、签发限流、DNS 权限和分发服务可能共享故障。应监测实际服务证书剩余时间、最近成功发布和各入口一致性,同时保留处理失败所需余量。

材料验证通过后原子替换文件、重载使用方,再从各入口确认实际送出的证书。证书文件更新不代表进程已加载,单个节点更新不代表整个集群完成,已有连接也不会自动重新握手。证书续期与私钥轮换应分别记录,避免以为新证书必然意味着新密钥。

继续查阅