mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
5171 字
15 分钟
证书与 PKI
2026-03-30

你浏览器地址栏里的 图标,背后是一条信任链: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)——包含所有字段和扩展身份证上的信息
signatureAlgorithmCA 使用的签名算法公安局的印章类型
signatureValueCA 对 tbsCertificate 的签名公安局的印章
graph TB subgraph Certificate["X.509 v3 证书结构"] TBS["tbsCertificate<br/>(待签名证书正文)"] SIGALG["signatureAlgorithm<br/>(签名算法)"] SIGVAL["signatureValue<br/>(签名值)"] end subgraph TBSContent["tbsCertificate 包含"] VER["版本 (Version v3)"] SER["序列号 (Serial Number)"] ISS["颁发者 (Issuer)"] VAL["有效期 (Validity)"] SUB["主体 (Subject)"] PKI["公钥信息 (SubjectPublicKeyInfo)"] EXT["扩展 (Extensions)"] end TBS --> VER & SER & ISS & VAL & SUB & PKI & EXT TBS -.->|"CA 对此签名"| SIGVAL style TBS fill:#e8f5e9 style SIGALG fill:#e3f2fd style SIGVAL fill:#fff3e0 style TBSContent fill:#fafafa

1.2 证书字段详解#

字段说明示例
Version证书版本v3(当前主流)
Serial NumberCA 分配的唯一序列号0E:8B:…
Signature Algorithm签名算法sha256WithRSAEncryption
Issuer颁发 CA 的 DNC=US, O=DigiCert, CN=DigiCert SHA2
Validity有效期(Not Before / Not After)2024-01-01 ~ 2025-01-01
Subject证书持有者的 DNC=CN, O=MyOrg, CN=example.com
Subject Public Key Info公钥算法 + 公钥RSA-2048 / ECDSA-P256
Extensionsv3 扩展(见下节)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, pathlen:1
Authority Key Identifier颁发者公钥标识keyid:01:23:…
Subject Key Identifier本证书公钥标识keyid:45:67:…
Authority Information AccessCA 证书和 OCSP 的 URLOCSP URI, caIssuers URI
CRL Distribution PointsCRL 下载地址URI: http://crl.digicert.com/
Note

现代 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),形成层次结构:

graph TB ROOT["根 CA<br/> 离线存储,自签名<br/>预装在浏览器/OS 信任库"] --> INT1["中间 CA 1<br/> 在线,根 CA 签发<br/>用于 Web 服务器证书"] ROOT --> INT2["中间 CA 2<br/> 在线,根 CA 签发<br/>用于代码签名证书"] INT1 --> LEAF1["终端证书<br/>github.com"] INT1 --> LEAF2["终端证书<br/>google.com"] INT2 --> LEAF3["代码签名证书<br/>Microsoft"] style ROOT fill:#e8f5e9,stroke:#2e7d32 style INT1 fill:#e3f2fd,stroke:#1565c0 style INT2 fill:#e3f2fd,stroke:#1565c0 style LEAF1 fill:#fff3e0,stroke:#e65100 style LEAF2 fill:#fff3e0,stroke:#e65100 style LEAF3 fill:#fff3e0,stroke:#e65100
证书类型存储位置签名者密钥状态用途
根证书浏览器/OS 信任库自签名离线(HSM)信任锚
中间证书服务器发送根 CA在线但受限减少根 CA 风险
终端证书服务器发送中间 CA在线标识具体域名

2.2 交叉认证#

有时候两个不同的根 CA 需要互相信任——比如企业内部 CA 和公共 CA 之间。交叉认证(Cross-Certification)就是让 CA A 签发一张证书给 CA B 的公钥,反之亦然:

graph LR ROOTA["根 CA A<br/>(企业内部)"] --> INTA["中间 CA A"] ROOTB["根 CA B<br/>(公共 CA)"] --> INTB["中间 CA B"] ROOTA -.->|"交叉认证<br/>签发 CA B 的公钥"| INTB ROOTB -.->|"交叉认证<br/>签发 CA A 的公钥"| INTA INTA --> LEAF1["终端证书 1"] INTB --> LEAF2["终端证书 2"] style ROOTA fill:#e8f5e9 style ROOTB fill:#e3f2fd

交叉认证的实际用途:企业内网的设备证书由内部 CA 签发,但外部合作伙伴不信任内部 CA。通过让公共 CA 交叉认证内部 CA,外部合作伙伴就能验证内部证书。

2.3 证书链验证#

当浏览器收到服务器证书时,它需要逐级验证签名,直到到达一个受信任的根 CA:

sequenceDiagram participant C as Client participant S as Server S->>C: 终端证书 + 中间证书 C->>C: 1. 检查终端证书有效期 C->>C: 2. 用中间 CA 公钥验证终端证书签名 C->>C: 3. 检查中间证书有效期 C->>C: 4. 用根 CA 公钥验证中间证书签名 C->>C: 5. 根 CA 在本地信任库中? C->>C: 6. 检查 CRL/OCSP 是否吊销 C->>C: 7. 检查 SAN 是否匹配域名 C->>C: 8. 检查 Key Usage / EKU Note over C: 全部通过 → 信任建立

用代码实现证书链验证:

from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.x509.oid import NameOID
import datetime
def 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 证书申请流程#

证书不是凭空出现的,它经历了一个完整的生命周期:

flowchart TB START[" 生成密钥对"] --> CSR[" 创建 CSR<br/>(证书签名请求)"] CSR --> SUBMIT[" 提交 CSR 给 CA"] SUBMIT --> VERIFY[" CA 验证身份<br/>(DV/OV/EV)"] VERIFY --> |"验证通过"| ISSUE[" CA 签发证书"] VERIFY --> |"验证失败"| REJECT["拒绝签发"] ISSUE --> DEPLOY[" 部署证书到服务器"] DEPLOY --> ACTIVE["证书生效"] ACTIVE --> |"到期前 30 天"| RENEW[" 续期<br/>(重新签发)"] ACTIVE --> |"私钥泄露"| REVOKE[" 吊销证书"] RENEW --> DEPLOY REVOKE --> START style START fill:#e8f5e9 style ISSUE fill:#e3f2fd style ACTIVE fill:#c8e6c9 style REVOKE fill:#ffcdd2 style RENEW fill:#fff3e0

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.key
openssl 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_name
req_extensions = v3_req
[req_distinguished_name]
CN = example.com
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = example.com
DNS.2 = www.example.com
DNS.3 = *.example.com
EOF
openssl req -new -key private.key -out request.csr -config san.cnf
openssl req -in request.csr -text -noout

3.2 CA 验证级别#

CA 在签发证书前需要验证申请者的身份,验证级别决定了证书的可信度:

级别全称验证内容证书标识典型价格
DVDomain Validation证明你控制该域名无特殊标识免费 ~ $10/年
OVOrganization Validation验证组织身份+域名控制显示组织名50 50 ~ 200/年
EVExtended Validation严格身份审查+域名控制历史上显示绿色地址栏150 150 ~ 500/年
Warning

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 GMT

CRL 的问题很明显:

问题说明
延迟CRL 更新周期通常 7-14 天,刚吊销的证书可能不在列表中
体积大型 CA 的 CRL 可达数 MB,每次下载浪费带宽
隐私客户端需要下载完整列表,无法只查询单个证书

4.2 OCSP(Online Certificate Status Protocol)#

OCSP 解决了 CRL 的延迟和体积问题——客户端实时查询某个证书的吊销状态:

sequenceDiagram participant C as Client (浏览器) participant S as Server (网站) participant O as OCSP Responder (CA) C->>S: TLS 握手,获取证书 S->>C: 返回终端证书 C->>O: 查询证书序列号的吊销状态 O->>C: 返回 Good / Revoked / Unknown C->>C: 根据状态决定是否信任
echo | openssl s_client -connect github.com:443 -showcerts 2>/dev/null \
| awk '/BEGIN CERT/,/END CERT/' > certs.pem
csplit -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 响应:

sequenceDiagram participant S as Server participant O as OCSP Responder participant C as Client Note over S,O: 服务器定期查询(后台) S->>O: 查询证书吊销状态 O->>S: 返回签名后的 OCSP 响应 Note over S,C: TLS 握手时 S->>C: 证书 + OCSP 响应(Stapled) C->>C: 验证 OCSP 响应签名 Note over C: 无需直接联系 CA,保护隐私

4.4 CRL vs OCSP vs OCSP Stapling#

维度CRLOCSPOCSP Stapling
实时性低(7-14 天延迟)高(实时查询)中(服务器定期查询)
隐私好(下载完整列表)差(CA 知道你访问谁)好(服务器代理查询)
性能差(下载大列表)中(每次握手多一次请求)好(握手时附带)
可用性好(可缓存)差(CA 不可用则软失败)好(服务器缓存响应)
复杂度高(需服务器支持)
浏览器支持部分支持广泛支持广泛支持
Note

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 签发证书。

sequenceDiagram participant C as Client (certbot) participant LE as Let's Encrypt (ACME Server) C->>LE: 1. 注册账户(带公钥) LE->>C: 2. 返回 nonce + 账户 URL C->>LE: 3. 提交订单(domain: example.com) LE->>C: 4. 返回挑战(HTTP-01 / DNS-01) C->>C: 5. 配置挑战响应 Note over C: HTTP-01: 放置文件到 /.well-known/<br/>DNS-01: 添加 _acme-challenge TXT 记录 C->>LE: 6. 通知挑战已就绪 LE->>C: 7. 验证挑战 LE->>LE: 8. 验证通过 C->>LE: 9. 提交 CSR(带公钥) LE->>C: 10. 签发证书

5.2 HTTP-01 vs DNS-01 验证#

维度HTTP-01DNS-01
原理在网站根目录放置验证文件在 DNS 添加 TXT 记录
要求服务器 80 端口可达DNS API 可操作
通配符不支持支持
防火墙友好差(需要开放 80 端口)好(不需要入站连接)
适用场景普通 Web 服务器内网服务、通配符证书
自动化难度中(需要 DNS API)
sudo certbot certonly --nginx -d example.com -d www.example.com
sudo certbot certonly --standalone -d example.com
sudo certbot certonly --dns-cloudflare \
-d example.com -d '*.example.com' \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini
sudo certbot certonly --manual --preferred-challenges dns \
-d example.com -d '*.example.com'

5.3 自动化证书管理#

Let’s Encrypt 证书有效期只有 90 天,必须自动化续期:

sudo certbot renew --quiet
import josepy as jose
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
from acme import client, messages
private_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/v4
package main
import (
"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
价格免费5050-500/年
证书类型DV(域名验证)DV/OV/EV
有效期90 天1-2 年
通配符支持(DNS-01)支持
自动化ACME 原生部分支持
保险有(10K10K-1.5M)
适合个人/小站/自动化企业/金融/合规

六、证书信任模型#

6.1 浏览器如何决定信任一个证书#

当你访问一个 HTTPS 网站时,浏览器执行以下信任决策流程:

flowchart TB START["收到服务器证书"] --> EXPIRED{"证书在有效期内?"} EXPIRED --> |"否"| REJECT1["拒绝:证书过期"] EXPIRED --> |"是"| CHAIN{"能构建到信任库中<br/>根 CA 的证书链?"} CHAIN --> |"否"| REJECT2["拒绝:不可信的 CA"] CHAIN --> |"是"| REVOKED{"证书被吊销?<br/>(CRL/OCSP/CRLSet)"] REVOKED --> |"是"| REJECT3["拒绝:证书被吊销"] REVOKED --> |"否"| DOMAIN{"SAN 匹配<br/>请求的域名?"} DOMAIN --> |"否"| REJECT4["拒绝:域名不匹配"] DOMAIN --> |"是"| CT{"包含 CT SCT?<br/>(Chrome 要求) "} CT --> |"否"| REJECT5["拒绝:缺少透明度证明"] CT --> |"是"| TRUST["信任建立"] style REJECT1 fill:#ffcdd2 style REJECT2 fill:#ffcdd2 style REJECT3 fill:#ffcdd2 style REJECT4 fill:#ffcdd2 style REJECT5 fill:#ffcdd2 style TRUST fill:#c8e6c9

6.2 根证书存储#

根证书存储在不同操作系统的不同位置:

系统存储位置管理方式
WindowsWindows Certificate Store组策略 / certutil
macOSKeychain Access系统偏好设置
Linux/etc/ssl/certs/update-ca-certificates
AndroidSystem Trust Store系统更新
iOSSystem Trust Store系统更新
Firefox内置 cert9.db浏览器更新(独立于 OS)
ls /etc/ssl/certs/ | wc -l
openssl x509 -in /etc/ssl/certs/DigiCert_Global_Root_CA.pem -text -noout | head -20
sudo cp my-company-ca.pem /usr/local/share/ca-certificates/
sudo update-ca-certificates
sudo rm /usr/local/share/ca-certificates/my-company-ca.crt
sudo update-ca-certificates --fresh
Warning

添加根证书到系统信任库是极其危险的操作——你等于信任该 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 ConfigAndroid 原生支持移动端推荐
iOS App Transport SecurityiOS 原生支持移动端推荐
<!-- 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>
Note

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 -noout
echo | openssl s_client -connect example.com:443 2>/dev/null \
| openssl x509 -noout -dates
echo | openssl s_client -connect example.com:443 2>/dev/null \
| openssl x509 -noout -ext subjectAltName
echo | openssl s_client -connect example.com:443 -showcerts 2>/dev/null
echo | openssl s_client -connect example.com:443 2>/dev/null \
| openssl x509 -out remote-cert.pem
openssl ocsp -issuer intermediate.pem -cert leaf.pem \
-url http://ocsp.digicert.com -resp_text

8.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.key
openssl 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 = dn
req_extensions = v3_req
prompt = no
[dn]
C = CN
ST = Beijing
O = MyOrg
CN = example.com
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = example.com
DNS.2 = www.example.com
DNS.3 = api.example.com
IP.1 = 192.168.1.1
EOF
openssl req -new -key example.key -out example.csr -config san.cnf
openssl req -in example.csr -verify -noout
openssl req -in example.csr -text -noout

8.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.key
openssl req -new -key server.key -out server.csr \
-subj "/C=CN/ST=Beijing/O=DevOrg/CN=localhost"
cat > server-ext.cnf << 'EOF'
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
DNS.2 = *.localhost
IP.1 = 127.0.0.1
EOF
openssl x509 -req -in server.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt -days 365 -sha256 \
-extfile server-ext.cnf
openssl verify -CAfile ca.crt server.crt

8.4 证书调试清单#

当证书出问题时,按以下清单逐项排查:

步骤检查项命令/方法
1证书是否过期?openssl x509 -noout -dates
2SAN 是否包含目标域名?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
7Key Usage 是否正确?openssl x509 -noout -text | grep "Key Usage"
8签名算法是否安全?openssl x509 -noout -text | grep "Signature Algorithm"
9SCT 是否存在?openssl x509 -noout -text | grep "CT Precertificate"
10OCSP 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.com
check_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 -dates
subject=C = CN, ST = Beijing, O = PKI Lab, CN = PKI Lab Root CA
issuer=C = CN, ST = Beijing, O = PKI Lab, CN = PKI Lab Root CA
notBefore=May 7 14:31:29 2026 GMT
notAfter=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:0
keyUsage = critical, digitalSignature, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer
EOF
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 -issuer
Certificate request self-signature ok
subject=C = CN, ST = Beijing, O = PKI Lab, CN =PKI Lab Intermediate CA
subject=C = CN, ST = Beijing, O = PKI Lab, CN = PKI Lab Intermediate CA
issuer=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. 创建终端证书 CSR
openssl 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:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer
subjectAltName = DNS:server.pkilab.local, DNS:*.pkilab.local, IP:127.0.0.1
EOF
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 -dates
Certificate request self-signature ok
subject=C = CN, ST = Beijing, O = PKI Lab, CN = server.pkilab.local
issuer=C = CN, ST = Beijing, O = PKI Lab, CN = PKI Lab Intermediate CA
notBefore=May 7 14:31:29 2026 GMT
notAfter=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.crt
server.crt: OK

验证过程:

graph BT Server["终端证书<br/>CN=server.pkilab.local"] -->|"签名验证"| IntCA["中间 CA<br/>CN=PKI Lab Intermediate CA"] IntCA -->|"签名验证"| RootCA["Root CA<br/>CN=PKI Lab Root CA"] RootCA -->|"自签名"| Trust["信任库"] style Server fill:#e8f5e9,stroke:#2e7d32 style IntCA fill:#fff3e0,stroke:#e65100 style RootCA fill:#e3f2fd,stroke:#1565c0 style Trust fill:#fce4ec,stroke:#c62828

附.6 证书吊销与 CRL#

当私钥泄露或证书信息错误时,CA 需要吊销证书。CRL(Certificate Revocation List)是 CA 签发的”吊销名单”:

# 1. 初始化 CA 数据库
touch index.txt
echo "01" > serial.txt
echo "01" > crlnumber.txt
# 2. 生成空的 CRL
cat > ca.cnf << 'EOF'
[ca]
default_ca = CA_default
[CA_default]
database = index.txt
serial = serial.txt
crlnumber = crlnumber.txt
default_crl_days = 365
default_md = sha256
EOF
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 -10
Certificate 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:
1
No Revoked Certificates.

注:CRL 的主要缺陷是延迟——客户端必须下载完整的吊销列表才能知道证书是否被吊销。对于高流量网站,CRL 可能是几天前的。OCSP 和 OCSP Stapling 是更现代的解决方案(详见本章 5.3 节)。

附.7 实践小结#

步骤OpenSSL 命令关键参数安全要点
Root CAopenssl req -x509-days 3650, 自签名私钥离线保存
中间 CAopenssl x509 -reqCA:TRUE, pathlen:0限制路径深度
终端证书openssl x509 -reqCA:FALSE, SAN必须配置 SAN
链验证openssl verify-CAfile, -untrusted逐级验证签名
CRLopenssl ca -gencrldefault_crl_days延迟是主要缺陷

关键教训:

  1. Root CA 私钥必须离线保存:日常签发用中间 CA,Root CA 只在创建新中间 CA 时使用
  2. SAN 是必填项:现代浏览器只看 SAN,不看 CN
  3. 证书链必须完整:部署时需要同时提供终端证书和中间 CA 证书
  4. 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

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

证书与 PKI
https://blog.souloss.com/posts/cryptography/certificates-and-pki/
作者
Souloss
发布于
2026-03-30
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时