二、证书与安全基础:TLS/SSL 协议原理、X.509 证书结构、公钥基础设施 (PKI) 概述
说实话,在 CI/CD 流水线里折腾证书的接口回归测试,你要是对底层的安全协议一知半解,踩坑的概率会非常高。我自己刚做自动化那会儿,就被 TLS 握手失败折腾了一整个下午……后来发现是证书链没配全。所以这一章,咱们把 TLS/SSL、X.509 证书和 PKI 这三块硬骨头啃透。你不用背 RFC,但要理解它们怎么协作——这才是自动化测试设计的底气。
2.1 TLS/SSL 协议原理
TLS(Transport Layer Security)是 SSL 的继任者,目前主流是 TLS 1.2 和 TLS 1.3。你日常遇到的 HTTPS、LDAPS、甚至是 gRPC 加密传输,底层几乎都在跑 TLS。
2.1.1 握手阶段到底在干嘛?
我习惯把 TLS 握手拆成三个核心动作:身份验证、密钥协商、加密通信。说白了,就是客户端和服务器互相确认「你是你」,然后商量一把临时的对称密钥,之后所有数据都拿这把钥匙加密传输。
举个例子,TLS 1.2 的完整握手长这样:
- ClientHello —— 客户端发随机数 + 支持的密码套件列表。
- ServerHello —— 服务器选一个密码套件,返回自己的随机数。
- 服务器发送证书(X.509 公钥证书)和可选证书链。
- ServerHelloDone —— 告诉客户端「我说完了」。
- 客户端验证证书链,然后生成预主密钥(Pre-Master Secret),用服务器公钥加密发过去。
- 双方用随机数 + 预主密钥算出相同的会话密钥(Master Secret)。
- Client Finished / Server Finished —— 互相验证握手消息的完整性。
听起来步骤不少吧?TLS 1.3 直接把步骤砍到 1-RTT(甚至 0-RTT),去掉了不必要的密钥交换和证书重传。我在一个高并发接口回归测试项目里经历过,从 TLS 1.2 升级到 1.3,握手耗时直接降了 40%。
2.1.2 密码套件长什么样?
一个典型的密码套件字符串:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256。拆开看:
| 片段 | 含义 |
|---|---|
| TLS | 协议名称 |
| ECDHE | 密钥交换算法(椭圆曲线 Diffie-Hellman 临时密钥) |
| RSA | 身份验证算法(证书的公钥算法) |
| AES_128_GCM | 对称加密算法 + 模式 + 密钥长度 |
| SHA256 | 消息认证码(HMAC)使用的哈希算法 |
为什么我强调自动化回归测试里要校验密码套件?因为很多 CI/CD 环境的中间层(比如 Nginx 反向代理)会悄悄降级到弱套件来兼容旧客户端。你如果不检测,测试结果看起来是「绿色通过」,但实际通信安全性已经打折了。
TLS_AES_128_GCM_SHA256。别惊讶。
2.2 X.509 证书结构
我刚开始接触 X.509 证书时,被那一堆 ASN.1 编码搞得头大。后来发现,你只要理解证书本质上是「一个绑定了公钥和身份的声明」,并且这个声明有版本、序列号、签发者、有效期等标准字段,就够了。
2.2.1 证书核心字段
用 OpenSSL 随便看一张证书:openssl x509 -in cert.pem -text -noout,输出中关键的字段我列在下面:
| 字段 | 示例 | 解释 |
|---|---|---|
| Version | 3 | X.509 v3,支持扩展项 |
| Serial Number | 0x9c...(16 进制) | CA 分配的唯一编号,吊销时会用到 |
| Signature Algorithm | sha256WithRSAEncryption | 签发证书使用的签名算法 |
| Issuer | C=CN, O=MyCA, CN=MyRootCA | 签发该证书的 CA 身份 |
| Validity | Not Before / Not After | 有效时间区间 |
| Subject | C=CN, O=MyOrg, CN=api.example.com | 证书持有者的身份 |
| Subject Public Key Info | RSA 2048 bits 或 EC P-256 | 公钥算法 + 公钥内容 |
| Extensions | Basic Constraints, SAN, Key Usage… | v3 扩展,控制用途与限制 |
其中 SAN(Subject Alternative Name)是 CI/CD 自动化里最容易踩坑的地方。你给内网服务签发证书时,所有的域名(包括 LoadBalancer 的 FQDN、Kubernetes Service 名)必须列在 SAN 里。我有一次在流水线里用自签名证书,忘了加 Pod 的 DNS 名,结果服务间调用一直 TLS 告警,排查了一天才发现是 SAN 不匹配。
2.2.2 证书链是怎么回事?
你想想看,客户端拿到一张服务器证书,它怎么知道这证书可信?如果每个客户端都内置所有可信的公钥,那就不是互联网了。所以引入了证书链(Chain of Trust):
- 根证书(Root CA) —— 自签名,被操作系统或浏览器内置信任。
- 中间证书(Intermediate CA) —— 由根证书签发,用来签发末端实体证书。
- 实体证书(End-Entity) —— 你的服务器或客户端的身份凭证。
客户端验证时,从实体证书开始向上追溯,直到找到一个本地信任的根证书,并且每个节点的签名都合法,才算通过。我见过不少团队在自动化测试环境中只部署了服务器证书,忘了把中间证书拼接进去,导致 Java 客户端报「PKIX path building failed」。解决方案很简单:在服务器配置中把中间证书和实体证书合并成一个 PEM 文件。
避坑指南: 我曾经在 CI 脚本里用 openssl verify -CAfile root.crt server_chain.crt 来测试证书链是否完整。这是个好习惯,但注意 OpenSSL 默认只允许深度 10,如果你的中间层级超过 10,需要加 -untrusted intermediate.crt 参数手动指定中间证书。
2.3 公钥基础设施 (PKI) 概述
PKI 是一整套用于管理密钥和证书的体系,包括 CA、RA、证书库、CRL/OCSP 等组件。我把它理解成「数字世界的身份证系统」——CA 就是身份证办理中心,RA 是派出所,证书库是公开查询的黑白名单。
2.3.1 PKI 核心组件
| 组件 | 职责 |
|---|---|
| CA (Certificate Authority) | 签发、吊销证书;是信任的锚点 |
| RA (Registration Authority) | 验证申请者身份,将请求转发给 CA |
| 证书库 (Repository) | 存储已签发证书和 CRL(证书吊销列表),一般用 LDAP 或 HTTP |
| CRL (Certificate Revocation List) | CA 定期发布的已吊销证书序列号列表 |
| OCSP (Online Certificate Status Protocol) | 实时查询单张证书的吊销状态,比 CRL 更高效 |
在自动化回归测试中,我特别关注证书吊销场景的覆盖。比如你写一个接口测试,连接到一个配置了 OCSP Stapling 的服务,你需要模拟证书已被吊销的情况,看服务是否拒绝连接。因为生产环境真出问题时,很多团队发现自己的 OCSP 响应器挂了,但测试环境从来没验证过这个降级逻辑。
2.3.2 PKI 层次关系图
上图展示了一个标准 PKI 的层次结构。根 CA 是整个信任链的锚点,它签发中间 CA,中间 CA 再签发终端实体证书。CRL/OCSP 和 RA 是 PKI 的辅助服务,分别承担吊销查询和身份注册职责。我在搭建自动化测试 PKI 环境时,通常只搭一条根 CA → 中间 CA → 实体证书的链,外加一个 OCSP 模拟器(用 Python 的 Flask 写个简单应答),就能覆盖 90% 的证书场景。
2.3.3 自动化测试中 PKI 的关键关注点
- 证书有效期 —— 我习惯在 CI 脚本里提前 7 天告警即将过期的证书,而不是等测试失败才慌。用
openssl x509 -in cert.pem -noout -enddate解析到期时间,配合 GitLab CI 的 scheduled pipeline 跑。 - 吊销状态 —— 至少测一次 OCSP Responder 挂掉的情况。服务应该优雅降级还是直接拒绝?这个决策需要在测试用例中显式验证。
- 证书链顺序 —— 很多 Web 服务器要求证书链按「实体证书 → 中间证书 → 根证书」的顺序拼接。我曾经在 Nginx 里把顺序搞反了,导致 iOS 客户端报错,但 Android 和浏览器却没事。所以自动化测试必须覆盖不同客户端的行为。
一个实用脚本片段: 在回归测试中校验证书链的完整性,我常这么写:
openssl s_client -connect api.example.com:443 -servername api.example.com \
< /dev/null 2>/dev/null | openssl x509 -noout -text | grep -A2 "Subject:"
也可以用 openssl verify -CAfile root.pem -untrusted intermediate.pem server.pem 在本地静态检查。