Luke a Pro

Luke Sun

Developer & Marketer

🇺🇦
EN||

证书和 PKI:在互联网上建立信任

| , 9 minutes reading.

1. 为什么要关心这个问题?

你访问 https://bank.example.com。你的浏览器显示了一个锁标志。它怎么知道这真的是你的银行而不是攻击者?

答案是证书——将公钥绑定到身份的数字文档。没有它们,HTTPS 将毫无意义。

理解 PKI 能帮助你:

  • 调试 TLS 证书错误
  • 为你的应用程序设置 HTTPS
  • 理解为什么「直接点击忽略警告」是危险的
  • 知道何时以及如何使用客户端证书

2. 定义

数字证书(具体来说是 X.509)绑定了:

  • 公钥
  • 身份(域名、组织等)
  • 来自可信机构的签名

公钥基础设施 (PKI) 是一个系统,包括:

  • 颁发证书的证书颁发机构 (CA)
  • 颁发和撤销证书的政策
  • 包含可信根证书的信任库
证书 = 「我,可信 CA,证明
       公钥 XYZ 属于 bank.example.com」
       + CA 的签名

3. 证书内部有什么

X.509 证书结构

Certificate:
    Version: 3
    Serial Number: 04:00:00:00:00:01:2F:4E:E1:5B:3D
    Signature Algorithm: sha256WithRSAEncryption
    Issuer: CN=DigiCert Global Root CA
    Validity:
        Not Before: Mar 08 12:00:00 2023 GMT
        Not After:  Mar 08 12:00:00 2024 GMT
    Subject: CN=www.example.com
    Subject Public Key Info:
        Public Key Algorithm: id-ecPublicKey
        EC Public Key:
            pub: 04:AB:CD:...
            ASN1 OID: prime256v1
    X509v3 Extensions:
        Subject Alternative Name:
            DNS:www.example.com
            DNS:example.com
        Basic Constraints:
            CA:FALSE
        Key Usage:
            Digital Signature, Key Encipherment
        Extended Key Usage:
            TLS Web Server Authentication

关键字段解释

Subject:证书是给谁的
  CN=www.example.com(通用名称)
  O=Example Inc(组织)

Issuer:谁签署了证书
  为此身份担保的 CA

Validity:证书何时有效
  Not Before / Not After 日期

Subject Alternative Name (SAN):
  涵盖的其他域名
  现代浏览器要求这个

Key Usage:
  密钥可以用于什么
  数字签名、密钥加密等

Extended Key Usage:
  更具体的用途
  TLS Web 服务器认证
  代码签名等

4. 信任链

信任如何工作

根 CA(自签名,在浏览器信任库中)

    │ 签署

中间 CA 证书

    │ 签署

终端实体证书(你的网站)

浏览器验证:
1. 终端证书由中间 CA 签署
2. 中间 CA 由根 CA 签署
3. 根 CA 在信任库中
4. 所有证书有效(未过期、未撤销)

为什么需要中间 CA?

安全性:根密钥极其珍贵
  - 存储在离线 HSM 中
  - 很少使用(只用于签署中间 CA)
  - 如果泄露,整个 PKI 崩溃

灵活性:
  - 如果中间 CA 泄露可以撤销
  - 不同中间 CA 用于不同目的
  - 更短的有效期

纵深防御:
  - 多一层需要攻破
  - 可以轮换中间 CA 而不改变根 CA

5. 证书颁发机构 (CA)

CA 做什么

1. 验证证书申请者的身份
   - 域名验证 (DV):证明你控制域名
   - 组织验证 (OV):验证组织存在
   - 扩展验证 (EV):广泛的法律验证

2. 颁发证书
   - 用 CA 的私钥签名
   - 包含适当的约束

3. 维护撤销信息
   - CRL(证书撤销列表)
   - OCSP(在线证书状态协议)

4. 保护他们的密钥
   - 根密钥在 HSM 中
   - 严格的访问控制
   - 定期审计

主要 CA

商业:
- DigiCert
- Sectigo(前身 Comodo)
- GlobalSign
- Entrust

免费:
- Let's Encrypt(自动化 DV 证书)
- ZeroSSL

私有:
- 你组织的内部 CA
- 用于内部服务、客户端证书

6. 获取证书

使用 Let’s Encrypt(免费、自动化)

# 安装 certbot
sudo apt install certbot

# 获取证书(独立模式)
sudo certbot certonly --standalone -d example.com -d www.example.com

# 证书文件:
# /etc/letsencrypt/live/example.com/fullchain.pem  (证书 + 中间证书)
# /etc/letsencrypt/live/example.com/privkey.pem   (私钥)

# 自动续期(每天运行两次)
sudo certbot renew

使用 ACME 协议(编程方式)

# 使用 acme 库的概念示例
from acme import client, messages
from cryptography.hazmat.primitives.asymmetric import ec

# 生成账户密钥
account_key = ec.generate_private_key(ec.SECP256R1())

# 创建 ACME 客户端
acme_client = client.ClientV2(
    directory='https://acme-v02.api.letsencrypt.org/directory',
    key=account_key
)

# 请求域名证书
order = acme_client.new_order(['example.com'])

# 完成验证(证明域名所有权)
for auth in order.authorizations:
    challenge = get_http_challenge(auth)
    # 部署验证响应
    # ...
    acme_client.answer_challenge(challenge)

# 完成并获取证书
certificate = acme_client.finalize_order(order, csr)

7. 证书验证

浏览器检查什么

1. 签名链有效
   └─ 每个证书由其颁发者签名

2. 根 CA 可信
   └─ 在浏览器/操作系统信任库中

3. 证书未过期
   └─ 当前时间在有效期内

4. 证书未被撤销
   └─ 检查 CRL 或 OCSP

5. 域名匹配
   └─ Subject 或 SAN 包含该域名

6. 密钥用途适当
   └─ TLS Web 服务器认证

7. 证书策略满足
   └─ 各种 X.509 约束

验证代码示例

from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.x509.oid import ExtensionOID
import ssl
import socket
import datetime

def validate_server_certificate(hostname, port=443):
    """验证服务器的证书"""
    # 获取证书
    context = ssl.create_default_context()
    with socket.create_connection((hostname, port)) as sock:
        with context.wrap_socket(sock, server_hostname=hostname) as ssock:
            der_cert = ssock.getpeercert(binary_form=True)

    cert = x509.load_der_x509_certificate(der_cert, default_backend())

    # 检查有效期
    now = datetime.datetime.utcnow()
    if now < cert.not_valid_before or now > cert.not_valid_after:
        return False, "证书已过期或尚未生效"

    # 检查主题备用名称
    try:
        san = cert.extensions.get_extension_for_oid(
            ExtensionOID.SUBJECT_ALTERNATIVE_NAME
        )
        names = san.value.get_values_for_type(x509.DNSName)
        if hostname not in names:
            return False, f"主机名 {hostname} 不在 SAN 中"
    except x509.ExtensionNotFound:
        return False, "没有 SAN 扩展"

    return True, "证书有效"

# 检查
valid, message = validate_server_certificate("www.google.com")
print(f"有效: {valid}, 消息: {message}")

8. 证书撤销

为什么撤销很重要

需要撤销的场景:
- 私钥泄露
- 证书颁发错误
- 域名所有权变更
- 组织不再可信

没有撤销检查:
- 持有被盗密钥的攻击者可以冒充网站
- 直到证书自然过期(最长 398 天!)

CRL vs OCSP

CRL(证书撤销列表):
┌────────────────────────────────┐
│ 已撤销序列号的大列表            │
│ 定期下载                        │
│ 可能有几兆字节                  │
│ 更新之间有延迟                  │
└────────────────────────────────┘

OCSP(在线证书状态协议):
┌────────────────────────────────┐
│ 查询:「这个证书被撤销了吗?」   │
│ 响应:「是/否」                 │
│ 实时                            │
│ 隐私问题(CA 看到查询)          │
└────────────────────────────────┘

OCSP Stapling:
┌────────────────────────────────┐
│ 服务器获取 OCSP 响应            │
│ 附加到 TLS 握手                 │
│ 客户端获得新鲜证明              │
│ 没有隐私泄露给 CA               │
└────────────────────────────────┘

9. 常见证书问题

证书错误和解决方案

错误:NET::ERR_CERT_DATE_INVALID
原因:证书已过期或尚未生效
修复:续期证书,检查服务器时钟

错误:NET::ERR_CERT_AUTHORITY_INVALID
原因:CA 不可信
修复:使用知名 CA,或安装根证书

错误:NET::ERR_CERT_COMMON_NAME_INVALID
原因:域名不在证书中
修复:获取包含正确 SAN 的证书

错误:SSL_ERROR_BAD_CERT_DOMAIN
原因:主机名不匹配
修复:使用正确的主机名访问

错误:证书链不完整
原因:缺少中间证书
修复:配置服务器使用完整链

调试证书

# 查看证书详情
openssl s_client -connect example.com:443 -showcerts

# 检查证书日期
openssl s_client -connect example.com:443 2>/dev/null | \
  openssl x509 -noout -dates

# 检查证书链
openssl s_client -connect example.com:443 -showcerts 2>/dev/null | \
  openssl x509 -noout -issuer -subject

# 验证证书
openssl verify -CAfile ca-bundle.crt certificate.pem

10. 客户端证书

用途

服务器证书:服务器向客户端证明身份
客户端证书:客户端向服务器证明身份

用例:
- 服务间认证的双向 TLS (mTLS)
- 无需密码的用户认证
- 物联网设备认证
- VPN 认证

设置 mTLS

import ssl
import socket

# 服务器端
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_context.load_cert_chain('server.crt', 'server.key')
server_context.load_verify_locations('client-ca.crt')
server_context.verify_mode = ssl.CERT_REQUIRED  # 要求客户端证书

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    sock.bind(('0.0.0.0', 8443))
    sock.listen()
    with server_context.wrap_socket(sock, server_side=True) as ssock:
        conn, addr = ssock.accept()
        # conn.getpeercert() 返回客户端证书

# 客户端
client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client_context.load_cert_chain('client.crt', 'client.key')
client_context.load_verify_locations('server-ca.crt')

with socket.create_connection(('server.example.com', 8443)) as sock:
    with client_context.wrap_socket(sock, server_hostname='server.example.com') as ssock:
        # 使用双向认证连接
        pass

11. 自签名证书

何时使用

适合:
- 开发和测试
- 内部服务(使用私有 CA)
- 学习和实验

不适合:
- 公开网站
- 被外部客户端使用的生产 API
- 任何信任重要的地方

创建自签名证书

# 生成私钥
openssl genrsa -out server.key 2048

# 生成自签名证书
openssl req -new -x509 -key server.key -out server.crt -days 365 \
  -subj "/CN=localhost"

# 带 SAN 扩展(现代浏览器要求)
openssl req -new -x509 -key server.key -out server.crt -days 365 \
  -subj "/CN=localhost" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

12. 证书透明度

它解决什么问题

如果 CA 为你的域名颁发了证书会怎样?
- 你可能永远不知道
- 攻击者可以拦截流量
- CA 泄露是灾难性的

证书透明度:
- 所有证书记录到公共日志
- 你可以监控你的域名的日志
- 错误颁发的证书可被检测

如何工作

1. CA 颁发证书
2. CA 提交到 CT 日志
3. 日志返回签名证书时间戳 (SCT)
4. 证书包含 SCT
5. 浏览器验证 SCT 存在
6. 域名所有者监控日志

工具:
- crt.sh(搜索 CT 日志)
- Google 证书透明度
- Cert Spotter(监控)

13. 本章小结

三点要记住:

  1. 证书将密钥绑定到身份。 证书是 CA 的签名声明,表明公钥属于特定域名或实体。

  2. 信任来自根 CA。 你的浏览器信任约 100 个根 CA。那些 CA 签署中间 CA,中间 CA 签署终端证书。如果任何环节断裂,信任就失败了。

  3. 证书管理是运营工作。 续期、撤销和链配置是持续的工作。尽可能使用 Let’s Encrypt 自动化。

14. 下一步

我们已经介绍了非对称密码学、签名和证书。但还有另一个基本原语:确保数据没有被修改,而不需要证明谁修改了它。

在下一篇文章中:HMAC 和数据完整性——当你已经共享密钥时,如何用对称密钥检测篡改。