3. OpenSSL 基础操作:构建你的第一个证书链

说实话,很多搞 PKI 的人,最后栽跟头都栽在最基础的证书链上。我自己刚入行那会儿,也闹过笑话——生成了一堆证书,结果服务端死活不认客户端,折腾了两天才发现是根证书没装对。

这一章,我们就亲手搭一套完整的证书链。从根证书,到服务端证书,再到客户端证书。你跟着敲一遍,很多概念就通了。

3.1 为什么需要证书链?

你想想看,如果每个服务端证书都要浏览器或操作系统内置信任,那全球几十亿个网站,谁的设备也装不下。所以 PKI 用了「信任传递」的思路:

  • 根 CA:被广泛信任,自己签自己(自签名)。
  • 中间 CA:由根 CA 签发,负责具体签发业务证书。
  • 终端证书:服务端或客户端用的,由中间 CA 签发。

浏览器拿到服务端证书后,会顺着证书里的「签发者」字段往上找,直到找到它内置信任的根证书。这条路径,就是证书链。

核心要点:证书链的完整性,决定了 TLS 握手能否成功。缺了中间证书,客户端就会报「unable to get local issuer certificate」。

3.2 生成 CA 根证书

根证书是整个信任体系的基石。我个人习惯把根证书的私钥单独放在一个加密目录里,权限设到 400,因为一旦私钥泄露,整个 PKI 体系就废了。

3.2.1 生成根 CA 私钥

# 生成 4096 位 RSA 私钥,加密存储
openssl genrsa -aes256 -out ca-key.pem 4096

这里会提示你输入密码。我建议用 16 位以上的随机密码,记在密码管理器里。曾经有个客户把根 CA 密码写在便签纸上贴在显示器上……嗯,我帮他们重新建了一套。

3.2.2 生成根 CA 证书

# 自签名根证书,有效期 10 年
openssl req -x509 -new -nodes \
  -key ca-key.pem \
  -sha256 -days 3650 \
  -out ca-cert.pem \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=MyLab/CN=MyLab Root CA"

几个参数说明:

  • -x509:直接输出自签名证书,不需要 CSR。
  • -nodes:不对私钥加密(因为前面已经加密过了)。
  • -days 3650:根证书有效期通常设 5-10 年。
  • -subj:主题信息,CN 字段要能标识这是根 CA。

我的习惯:根证书的 CN 里加上 "Root CA" 字样,方便后续区分。另外,根证书的 Basic Constraints 必须标记为 CA:TRUE,否则它不能用来签发下级证书。

3.3 生成服务端证书

服务端证书,就是你的网站或 API 网关要用的证书。这里有个坑——很多人忘了配置 SAN(Subject Alternative Name),导致 Chrome 报错。

3.3.1 创建 OpenSSL 配置文件

我建议每次都写一个配置文件,而不是靠命令行参数。这样可重复、可审计。

# server.conf
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no

[req_distinguished_name]
C = CN
ST = Beijing
L = Beijing
O = MyLab
CN = myserver.mylab.com

[v3_req]
keyUsage = keyEncipherment, digitalSignature
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = myserver.mylab.com
DNS.2 = *.mylab.com
IP.1 = 192.168.1.100

注意:CN 字段在 RFC 2818 中已被弃用,浏览器现在只看 SAN。所以 SAN 里一定要写上你的域名或 IP,否则证书装上也没用。

3.3.2 生成服务端私钥和 CSR

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

# 生成 CSR
openssl req -new \
  -key server-key.pem \
  -out server.csr \
  -config server.conf

3.3.3 用根 CA 签发服务端证书

openssl x509 -req \
  -in server.csr \
  -CA ca-cert.pem \
  -CAkey ca-key.pem \
  -CAcreateserial \
  -out server-cert.pem \
  -days 365 \
  -sha256 \
  -extfile server.conf \
  -extensions v3_req

-CAcreateserial 会生成一个 ca-cert.srl 文件,记录序列号。下次签发时,它会自动递增,避免序列号重复。

3.4 生成客户端证书

客户端证书用于 mTLS(双向 TLS)场景。比如微服务之间的认证,或者 VPN 接入。我在项目中遇到过客户把服务端和客户端证书搞混,结果调试了一整天。

3.4.1 生成客户端私钥和 CSR

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

# 生成 CSR,CN 通常填用户名或设备 ID
openssl req -new \
  -key client-key.pem \
  -out client.csr \
  -subj "/C=CN/ST=Beijing/O=MyLab/CN=alice@mylab.com"

3.4.2 签发客户端证书

openssl x509 -req \
  -in client.csr \
  -CA ca-cert.pem \
  -CAkey ca-key.pem \
  -CAcreateserial \
  -out client-cert.pem \
  -days 365 \
  -sha256 \
  -extfile <(echo -e "extendedKeyUsage=clientAuth\nkeyUsage=digitalSignature")

注意这里指定了 extendedKeyUsage=clientAuth,这样客户端证书就只能用于客户端认证,不能冒充服务端。

3.5 验证证书链

证书都生成好了,怎么确认链是完整的?我一般用三步验证法:

3.5.1 查看证书内容

# 查看根证书
openssl x509 -in ca-cert.pem -text -noout | grep -E "Subject:|Issuer:"

# 查看服务端证书
openssl x509 -in server-cert.pem -text -noout | grep -E "Subject:|Issuer:"

输出应该类似:

# 根证书
Subject: CN = MyLab Root CA
Issuer:  CN = MyLab Root CA    # 自签名,Subject == Issuer

# 服务端证书
Subject: CN = myserver.mylab.com
Issuer:  CN = MyLab Root CA    # 由根 CA 签发

3.5.2 验证证书链

# 验证服务端证书
openssl verify -CAfile ca-cert.pem server-cert.pem

# 验证客户端证书
openssl verify -CAfile ca-cert.pem client-cert.pem

如果输出 OK,说明链是完整的。如果报错,八成是中间证书没配,或者根证书没在 -CAfile 里。

3.5.3 模拟 TLS 握手测试

# 启动一个测试服务端(需要输入私钥密码)
openssl s_server \
  -cert server-cert.pem \
  -key server-key.pem \
  -CAfile ca-cert.pem \
  -Verify 1 \
  -port 8443

# 另一个终端,用客户端证书连接
openssl s_client \
  -connect localhost:8443 \
  -cert client-cert.pem \
  -key client-key.pem \
  -CAfile ca-cert.pem

如果握手成功,你会看到 SSL handshake has read ... 和 Verify return code: 0 (ok)。

避坑指南:我曾经在测试时忘了加 -CAfile,结果客户端一直报证书验证失败。后来才发现,OpenSSL 默认不信任任何 CA,你必须显式指定信任的根证书。

3.6 证书链结构图

下面这张图,把整个证书链的签发关系和信任传递画清楚了。

证书链结构图 根 CA 自签名 · CA:TRUE 有效期 10 年 签发 签发 服务端证书 CN = myserver.mylab.com EKU: serverAuth 有效期 1 年 客户端证书 CN = alice@mylab.com EKU: clientAuth 有效期 1 年 信任传递过程 1. 客户端/浏览器内置信任根 CA 证书 2. 服务端发送证书链(服务端证书 + 根证书) 3. 客户端验证服务端证书的 Issuer 指向根证书 4. 验证通过 → TLS 握手成功

3.7 本章小结

这一章我们亲手做了三件事:

  • 生成了自签名的根 CA 证书,它是信任的起点。
  • 生成了服务端证书,配置了 SAN 和 serverAuth 用途。
  • 生成了客户端证书,配置了 clientAuth 用途。

你可能会问:为什么不用一个证书搞定所有事?嗯,我在早期项目里也这么干过。后来发现,一旦证书泄露,整个系统都得重建。分层签发的最大好处,就是可以独立吊销某个终端证书,而不影响其他证书。