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 证书链结构图
下面这张图,把整个证书链的签发关系和信任传递画清楚了。
3.7 本章小结
这一章我们亲手做了三件事:
- 生成了自签名的根 CA 证书,它是信任的起点。
- 生成了服务端证书,配置了 SAN 和 serverAuth 用途。
- 生成了客户端证书,配置了 clientAuth 用途。
你可能会问:为什么不用一个证书搞定所有事?嗯,我在早期项目里也这么干过。后来发现,一旦证书泄露,整个系统都得重建。分层签发的最大好处,就是可以独立吊销某个终端证书,而不影响其他证书。