你浏览器地址栏里的 图标,背后是一条信任链:DigiCert 签发了 github.com 的证书,你的操作系统信任 DigiCert,所以你信任 github.com。但如果有人伪造了 DigiCert 的证书呢?如果中间人用自己的 CA 签发了 github.com 的证书呢?PKI(Public Key Infrastructure) 就是管理这条信任链的体系——证书怎么签发、怎么验证、怎么撤销、谁信任谁,全在这里面。
PKI(Public Key Infrastructure,公钥基础设施)是一套将公钥与身份绑定的信任体系。它的核心思路很简单:你不需要亲自验证每个网站的身份,而是信任少数几个根 CA,由它们担保下级 CA,层层传递信任。但这个模型远比看上去脆弱——DigiNotar 被入侵后签发了伪造证书,Symantec 违规签发证书最终被浏览器厂商不信任,每次事件都动摇了整个信任链。
本章从 X.509 证书的结构出发,逐步深入 CA 层次、证书生命周期、吊销机制、Let’s Encrypt 自动化、信任模型、PKI 攻击,最后给出实用的证书调试清单。
一、X.509 证书
1.1 证书是什么
X.509 证书本质上是一份”公钥身份证”——它把一个公钥和一组身份信息绑定在一起,然后由一个可信的 CA(证书颁发机构)对这份绑定做数字签名。任何人拿到证书后,都可以用 CA 的公钥验证签名,确认”这个公钥确实属于这个身份”。
一张 X.509 v3 证书由三个关键部分组成:
| 部分 | 说明 | 类比 |
|---|---|---|
| tbsCertificate | 证书正文(To Be Signed)——包含所有字段和扩展 | 身份证上的信息 |
| signatureAlgorithm | CA 使用的签名算法 | 公安局的印章类型 |
| signatureValue | CA 对 tbsCertificate 的签名 | 公安局的印章 |
1.2 证书字段详解
| 字段 | 说明 | 示例 |
|---|---|---|
| Version | 证书版本 | v3(当前主流) |
| Serial Number | CA 分配的唯一序列号 | 0E:8B:… |
| Signature Algorithm | 签名算法 | sha256WithRSAEncryption |
| Issuer | 颁发 CA 的 DN | C=US, O=DigiCert, CN=DigiCert SHA2 |
| Validity | 有效期(Not Before / Not After) | 2024-01-01 ~ 2025-01-01 |
| Subject | 证书持有者的 DN | C=CN, O=MyOrg, CN=example.com |
| Subject Public Key Info | 公钥算法 + 公钥 | RSA-2048 / ECDSA-P256 |
| Extensions | v3 扩展(见下节) | SAN, KeyUsage, BasicConstraints |
1.3 证书扩展
X.509 v3 的扩展机制是它比 v1/v2 强大的关键。以下是几个最重要的扩展:
| 扩展 | 说明 | 关键值 |
|---|---|---|
| Subject Alternative Name (SAN) | 证书适用的域名/IP 列表 | DNS.com, DNS:*.example.com |
| Key Usage | 公钥的允许用途 | digitalSignature, keyEncipherment |
| Extended Key Usage | 更具体的用途 | serverAuth, clientAuth, codeSigning |
| Basic Constraints | 是否为 CA 证书,路径深度 | CA |
| Authority Key Identifier | 颁发者公钥标识 | keyid:01:23:… |
| Subject Key Identifier | 本证书公钥标识 | keyid:45:67:… |
| Authority Information Access | CA 证书和 OCSP 的 URL | OCSP URI, caIssuers URI |
| CRL Distribution Points | CRL 下载地址 | URI: http://crl.digicert.com/… |
现代 TLS 证书的身份验证已经不再依赖 Subject 的 CN(Common Name)字段,而是完全依赖 SAN 扩展。Chrome 从 2017 年起就不再检查 CN,只看 SAN。如果你生成证书时只填了 CN 而没有加 SAN,浏览器会报错。
1.4 真实证书解码
用 openssl 解码一张真实的证书,看看这些字段长什么样:
输出摘要(简化):
Certificate: Data: Version: 3 (0x2) Serial Number: 0e:8b:7d:1a:3b:...:a1 Signature Algorithm: sha256WithRSAEncryption Issuer: C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1 Validity Not Before: Mar 7 00:00:00 2024 GMT Not After : Mar 7 23:59:59 2025 GMT Subject: C = US, ST = California, O = GitHub, Inc., CN = github.com Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (2048 bit) X509v3 extensions: X509v3 Key Usage: critical Digital Signature, Key Encipherment X509v3 Extended Key Usage: TLS Web Server Authentication X509v3 Subject Alternative Name: DNS:github.com, DNS:www.github.com Authority Information Access: OCSP - URI:http://ocsp.digicert.com CA Issuers - URI:http://cacerts.digicert.com/... X509v3 CRL Distribution Points: Full Name: URI:http://crl3.digicert.com/...crl Signature Algorithm: sha256WithRSAEncryption 4a:3b:1c:...:f2注意几个关键点:Key Usage 标记为 critical(必须检查),Extended Key Usage 限定为 serverAuth(只能用于服务端认证),SAN 列出了 github.com 和 www.github.com。
二、CA 层次结构
2.1 为什么需要 CA 层次
如果每个网站都由根 CA 直接签发证书,会有严重问题:
| 问题 | 说明 |
|---|---|
| 根 CA 密钥风险 | 根 CA 密钥一旦泄露,整个信任体系崩塌——所有由它签发的证书都不可信 |
| 运营风险 | 根 CA 需要在线签发证书,在线就意味着可能被攻击 |
| 管理困难 | 全球数亿张证书由一个根 CA 管理,无法按区域/业务分级 |
| 吊销困难 | 根 CA 密钥无法吊销——它自签名,预装在所有设备中 |
解决方案是引入中间 CA(Intermediate CA),形成层次结构:
| 证书类型 | 存储位置 | 签名者 | 密钥状态 | 用途 |
|---|---|---|---|---|
| 根证书 | 浏览器/OS 信任库 | 自签名 | 离线(HSM) | 信任锚 |
| 中间证书 | 服务器发送 | 根 CA | 在线但受限 | 减少根 CA 风险 |
| 终端证书 | 服务器发送 | 中间 CA | 在线 | 标识具体域名 |
2.2 交叉认证
有时候两个不同的根 CA 需要互相信任——比如企业内部 CA 和公共 CA 之间。交叉认证(Cross-Certification)就是让 CA A 签发一张证书给 CA B 的公钥,反之亦然:
交叉认证的实际用途:企业内网的设备证书由内部 CA 签发,但外部合作伙伴不信任内部 CA。通过让公共 CA 交叉认证内部 CA,外部合作伙伴就能验证内部证书。
2.3 证书链验证
当浏览器收到服务器证书时,它需要逐级验证签名,直到到达一个受信任的根 CA:
用代码实现证书链验证:
from cryptography import x509from cryptography.hazmat.primitives import hashesfrom cryptography.hazmat.primitives.asymmetric import rsa, paddingfrom cryptography.x509.oid import NameOIDimport datetimedef verify_cert_chain(leaf_cert, intermediate_cert, root_cert): """验证证书链:leaf → intermediate → root""" now = datetime.datetime.now(datetime.timezone.utc) for cert in [leaf_cert, intermediate_cert, root_cert]: if now < cert.not_valid_before_utc or now > cert.not_valid_after_utc: raise ValueError(f"证书过期: {cert.subject}") intermediate_cert.public_key().verify( leaf_cert.signature, leaf_cert.tbs_certificate_bytes, padding.PKCS1v15(), leaf_cert.signature_hash_algorithm, ) root_cert.public_key().verify( intermediate_cert.signature, intermediate_cert.tbs_certificate_bytes, padding.PKCS1v15(), intermediate_cert.signature_hash_algorithm, ) if root_cert.issuer != root_cert.subject: raise ValueError("根证书不是自签名") print("证书链验证通过")三、证书生命周期
3.1 证书申请流程
证书不是凭空出现的,它经历了一个完整的生命周期:
CSR(Certificate Signing Request)是证书申请的起点,它包含申请者的公钥和身份信息:
openssl req -newkey rsa:2048 -keyout private.key -out request.csr \ -subj "/C=CN/ST=Beijing/O=MyOrg/CN=example.com"openssl ecparam -name prime256v1 -genkey -noout -out private.keyopenssl req -new -key private.key -out request.csr \ -subj "/C=CN/ST=Beijing/O=MyOrg/CN=example.com"cat > san.cnf << 'EOF'[req]distinguished_name = req_distinguished_namereq_extensions = v3_req[req_distinguished_name]CN = example.com[v3_req]subjectAltName = @alt_names[alt_names]DNS.1 = example.comDNS.2 = www.example.comDNS.3 = *.example.comEOFopenssl req -new -key private.key -out request.csr -config san.cnfopenssl req -in request.csr -text -noout3.2 CA 验证级别
CA 在签发证书前需要验证申请者的身份,验证级别决定了证书的可信度:
| 级别 | 全称 | 验证内容 | 证书标识 | 典型价格 |
|---|---|---|---|---|
| DV | Domain Validation | 证明你控制该域名 | 无特殊标识 | 免费 ~ $10/年 |
| OV | Organization Validation | 验证组织身份+域名控制 | 显示组织名 | 200/年 |
| EV | Extended Validation | 严格身份审查+域名控制 | 历史上显示绿色地址栏 | 500/年 |
EV 证书曾经通过绿色地址栏提供视觉区分,但 Chrome 77(2019)和 Firefox 72(2020)已移除这一特性。现在 EV 证书和 DV 证书在浏览器界面看起来一样,区别只在证书的 Subject 字段中。这意味着普通用户无法通过视觉区分 DV 和 EV 证书。
3.3 证书续期
证书到期后需要续期。续期有两种方式:
| 方式 | 说明 | 适用场景 |
|---|---|---|
| 重新签发 | 生成新密钥对,提交新 CSR | 密钥可能泄露时 |
| 续期签发 | 复用原密钥对,只更新有效期 | 密钥安全,只是到期 |
四、证书吊销
证书在到期前可能需要吊销——比如私钥泄露、域名所有权变更、CA 违规签发。吊销机制是 PKI 安全的关键防线。
4.1 CRL(Certificate Revocation List)
CRL 是 CA 定期发布的吊销证书列表,客户端下载后缓存:
Certificate Revocation List (CRL): Version: 2 Issuer: C = US, O = DigiCert Inc, CN = DigiCert SHA2 Last Update: Mar 15 00:00:00 2024 GMT Next Update: Mar 22 00:00:00 2024 GMT Revoked Certificates: Serial Number: 0E8B7D1A3B...A1 Revocation Date: Mar 10 00:00:00 2024 GMT Serial Number: A1B2C3D4E5...F6 Revocation Date: Mar 12 00:00:00 2024 GMTCRL 的问题很明显:
| 问题 | 说明 |
|---|---|
| 延迟 | CRL 更新周期通常 7-14 天,刚吊销的证书可能不在列表中 |
| 体积 | 大型 CA 的 CRL 可达数 MB,每次下载浪费带宽 |
| 隐私 | 客户端需要下载完整列表,无法只查询单个证书 |
4.2 OCSP(Online Certificate Status Protocol)
OCSP 解决了 CRL 的延迟和体积问题——客户端实时查询某个证书的吊销状态:
echo | openssl s_client -connect github.com:443 -showcerts 2>/dev/null \ | awk '/BEGIN CERT/,/END CERT/' > certs.pemcsplit -s -z certs.pem '/-----BEGIN CERTIFICATE-----/' '{1}'openssl ocsp -issuer xx01 -cert xx00 \ -url http://ocsp.digicert.com \ -resp_text但 OCSP 也有问题:隐私泄露——CA 的 OCSP 服务器知道你在访问哪个网站。而且如果 OCSP 服务器不可用,浏览器通常选择”软失败”(soft-fail),即认为证书有效,这削弱了安全性。
4.3 OCSP Stapling
OCSP Stapling 让服务器代替客户端查询 OCSP,并在 TLS 握手时附带 OCSP 响应:
4.4 CRL vs OCSP vs OCSP Stapling
| 维度 | CRL | OCSP | OCSP Stapling |
|---|---|---|---|
| 实时性 | 低(7-14 天延迟) | 高(实时查询) | 中(服务器定期查询) |
| 隐私 | 好(下载完整列表) | 差(CA 知道你访问谁) | 好(服务器代理查询) |
| 性能 | 差(下载大列表) | 中(每次握手多一次请求) | 好(握手时附带) |
| 可用性 | 好(可缓存) | 差(CA 不可用则软失败) | 好(服务器缓存响应) |
| 复杂度 | 低 | 中 | 高(需服务器支持) |
| 浏览器支持 | 部分支持 | 广泛支持 | 广泛支持 |
Chrome 从 2017 年起不再在线检查 CRL/OCSP(CRLSet 除外),理由是 OCSP 软失败提供了虚假的安全感。Chrome 使用自己的 CRLSet 机制——一个压缩的、浏览器推送的吊销列表。Firefox 仍然默认启用 OCSP 检查。这意味着不同浏览器对吊销证书的处理方式不同。
五、Let’s Encrypt
5.1 ACME 协议
Let’s Encrypt 通过 ACME(Automatic Certificate Management Environment)协议实现了证书签发的完全自动化。ACME 的核心思路是:你要证明你控制某个域名,CA 给你一个挑战,你完成挑战,CA 签发证书。
5.2 HTTP-01 vs DNS-01 验证
| 维度 | HTTP-01 | DNS-01 |
|---|---|---|
| 原理 | 在网站根目录放置验证文件 | 在 DNS 添加 TXT 记录 |
| 要求 | 服务器 80 端口可达 | DNS API 可操作 |
| 通配符 | 不支持 | 支持 |
| 防火墙友好 | 差(需要开放 80 端口) | 好(不需要入站连接) |
| 适用场景 | 普通 Web 服务器 | 内网服务、通配符证书 |
| 自动化难度 | 低 | 中(需要 DNS API) |
sudo certbot certonly --nginx -d example.com -d www.example.comsudo certbot certonly --standalone -d example.comsudo certbot certonly --dns-cloudflare \ -d example.com -d '*.example.com' \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.inisudo certbot certonly --manual --preferred-challenges dns \ -d example.com -d '*.example.com'5.3 自动化证书管理
Let’s Encrypt 证书有效期只有 90 天,必须自动化续期:
sudo certbot renew --quietimport josepy as josefrom cryptography.hazmat.primitives.asymmetric import ecfrom cryptography.hazmat.primitives import serializationfrom acme import client, messagesprivate_key = ec.generate_private_key(ec.SECP256R1())key = jose.JWKRSA(key=private_key) # 简化示例net = client.ClientNetwork(key, account=None)directory = messages.Directory.from_json(net.get( "https://acme-v02.api.letsencrypt.org/directory").json())client_acme = client.ClientV2(directory, net=net)registration = client_acme.new_account( messages.NewRegistration.from_data( email="admin@example.com", terms_of_service_agreed=True, ))print(f"账户注册成功: {registration.uri}")// Go: 使用 lego 库自动化证书管理// go get github.com/go-acme/lego/v4package mainimport ( "fmt" "log" "os" "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/registration")type MyUser struct { Email string Registration *registration.Resource key crypto.PrivateKey}func (u *MyUser) GetEmail() string { return u.Email }func (u *MyUser) GetRegistration() *registration.Resource { return u.Registration }func (u *MyUser) GetPrivateKey() crypto.PrivateKey { return u.key }func main() { // 创建用户和配置 privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) myUser := MyUser{ Email: "admin@example.com", key: privateKey, } config := lego.NewConfig(&myUser) config.CADirURL = lego.LEDirectoryProduction client, err := lego.NewClient(config) if err != nil { log.Fatal(err) } // 注册账户 reg, err := client.Registration.Register( registration.RegisterOptions{TermsOfServiceAgreed: true}, ) myUser.Registration = reg // 获取证书 request := certificate.ObtainRequest{ Domains: []string{"example.com", "*.example.com"}, Bundle: true, } certificates, err := client.Certificate.Obtain(request) if err != nil { log.Fatal(err) } fmt.Printf("证书获取成功: %s\n", certificates.Domain)}5.4 Let’s Encrypt vs 商业 CA
| 维度 | Let’s Encrypt | 商业 CA |
|---|---|---|
| 价格 | 免费 | 500/年 |
| 证书类型 | DV(域名验证) | DV/OV/EV |
| 有效期 | 90 天 | 1-2 年 |
| 通配符 | 支持(DNS-01) | 支持 |
| 自动化 | ACME 原生 | 部分支持 |
| 保险 | 无 | 有(1.5M) |
| 适合 | 个人/小站/自动化 | 企业/金融/合规 |
六、证书信任模型
6.1 浏览器如何决定信任一个证书
当你访问一个 HTTPS 网站时,浏览器执行以下信任决策流程:
6.2 根证书存储
根证书存储在不同操作系统的不同位置:
| 系统 | 存储位置 | 管理方式 |
|---|---|---|
| Windows | Windows Certificate Store | 组策略 / certutil |
| macOS | Keychain Access | 系统偏好设置 |
| Linux | /etc/ssl/certs/ | update-ca-certificates |
| Android | System Trust Store | 系统更新 |
| iOS | System Trust Store | 系统更新 |
| Firefox | 内置 cert9.db | 浏览器更新(独立于 OS) |
ls /etc/ssl/certs/ | wc -lopenssl x509 -in /etc/ssl/certs/DigiCert_Global_Root_CA.pem -text -noout | head -20sudo cp my-company-ca.pem /usr/local/share/ca-certificates/sudo update-ca-certificatessudo rm /usr/local/share/ca-certificates/my-company-ca.crtsudo update-ca-certificates --fresh添加根证书到系统信任库是极其危险的操作——你等于信任该 CA 签发的所有证书。如果该 CA 被入侵,攻击者可以为任何域名签发可信证书。只在必要时添加企业内部 CA,并确保该 CA 的私钥安全。
6.3 证书透明度(CT)
CT(Certificate Transparency)要求所有公开信任的证书必须记录在公开的、不可篡改的日志中:
| 组件 | 说明 |
|---|---|
| CT Log | 基于 Merkle Tree 的公开证书日志,只能追加,不可删除 |
| SCT (Signed Certificate Timestamp) | CT Log 签发的时间戳,证明证书已记录 |
| Monitor | 监控 CT Log 中的可疑证书(域名所有者使用) |
| Auditor | 审计 CT Log 的一致性(验证日志未被篡改) |
Chrome 要求所有新签发的证书必须包含至少 2 个不同 CT Log 的 SCT,否则拒绝信任。这意味着 CA 不能秘密签发证书——所有证书都会出现在公开日志中。
七、PKI 攻击
7.1 DigiNotar 事件(2011)
DigiNotar 是荷兰的一家 CA,2011 年被黑客入侵后,攻击者签发了超过 500 张伪造证书,包括 google.com、mozilla.com 等高价值域名。这些伪造证书被用于伊朗的中间人攻击,监控公民的 Gmail 通信。
事件影响:
| 维度 | 影响 |
|---|---|
| DigiNotar | 破产,根证书被所有浏览器/OS 移除 |
| 行业 | 推动了 CT(证书透明度)的制定和部署 |
| 用户 | 伊朗用户的 Gmail 被中间人攻击 |
| CA 安全 | CA 安全审计标准大幅提高 |
7.2 Symantec 误发事件(2015-2017)
Symantec(后被 DigiCert 收购)多次违规签发证书:
- 2015:签发了 127 张未经验证的证书(测试证书流入生产)
- 2016:签发了 google.com 和 www.google.com 的证书给非 Google 实体
- 2017:发现更多违规签发
最终结果:Chrome 66(2018)起不信任 Symantec 旧证书,Firefox 跟进。Symantec 的 PKI 业务被 DigiCert 收购。
7.3 中间人攻击与证书
中间人攻击(MITM)在 HTTPS 场景下需要伪造证书。攻击者有几种方式:
| 攻击方式 | 原理 | 防御 |
|---|---|---|
| 自签名证书 | 攻击者自己签发证书 | 浏览器警告,用户需手动信任 |
| 入侵 CA | 获取 CA 私钥签发伪造证书 | CT 监控、CA 安全审计 |
| 恶意根证书 | 诱导用户安装攻击者的根证书 | 不要随意安装根证书 |
| 降级攻击 | 强制使用弱算法或旧协议 | HSTS、TLS 1.3 |
7.4 证书固定(Certificate Pinning)
证书固定是一种防御手段——应用只信任特定的证书或公钥,而不是信任所有 CA:
| 固定方式 | 说明 | 优缺点 |
|---|---|---|
| SPKI Pin | 固定公钥的哈希 | 灵活(证书续期时公钥可不变) |
| Certificate Pin | 固定整张证书 | 严格(续期时必须更新) |
| HPKP (HTTP) | HTTP 头声明固定 | 已废弃(误配置导致网站不可访问) |
| Android Network Security Config | Android 原生支持 | 移动端推荐 |
| iOS App Transport Security | iOS 原生支持 | 移动端推荐 |
<!-- Android: Network Security Config (推荐) --><?xml version="1.0" encoding="utf-8"?><network-security-config> <domain-config> <domain includeSubdomains="true">example.com</domain> <pin-set> <!-- 主公钥固定 --> <pin digest="SHA-256">base64EncodedHash==</pin> <!-- 备份公钥固定(防止主密钥丢失导致应用无法连接) --> <pin digest="SHA-256">backupBase64EncodedHash==</pin> </pin-set> </domain-config></network-security-config>HPKP(HTTP Public Key Pinning)已被 Chrome 在 2018 年废弃,原因是误配置会导致网站长时间不可访问(max-age 设置过大时),且没有恢复机制。现代替代方案是使用 Certificate Transparency + Expect-CT 头,或在移动端使用平台原生的证书固定 API。
八、实践
8.1 证书检查命令
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \ | openssl x509 -text -nooutecho | openssl s_client -connect example.com:443 2>/dev/null \ | openssl x509 -noout -datesecho | openssl s_client -connect example.com:443 2>/dev/null \ | openssl x509 -noout -ext subjectAltNameecho | openssl s_client -connect example.com:443 -showcerts 2>/dev/nullecho | openssl s_client -connect example.com:443 2>/dev/null \ | openssl x509 -out remote-cert.pemopenssl ocsp -issuer intermediate.pem -cert leaf.pem \ -url http://ocsp.digicert.com -resp_text8.2 CSR 生成
openssl req -newkey rsa:2048 -nodes \ -keyout example.key -out example.csr \ -subj "/C=CN/ST=Beijing/L=Beijing/O=MyOrg/CN=example.com"openssl ecparam -name prime256v1 -genkey -noout -out example.keyopenssl req -new -key example.key -out example.csr \ -subj "/C=CN/ST=Beijing/L=Beijing/O=MyOrg/CN=example.com"cat > san.cnf << 'EOF'[req]distinguished_name = dnreq_extensions = v3_reqprompt = no[dn]C = CNST = BeijingO = MyOrgCN = example.com[v3_req]subjectAltName = @alt_names[alt_names]DNS.1 = example.comDNS.2 = www.example.comDNS.3 = api.example.comIP.1 = 192.168.1.1EOFopenssl req -new -key example.key -out example.csr -config san.cnfopenssl req -in example.csr -verify -nooutopenssl req -in example.csr -text -noout8.3 自签名证书
openssl req -x509 -new -nodes \ -newkey rsa:2048 -keyout ca.key \ -sha256 -days 3650 \ -out ca.crt \ -subj "/C=CN/ST=Beijing/O=DevCA/CN=Development Root CA"openssl ecparam -name prime256v1 -genkey -noout -out server.keyopenssl req -new -key server.key -out server.csr \ -subj "/C=CN/ST=Beijing/O=DevOrg/CN=localhost"cat > server-ext.cnf << 'EOF'basicConstraints = CA:FALSEkeyUsage = digitalSignature, keyEnciphermentextendedKeyUsage = serverAuthsubjectAltName = @alt_names[alt_names]DNS.1 = localhostDNS.2 = *.localhostIP.1 = 127.0.0.1EOFopenssl x509 -req -in server.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -out server.crt -days 365 -sha256 \ -extfile server-ext.cnfopenssl verify -CAfile ca.crt server.crt8.4 证书调试清单
当证书出问题时,按以下清单逐项排查:
| 步骤 | 检查项 | 命令/方法 |
|---|---|---|
| 1 | 证书是否过期? | openssl x509 -noout -dates |
| 2 | SAN 是否包含目标域名? | openssl x509 -noout -ext subjectAltName |
| 3 | 证书链是否完整? | openssl s_client -connect host:443 -showcerts |
| 4 | 中间证书是否发送? | 检查 showcerts 输出是否包含中间 CA |
| 5 | 根 CA 是否在信任库? | openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt cert.pem |
| 6 | 证书是否被吊销? | openssl ocsp -issuer ca.pem -cert cert.pem -url OCSP_URL |
| 7 | Key Usage 是否正确? | openssl x509 -noout -text | grep "Key Usage" |
| 8 | 签名算法是否安全? | openssl x509 -noout -text | grep "Signature Algorithm" |
| 9 | SCT 是否存在? | openssl x509 -noout -text | grep "CT Precertificate" |
| 10 | OCSP Stapling 是否启用? | openssl s_client -connect host:443 -status |
check_cert() { local host="$1" echo "=== 证书检查: $host ===" cert=$(echo | openssl s_client -connect "$host:443" -servername "$host" 2>/dev/null) echo "$cert" | openssl x509 -noout -dates 2>/dev/null || echo "无法获取证书" echo "SAN:" echo "$cert" | openssl x509 -noout -ext subjectAltName 2>/dev/null || echo "无 SAN" echo "签名算法:" echo "$cert" | openssl x509 -noout -text 2>/dev/null | grep "Signature Algorithm" | head -1 echo "OCSP Stapling:" echo "$cert" | openssl s_client -connect "$host:443" -status 2>/dev/null | grep "OCSP response" echo "---"}check_cert github.comcheck_cert google.com八·附、实践:从零构建 PKI 体系
X.509 证书标准诞生于 1988 年的 CCITT(现 ITU-T)X.500 目录服务规范,最初设计用于电信网络的身份认证。1995 年 Verisign 成为第一家商业 CA,开启了 Web PKI 时代——浏览器内置一组受信任的根证书,所有 HTTPS 网站的证书必须由这些根证书(或其下属中间 CA)签发。2011 年 DigiNotar 事件彻底改变了 PKI 的信任模型:荷兰 CA DigiNotar 被入侵后签发了大量伪造的 Google 证书,导致其根证书被所有浏览器移除、公司破产。这一事件催生了证书透明度(Certificate Transparency, CT)机制——所有公开信任的证书必须记录在公开的、可审计的日志中。2015 年 Let’s Encrypt 的推出则让 PKI 从”付费购买信任”走向”自动化免费获取”,90 天有效期 + ACME 协议使证书管理从手动操作变成代码。
从 Verisign 到 Let’s Encrypt,PKI 的核心矛盾始终是:信任必须集中管理,但集中管理本身就是单点故障。理解 PKI 的最佳方式是从零构建一套完整的证书体系——从 Root CA 到终端证书,从签发到吊销。
附.1 前置知识
- OpenSSL 3.0+ 命令行工具
- 理解本章前八节的理论内容(X.509 结构、CA 层次、CRL/OCSP)
附.2 创建 Root CA
Root CA 是信任链的起点,其自签名证书会被手动导入到客户端信任库中:
# 1. 生成 Root CA 私钥(RSA-2048)openssl genrsa -out root-ca.key 2048
# 2. 创建自签名根证书(有效期 10 年)openssl req -x509 -new -nodes \ -key root-ca.key -sha256 -days 3650 \ -out root-ca.crt \ -subj "/C=CN/ST=Beijing/O=PKI Lab/CN=PKI Lab Root CA"
# 3. 查看根证书信息openssl x509 -in root-ca.crt -noout -subject -issuer -datessubject=C = CN, ST = Beijing, O = PKI Lab, CN = PKI Lab Root CAissuer=C = CN, ST = Beijing, O = PKI Lab, CN = PKI Lab Root CAnotBefore=May 7 14:31:29 2026 GMTnotAfter=May 4 14:31:29 2036 GMT注:Root CA 的 subject 和 issuer 相同,因为它是自签名的。有效期通常设为 10-20 年,因为更换根证书需要更新所有客户端的信任库,代价极高。
附.3 创建中间 CA
中间 CA 是 Root CA 与终端证书之间的桥梁。Root CA 的私钥通常离线保存,日常签发由中间 CA 完成——即使中间 CA 被入侵,也可以用 Root CA 吊销它,而不影响整个信任体系:
# 1. 生成中间 CA 私钥openssl genrsa -out int-ca.key 2048
# 2. 创建中间 CA 的 CSR(证书签名请求)openssl req -new -key int-ca.key -out int-ca.csr \ -subj "/C=CN/ST=Beijing/O=PKI Lab/CN=PKI Lab Intermediate CA"
# 3. 用 Root CA 签发中间 CA 证书(需要扩展配置)cat > int-ca.ext << 'EOF'basicConstraints = critical, CA:TRUE, pathlen:0keyUsage = critical, digitalSignature, keyCertSign, cRLSignsubjectKeyIdentifier = hashauthorityKeyIdentifier = keyid:always, issuerEOF
openssl x509 -req -in int-ca.csr \ -CA root-ca.crt -CAkey root-ca.key -CAcreateserial \ -out int-ca.crt -days 1825 -sha256 \ -extfile int-ca.ext
# 4. 查看中间 CA 证书openssl x509 -in int-ca.crt -noout -subject -issuerCertificate request self-signature oksubject=C = CN, ST = Beijing, O = PKI Lab, CN =PKI Lab Intermediate CAsubject=C = CN, ST = Beijing, O = PKI Lab, CN = PKI Lab Intermediate CAissuer=C = CN, ST = Beijing, O = PKI Lab, CN = PKI Lab Root CA注意:
basicConstraints = critical, CA:TRUE, pathlen:0表示这是一个 CA 证书,且不能再签发下级 CA(pathlen=0)。这是防止 CA 权限过度委托的关键约束。
附.4 签发终端证书
终端证书是实际部署在服务器上的证书,使用 ECC 密钥(更短、更快):
# 1. 生成 ECC 私钥(P-256 曲线)openssl ecparam -name prime256v1 -genkey -noout -out server.key
# 2. 创建终端证书 CSRopenssl req -new -key server.key -out server.csr \ -subj "/C=CN/ST=Beijing/O=PKI Lab/CN=server.pkilab.local"
# 3. 用中间 CA 签发终端证书(含 SAN 扩展)cat > server.ext << 'EOF'basicConstraints = CA:FALSEkeyUsage = critical, digitalSignature, keyEnciphermentextendedKeyUsage = serverAuthsubjectKeyIdentifier = hashauthorityKeyIdentifier = keyid:always, issuersubjectAltName = DNS:server.pkilab.local, DNS:*.pkilab.local, IP:127.0.0.1EOF
openssl x509 -req -in server.csr \ -CA int-ca.crt -CAkey int-ca.key -CAcreateserial \ -out server.crt -days 365 -sha256 \ -extfile server.ext
# 4. 查看终端证书openssl x509 -in server.crt -noout -subject -issuer -datesCertificate request self-signature oksubject=C = CN, ST = Beijing, O = PKI Lab, CN = server.pkilab.localissuer=C = CN, ST = Beijing, O = PKI Lab, CN = PKI Lab Intermediate CAnotBefore=May 7 14:31:29 2026 GMTnotAfter=May 7 14:31:29 2027 GMT注:
subjectAltName(SAN)是现代证书的必填项。Chrome 从 2017 年起不再使用 Common Name(CN)匹配域名,只看 SAN。忘记配置 SAN 是最常见的证书部署错误。
附.5 证书链验证
证书链验证是 PKI 的核心操作——从终端证书逐级验证签名,直到到达受信任的根证书:
# 验证终端证书(指定 Root CA 和中间 CA)openssl verify -CAfile root-ca.crt -untrusted int-ca.crt server.crtserver.crt: OK验证过程:
附.6 证书吊销与 CRL
当私钥泄露或证书信息错误时,CA 需要吊销证书。CRL(Certificate Revocation List)是 CA 签发的”吊销名单”:
# 1. 初始化 CA 数据库touch index.txtecho "01" > serial.txtecho "01" > crlnumber.txt
# 2. 生成空的 CRLcat > ca.cnf << 'EOF'[ca]default_ca = CA_default[CA_default]database = index.txtserial = serial.txtcrlnumber = crlnumber.txtdefault_crl_days = 365default_md = sha256EOF
openssl ca -gencrl -keyfile root-ca.key -cert root-ca.crt \ -out root-ca.crl -config ca.cnf
# 3. 查看 CRL 内容openssl crl -in root-ca.crl -text -noout | head -10Certificate Revocation List (CRL): Version 2 (0x1) Signature Algorithm: sha256WithRSAEncryption Issuer: C = CN, ST = Beijing, O = PKI Lab, CN = PKI Lab Root CA Last Update: May 7 14:31:29 2026 GMT Next Update: May 7 14:31:29 2027 GMT CRL extensions: X509v3 CRL Number: 1No Revoked Certificates.注:CRL 的主要缺陷是延迟——客户端必须下载完整的吊销列表才能知道证书是否被吊销。对于高流量网站,CRL 可能是几天前的。OCSP 和 OCSP Stapling 是更现代的解决方案(详见本章 5.3 节)。
附.7 实践小结
| 步骤 | OpenSSL 命令 | 关键参数 | 安全要点 |
|---|---|---|---|
| Root CA | openssl req -x509 | -days 3650, 自签名 | 私钥离线保存 |
| 中间 CA | openssl x509 -req | CA:TRUE, pathlen:0 | 限制路径深度 |
| 终端证书 | openssl x509 -req | CA:FALSE, SAN | 必须配置 SAN |
| 链验证 | openssl verify | -CAfile, -untrusted | 逐级验证签名 |
| CRL | openssl ca -gencrl | default_crl_days | 延迟是主要缺陷 |
关键教训:
- Root CA 私钥必须离线保存:日常签发用中间 CA,Root CA 只在创建新中间 CA 时使用
- SAN 是必填项:现代浏览器只看 SAN,不看 CN
- 证书链必须完整:部署时需要同时提供终端证书和中间 CA 证书
- CRL 有延迟:生产环境应使用 OCSP Stapling 替代
九、总结
上一章理解了TLS 1.3 握手协议。
| 维度 | 关键要点 |
|---|---|
| X.509 | 公钥+身份+CA签名,v3 扩展是关键(SAN、KeyUsage、EKU) |
| 证书链 | 根CA→中间CA→终端证书,逐级验证签名 |
| 生命周期 | CSR → CA 验证 → 签发 → 部署 → 续期/吊销 |
| 吊销 | CRL 延迟大、OCSP 泄隐私、Stapling 最优 |
| CT | 证书透明度,所有证书必须记录在公开日志 |
| Let’s Encrypt | 免费 DV 证书,ACME 自动化,90 天有效期 |
| 信任模型 | 根证书存储在 OS/浏览器,CT 监控误发 |
| 攻击 | DigiNotar/Symantec 事件,CT 是核心防御 |
| 实践 | openssl 检查、SAN 必须配置、OCSP Stapling 必须启用 |
PKI 信任模型对比:
| 模型 | 信任基础 | 优点 | 缺点 | 典型应用 |
|---|---|---|---|---|
| 层次模型 | 单一根 CA | 简单、高效 | 根 CA 是单点故障 | Web PKI |
| 网状模型 | 多 CA 交叉认证 | 灵活、冗余 | 复杂、路径发现困难 | 企业 PKI |
| 信任列表 | 多个根 CA 平等 | 去中心化 | 管理分散 | 浏览器信任库 |
| CT 增强 | 层次模型+公开日志 | 可审计、可监控 | 增加延迟 | 现代 Web PKI |
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






