Skip to content

元数据卡

  • 前置知识:第1章 密码学基础(对称/非对称加密、哈希)
  • 预计时间:50 分钟
  • 核心难度:进阶
  • 完成标志:能解释 X.509 证书的结构,能用 openssl 生成自签名证书,能理解证书链验证过程

你的进度

你学会了用非对称加密法阵保护通信——你有一把公开符文,对方有一把私密符文,加密的法术信函只有私密符文持有者能解开。

但你遇到一个新问题:你怎么确定你手里的公开符文真的属于“灰塔的传送塔卫”,而不是某个冒充塔卫的邪术师?

上一章结束时我们说“密钥分发问题”已经被非对称加密解决了,但那只是把问题从“怎么安全传递密钥”变成了“怎么安全地获取对方的公开符文”。这是一个信任问题,而信任在魔法世界里需要一个基础设施——符文认证体系。

你的任务

理解 PKI(Public Key Infrastructure)如何解决"公钥归属"问题。你将掌握 X.509 证书的结构、证书链的验证机制、CA 的信任模型,以及现代实践中的 ACME 自动签发和 Certificate Transparency。

本章分层

  • 必读:X.509 证书结构、证书链、自签名证书实操、openssl
  • 选读:ACME 协议流程、CRL 与 OCSP
  • 进阶:Certificate Transparency 的 Merkle Tree 证明

破局 · 溯源

问题:你收到一封自称"边境要塞传令兵"的信,附带了对方的公钥。你怎么知道这真的是要塞的公钥?

直觉的做法:找一个大家都信任的第三方来做担保。这个第三方被称为证书颁发机构(CA, Certificate Authority)

CA 的工作是:

  1. 验证申请者的身份(你是你要声称的那个人吗?)
  2. 签发一个数字证书,把"公钥"绑定到"身份"
  3. 当有人质疑某公钥的归属时,CA 可以验证该证书

X.509 证书长什么样?

X.509 是定义证书格式的标准(ITU-T 标准,RFC 5280)。一个 X.509 证书包含:

证书(v3 格式):
├── 版本号(Version,目前 v3)
├── 序列号(Serial Number,CA 唯一分配)
├── 签名算法(Signature Algorithm,如 sha256WithRSAEncryption)
├── 签发者(Issuer,签发此证书的 CA 的名称)
├── 有效期(Validity)
│   ├── notBefore:生效时间
│   └── notAfter:到期时间
├── 主题(Subject,证书持有者名称)
├── 公钥信息(Subject Public Key Info)
│   ├── 算法(如 RSA/ECC)
│   └── 公钥值
├── 扩展(Extensions,v3 新增)
│   ├── Subject Alternative Name(SAN,现代 Web 用这个而不是 Subject)
│   ├── Key Usage(digitalSignature, keyEncipherment 等)
│   ├── Extended Key Usage(serverAuth, clientAuth)
│   ├── Basic Constraints(是否 CA 证书)
│   └── ... 更多扩展
└── 签名(CA 的签名,用 CA 的私钥对整个证书做哈希后签名)

用 openssl 查看一个真实的网站证书:

bash
openssl s_client -connect github.com:443 -servername github.com </dev/null 2>/dev/null \
  | openssl x509 -text -noout | head -60

你会看到类似这样的输出:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            04:...
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, O=DigiCert Inc, CN=DigiCert Global G2 TLS RSA SHA256 2020 CA1
        Validity
            Not Before: Mar 15 00:00:00 2024 GMT
            Not After : Mar 20 23:59:59 2025 GMT
        Subject: CN=github.com
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                ...
        X509v3 extensions:
            X509v3 Subject Alternative Name:
                DNS:github.com, DNS:www.github.com

证书链:多层信任

CA 的证书是谁签发的?另一个更高级的 CA。最终形成一个链:

根 CA(自签名)
  └── 中间 CA1(根 CA 签发)
        └── 中间 CA2(中间 CA1 签发)
              └── 最终实体证书(中间 CA2 签发,如 github.com)

根 CA 的证书是自签名的——它自己签发自己的证书。浏览器和操作系统中内置了一组根证书(约 100-150 个),这些就是信任锚点。

验证过程是链式的:

python
# 伪代码:证书链验证
def verify_cert_chain(leaf_cert, intermediate_certs, root_store):
    """
    验证证书链:
    1. 从叶子证书开始
    2. 找到签发它的 CA 证书
    3. 用 CA 的公钥验证叶子证书的签名
    4. 递归验证 CA 证书,直到根证书
    """
    cert = leaf_cert
    while not cert.is_self_signed():
        issuer = find_issuer_cert(cert, intermediate_certs, root_store)
        if issuer is None:
            return False  # 找不到签发者
        
        # 用签发者的公钥验证当前证书的签名
        if not verify_signature(cert, issuer.public_key):
            return False
        
        # 检查证书是否在有效期内
        if not cert.is_within_validity_period():
            return False
        
        # 检查证书是否已被吊销
        if is_revoked(cert):
            return False
        
        cert = issuer  # 向上递归
    
    # 最终:验证根证书在信任存储中
    return cert in root_store

做一个自签名证书

为了理解证书的生成过程,自己签一个:

bash
# 1. 生成私钥
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
  -out mykey.pem

# 2. 创建证书签名请求(CSR)
openssl req -new -key mykey.pem -out mycsr.csr \
  -subj "/CN=myapp.local/O=MyOrg/C=CN" \
  -addext "subjectAltName=DNS:myapp.local,DNS:localhost,IP:127.0.0.1"

# 3. 自签名(用 CA 角色签发证书)
openssl x509 -req -in mycsr.csr -signkey mykey.pem \
  -out mycert.pem -days 365 \
  -extfile <(echo "subjectAltName=DNS:myapp.local,DNS:localhost") \
  -sha256

# 4. 查看证书内容
openssl x509 -in mycert.pem -text -noout

这个自签名证书可以直接用于开发环境的 TLS。打开一个简单的 Python HTTPS 服务器:

python
# server.py
import http.server
import ssl

server = http.server.HTTPServer(('0.0.0.0', 8443), http.server.SimpleHTTPRequestHandler)

context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('mycert.pem', 'mykey.pem')

server.socket = context.wrap_socket(server.socket, server_side=True)
print("HTTPS 服务器运行在 https://localhost:8443")
server.serve_forever()

访问 https://localhost:8443 时,浏览器会警告"证书不受信任"——这是对的,因为这个证书不是由浏览器信任的 CA 签发的。

CA 签发证书的流程(ACME)

在真实环境中,你不会自签名,而是去 Let's Encrypt(免费 CA)签一个。Let's Encrypt 使用 ACME(Automatic Certificate Management Environment)协议自动签发证书。

ACME 的核心流程(以 HTTP-01 验证为例):

1. 客户端(你的服务器)生成密钥对和 CSR
2. 客户端向 ACME 服务器请求签发证书
3. 服务器给出挑战:在特定路径下放一个特定的 token 文件
4. 客户端在网站根目录创建文件:/.well-known/acme-challenge/<token>
5. ACME 服务器 HTTP 访问该路径验证控制权
6. 验证通过,ACME 服务器签发证书
7. 客户端下载证书和中间 CA 证书

用 certbot 一行搞定:

bash
# 安装 certbot(Ubuntu)
sudo apt install certbot python3-certbot-nginx

# 签发证书
sudo certbot --nginx -d example.com -d www.example.com

# 证书会放到 /etc/letsencrypt/live/example.com/
# 包含:fullchain.pem(完整链)+ privkey.pem(私钥)

Let's Encrypt 签发的证书有效期是 90 天。这看起来不方便,但这是刻意的设计——短有效期减少了吊销的复杂度,而且推动了自动化。

吊销检查

证书可能因为私钥泄露或身份变更而需要在到期前失效。吊销检查有两种方式:

  • CRL(Certificate Revocation List):CA 定期发布一份被吊销证书的序列号列表。客户端下载并缓存。
  • OCSP(Online Certificate Status Protocol):实时查询,客户端向 CA 的 OCSP 服务器询问某个证书是否有效。

OCSP 比 CRL 更实时,但有一个隐私问题——CA 能知道访问了哪些网站。OCSP Stapling 解决了这个问题:服务器自己定期从 CA 获取 OCSP 响应,在 TLS 握手时捎带给客户端。

Certificate Transparency(CT)

2011 年,一个攻击者冒充 CA(DigiNotar)签发了 Google 的假证书,用于针对伊朗用户的中间人攻击。这件事暴露了 PKI 的一个根本缺陷:任何 CA 都可以为任何域名签发证书,而且没有全局可见性

CT 解决这个问题的方案:所有 CA 签发的证书都必须记录在公共日志中,任何人都可以审计。

Certificate Transparency 日志(Merkle Tree 结构):
├── Root hash(日志的当前状态)
├── 证书条目 1(含签名时间戳)
├── 证书条目 2
├── ...
└── 证书条目 N

每个证书被签发时,CA 提交到日志
日志返回 Signed Certificate Timestamp(SCT)
SCT 嵌入在证书中或通过 TLS 扩展传递
浏览器要求证书至少包含 2-3 个不同的 SCT

从 2018 年起,Chrome 要求所有新签发的 SSL/TLS 证书必须有 SCT 嵌入,否则视为不可信。


常见陷阱

  • 通配符证书滥用*.example.com 可以保护 www.example.com 但不能保护 example.com,更不能保护 other.com。现代做法是用多 SAN 证书。
  • 自签名证书在生产环境使用。自签名证书只用于开发和内部测试。生产环境应该通过公共 CA 或内部 CA。
  • 忽略证书续期。Let's Encrypt 的 90 天有效期需要自动化续期。用 systemd timer 或 cron 来保证。
  • 证书链不完整。有些服务器只发送叶子证书,不发送中间 CA 证书。浏览器可能需要额外下载中间证书(增加握手延迟,部分旧客户端不支持)。
  • 私钥权限过宽。私钥文件应该是 600 权限,只有运行 web 服务器的用户可读。
  • 用 Subject 而不是 SAN。从 Chrome 58 开始,浏览器只看 Subject Alternative Name 来确定站点身份,不再匹配 Common Name(CN)。

通关挑战

  • 热身:用 openssl 连接 google.com:443,提取并打印服务器证书的 Issuer 和 Subject。然后写一行命令提取证书的有效期。
  • 挑战:创建一个简易的本地 HTTPS 服务。首先生成 CA 根证书(自签名),然后用它签发一个服务器证书。配置 nginx 或 Python HTTP 服务器使用这个证书,并让浏览器信任你的 CA。
  • 排障:一个同事说"我的 HTTPS 网站可以在 curl 中正常访问,但浏览器报 NET::ERR_CERT_COMMON_NAME_INVALID"。诊断并修复这个问题。
  • 观察:访问 https://crt.sh 搜索你的域名(如果有),看看有哪些证书记录在 CT 日志中。

旅人笔记

  • PKI 解决"公钥属于谁"的信任问题,核心是 X.509 证书和 CA 体系
  • X.509 证书包含身份信息、公钥、有效期和签发者的签名
  • 证书链从根 CA 到中间 CA 到叶子证书,根证书由操作系统/浏览器内置
  • ACME 自动化(Let's Encrypt)使证书生命周期管理不再需要手动操作
  • Certificate Transparency 让所有证书对公众可见,防止恶意 CA 暗箱操作

下一站预告

你有加密能力,也有验证对方身份的证书机制。但对方是谁——身份怎么确认?持有私钥就叫"认证"了吗?下一章,我们拆解认证与授权体系。

Built with VitePress | Software Systems Atlas