Envoy 集成测试证书体系全解析:身份设计、生成流程与测试应用
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
Envoy 是云原生场景下的高性能边缘/中间/服务代理,其 TLS/mTLS 能力(下游 TLS 终止、上游 TLS 发起、SDS 动态证书下发、OCSP 装订等)都依赖一套可复现、覆盖各种边界场景的测试证书。本文以 test/config/integration/certs/README.md 为核心,系统拆解 Envoy 集成测试证书库中 6 类身份的设计意图、基于@envoy_toolshed//certs:gen的 Bazel 构建期生成流程,以及这些证书如何被集成测试消费。读完本文,你将能看懂 Envoy 测试证书的命名约定与信任链结构,掌握重新生成/更新证书的具体命令,并能在自己的项目中复刻这套“规范驱动、密钥入库、证书生成”的测试凭证管理方案。
一、六类身份:测试证书库的信任模型总览
该目录下共定义了 6 类身份(identities),它们分别对应 Envoy 集成测试中不同的信任域。README 给出的原始清单如下:
| 身份 | 角色 | 证书文件 | 私钥文件 | 签发者 | 签名配置 |
|---|---|---|---|---|---|
| CA | Client 与 Server 的证书颁发机构 | cacert.pem(自签名) | cakey.pem | 自身 | — |
| Client | mTLS 客户端身份 | clientcert.pem | clientkey.pem | CA | — |
| Server | 下游服务端身份 | servercert.pem | serverkey.pem | CA(使用servercert.cfg) | servercert.cfg |
| Upstream CA | Upstream 的证书颁发机构 | upstreamcacert.pem(自签名) | upstreamcakey.pem | 自身 | — |
| Upstream | 上游集群身份 | upstreamcert.pem | upstreamkey.pem | Upstream CA(使用upstreamcert.cfg) | upstreamcert.cfg |
| Upstream localhost | 本机回环上游身份 | upstreamlocalhostcert.pem | upstreamlocalhostkey.pem | Upstream CA(使用upstreamlocalhostcert.cfg) | upstreamlocalhostcert.cfg |
关键设计点:
- 两套独立的信任域:Client/Server 由“CA”签发,Upstream/Upstream localhost 由“Upstream CA”签发。这样在测试中,下游链路与上游链路可以分别做信任配置、互不干扰,例如下游验证客户端证书信任 CA,而上游验证服务端证书信任 Upstream CA。
- Upstream localhost 与 Upstream 的唯一区别:前者在证书中带有
localhost的 SAN(Subject Alternative Name)。从 upstreamlocalhostcert.cfg 可以看到,其alt_names段包含DNS.2 = localhost、IP.1 = 127.0.0.1、IP.2 = ::1,专门用于集成测试中通过 127.0.0.1/::1/localhost 访问本机测试上游时主机名校验通过。 - 命名约定:目录命名统一为“名字 + cert”(如
servercert.pem),这与多数项目“server_cert.pem”的写法不同,是历史沿袭的约定,certs.spec中特别注释了这一点。
需要特别强调的是,这些私钥均为测试专用、刻意公开。目录下的 KEYS.md 明确声明:每一个*_key.pem/*key.pem文件都是有意公开的测试私钥,它们从未保护过任何真实数据,任何拿到仓库副本的人都知道这些密钥,绝不允许在 Envoy 测试套件之外使用。密钥被检入仓库是为了保证证书产物可复现——生成器只创建证书、从不创建密钥,因此同一份 spec 总是产出相同的公钥材料。安全扫描器对这些文件报警属于预期行为,不是安全事故。
二、证书是如何生成的:certs.spec 驱动的 Bazel 构建期生成
README 明确指出:目录中的证书、证书链、OCSP 响应以及*cert_hash.h、*cert_info.h头文件,都是在Bazel 构建期由@envoy_toolshed//certs:gen生成的;只有私钥和*.cfgOpenSSL 配置被检入仓库。
2.1 重新生成证书的命令
README 给出的核心命令:
$ bazel build //test/config/integration/certs:certs $ ls bazel-bin/test/config/integration/certs/第一条命令构建certs目标,其定义在 BUILD 中:
- 使用
generated_certs()宏(来自@envoy_toolshed//certs:certs.bzl); srcs = glob(["*.cfg", "*key.pem"])——输入是检入仓库的 OpenSSL 配置与私钥;outs = CERTS——输出由 certs.spec 生成的全部证书、链、OCSP 响应及头文件(共 40 余个文件,包括cacert.pem、servercert.pem、server_ecdsacert.pem、server_ocsp_resp.der、servercert_hash.h、servercert_info.h等);static_srcs = glob(["*.cfg", "*key.pem", "google_root_certs.pem", "pqc_cacert.pem", "pqc_cacert_info.h", "san_nul_servercert.pem"])——这些静态产物不经过生成器,原样保留。
第二条命令查看bazel-bin/test/config/integration/certs/目录,即可确认生成结果。
2.2 certs.spec 的语义:一张“测试证书清单”
certs.spec 是生成流程的“输入清单”,使用 INI 风格的区块描述每张证书,主要字段:
| 区块 | 字段 | 含义 |
|---|---|---|
[cert <name>] | key | 该证书对应的私钥文件(已检入仓库) |
cfg | OpenSSL 配置(已检入仓库) | |
issuer | 签发者,self表示自签名,否则引用其他[cert ...]区块名 | |
out | 生成的证书文件名 | |
hash_header | 生成的证书 SHA-256 哈希头文件(*_hash.h) | |
info_header | 生成的证书信息头文件(*_info.h) | |
validity | 有效期策略,如expired表示生成已过期证书 | |
[concat <file>] | parts | 按顺序拼接多个证书区块,生成证书链文件 |
[ocsp <file>] | cert/issuer/responder/status/next_update_days | 为指定证书生成 OCSP 响应,responder指明签署响应的 CA |
Spec 中最值得注意的几组设计:
- CA 与证书链:
ca(自签名)→intermediate_ca(由 ca 签发)→intermediate_ca_2(由 intermediate_ca 签发),并通过[concat intermediate_ca_cert_chain.pem](parts = ca, intermediate_ca, intermediate_ca_2)与[concat intermediate_partial_ca_cert_chain.pem](parts = intermediate_ca, intermediate_ca_2)生成两条不同深度的链,用于测试不同信任链长度下的校验逻辑。 - 多算法服务端证书:除了 RSA 的
server,还定义了server_ecdsa(复用servercert.cfg,ECC 密钥)、server_ecdsa_p384、server_ecdsa_p521,用于覆盖不同椭圆曲线签名算法的证书路径。 - 多级信任的客户端:
client由ca直接签发,client2由intermediate_ca_2签发并生成完整链client2_chain.pem(parts = client2, intermediate_ca_2, intermediate_ca, ca),client_ecdsa则复用clientcert.cfg使用 ECDSA 密钥,用于测试 mTLS 下不同客户端证书链与算法的组合。 - 过期证书:
[cert expired_]区块(末尾下划线是刻意保留的历史命名,对应常量TEST_EXPIRED__CERT_HASH)通过validity = expired让生成器直接写入过期的 notBefore/notAfter 字段。README 特别说明:生成过期证书不再需要docker run ... faketime,生成器直接写入时间字段即可,简化了 CI 依赖。 - OCSP 响应:5 个
[ocsp ...]区块(server、long_server、server_ecdsa、server_ecdsa_p384、server_ecdsa_p521)全部使用status = unknown——因为从未填充过 CA 索引(CA index),每个响应对相应证书都报告unknown,next_update_days = 730指定响应的有效期窗口。
2.3 三个“不走生成器”的静态证书
pqc_cacert.pem、google_root_certs.pem和san_nul_servercert.pem(以及pqc_cacert_info.h)不在生成范围内,而是原样检入:
pqc_cacert.pem/pqc_cakey.pem:后量子(Post-Quantum Cryptography)CA,服务于 PQC 相关测试场景;google_root_certs.pem:Google 根证书包,用于模拟真实公网信任链的测试;san_nul_servercert.pem:由 generate_nul_cert.py 生成的、SAN 中内嵌 NUL(\x00)字节的畸形证书,用于验证主机名校验在 NUL 注入攻击下的行为。
三、NUL-in-SAN 证书:安全边界测试的专门造物
san_nul_servercert.pem 是一种刻意构造的畸形证书,其生成脚本 generate_nul_cert.py 展示了完整的构造手法:
- 复用既有材料:脚本从当前目录加载
upstreamcacert.pem/upstreamcakey.pem作为签发 CA,加载upstreamkey.pem提取公钥——即“用既有 CA 签发、复用既有服务器公钥”,保证与 Upstream 证书的密钥材料一致。 - 构造恶意 SAN:核心是
san = x509.SubjectAlternativeName([x509.DNSName("example.com\x00.attacker.com")]),在example.com与.attacker.com之间嵌入 NUL 字节。 - 附加标准扩展:
BasicConstraints(ca=False)、KeyUsage(digitalSignature + keyEncipherment)、ExtendedKeyUsage(SERVER_AUTH)均设为 critical,SAN 为非 critical。 - 写入文件:
comment块中详细注释了攻击原理——存在缺陷的主机名校验器(例如 Envoy 修复前的 verifySubjectAltName 路径)会在 NUL 字节处截断 SAN,从而把该证书错误匹配为合法的example.com。脚本注释还给出了 SAN 的原始十六进制:65 78 61 6d 70 6c 65 2e 63 6f 6d 00 2e 61 74 74 61 63 6b 65 72 2e 63 6f 6d,NUL 位于偏移 11 处。
这类证书的价值在于:它能以真实证书形态驱动 TLS 握手,验证 Envoy 在处理 NUL 注入式 SAN 时不会发生截断误匹配,是安全回归测试的重要固定装置。
四、OpenSSL 配置:每张证书的“身份说明书”
检入的*.cfg文件是 OpenSSL 的 req/x509 配置,定义了证书的 DN(Distinguished Name)、扩展与 SAN。以 cacert.cfg(CA 自签名)为例:
- DN 使用 Envoy 早期开发方 Lyft 的测试信息(
O=Lyft、OU=Lyft Engineering、CN=Test CA); basicConstraints = critical, CA:TRUE与keyUsage = critical, cRLSign, keyCertSign——CA 证书必须的能力约束;- 非自签名证书(如 servercert.cfg)则使用
CA:FALSE、keyUsage = nonRepudiation, digitalSignature, keyEncipherment、extendedKeyUsage = clientAuth, serverAuth。
各证书 SAN 的设计意图也各有侧重:
| 配置 | SAN 内容 | 用途 |
|---|---|---|
| servercert.cfg | spiffe://lyft.com/backend-team、http://backend.lyft.com、lyft.com、www.lyft.com | 同时覆盖 SPIFFE URI、HTTP URI 与 DNS 三种 SAN 类型 |
| clientcert.cfg | 上述 URI/DNS 之外再加IP.1 = 1.2.3.4、IP.2 = 0:1:2:3::4 | 覆盖 IPv4/IPv6 地址 SAN |
| upstreamcert.cfg | DNS.1 = *.lyft.com、IP.1 = 127.0.0.1、IP.2 = ::1 | 通配符域名 + 本机回环地址 |
| upstreamlocalhostcert.cfg | DNS.2 = localhost、IP.1 = 127.0.0.1、IP.2 = ::1 | 回环主机名校验(Upstream localhost 专属) |
| expired_cert.cfg | DNS.1 = server1.example.com | 过期场景的基础身份 |
值得一提的还有 long_servercert.cfg:它在[v3_req]与[v3_ca]段通过自定义 OID2.3.4.5 = ASN1:UTF8String:<4KB 随机串>注入约 4KB 随机数据(注释说明来自dd if=/dev/random ... | base64),刻意把证书撑得“异常巨大”,用于验证证书压缩(certificate compression)场景下大证书无法被压缩(随机数据压缩率接近于 0),以及大证书在握手中的传输与缓冲处理。
五、集成测试如何消费这些证书
证书与私钥通过runfilesPath("test/config/integration/certs/...")注入到集成测试进程,最典型的两个消费者:
静态 SDS 测试test/integration/sds_static_integration_test.cc 直接以静态路径引用证书:
TestEnvironment::runfilesPath("test/config/integration/certs/cacert.pem"); TestEnvironment::runfilesPath("test/config/integration/certs/servercert.pem");动态 SDS 测试test/integration/sds_dynamic_integration_test.cc 则覆盖更广:
- 下游 mTLS:
cacert.pem作为 CA、servercert.pem/serverkey.pem作为服务端证书、clientcert.pem/clientkey.pem作为客户端身份; - 证书轮换(key rotation):将
servercert.pem/serverkey.pem复制到{{ test_tmpdir }}/root/current/供 SDS 监听,并在测试中替换为新证书验证轮换生效; - 多算法:
server_ecdsacert.pem/server_ecdsakey.pem、server2cert.pem/server2key.pem等覆盖 ECDSA 与多证书组合。
此外,test/integration/quic_mtls_integration_test.cc、test/integration/sds_dynamic_key_rotation_setup.sh、test/integration/stats_integration_test.cc 也引用了该目录的证书,分别用于 QUIC 上的 mTLS、SDS 密钥轮换脚本和连接统计相关断言。
配套的 Bazel 库目标(见 BUILD)让 C++ 测试可以直接包含头文件:
//test/config/integration/certs:hashes:导出全部*_hash.h(如servercert_hash.h、upstreamlocalhostcert_hash.h),测试可用预计算的 SHA-256 哈希断言证书内容未被改动;//test/config/integration/certs:certs_info:导出*_info.h(如cacert_info.h、servercert_info.h、expired_cert_info.h),供测试读取证书主题、序列号、有效期等元数据。
六、给项目复刻者的实践清单
从 Envoy 这套方案可以提炼出可复用的测试证书管理范式:
- 私钥与配置入库,证书不入库:只检入
*.key.pem与*.cfg,所有派生证书、链、OCSP 响应和头文件交给构建期生成器,保证产物始终与 spec 一致、可复现。 - 用 spec 声明证书矩阵:一份 certs.spec 即可表达多 CA、多级中间 CA、多算法(RSA/ECDSA P-384/P-521)、过期证书、畸形证书、OCSP 响应等全部变体,新增场景只需增补区块。
- 显式标记测试密钥:用独立的 KEYS.md 声明“这些密钥是测试专用、可公开、禁止外流使用”,避免误用,并让安全扫描器的告警可解释。
- 安全边界要有专门的畸形样本:NUL-in-SAN、超大证书这类“反模式”固定装置,是验证主机名校验、证书压缩等安全与性能逻辑的关键输入,应像正常证书一样纳入生成与版本管理。
七、总结
Envoy 的 test/config/integration/certs 目录并非一摞随手生成的 PEM 文件,而是一套“身份设计明确、构建期自动生成、测试广泛消费”的证书工程体系:6 类身份构成两套独立信任域,certs.spec+@envoy_toolshed//certs:gen在 Bazel 构建期产出证书、链、OCSP 与哈希/信息头文件,私钥和 OpenSSL 配置以“刻意公开的测试密钥”身份入库以保证可复现性,而 NUL-in-SAN、超大随机证书、过期证书等特殊样本则专为安全与边界测试服务。理解这套机制,既能帮助你读懂 Envoy 各类 TLS/mTLS 集成测试的信任设置,也能为自建代理或网关的测试基础设施提供一份可直接借鉴的范式。
</|DSML|parameter> </|DSML|invoke> </|DSML|tool_calls>
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考