Deno TLS 测试证书体系与 PKCS#12 (PFX) 加载实现详解:从 tests/testdata/tls 证书生成到 op_node_load_pfx 源码剖析
【免费下载链接】denoA modern runtime for JavaScript and TypeScript.项目地址: https://gitcode.com/GitHub_Trending/de/deno
Deno 的 TLS 测试基础设施集中在 tests/testdata/tls 目录:一套由本地自签名 CA 签发的 RSA/ECC 域证书,以及覆盖各 MAC 算法、现代 PBES2 加密形态、无 MAC 变体和证书链打包场景的 PKCS#12 (PFX) 夹具。读完本文,你既能完整复现这套证书/密钥/PFX 夹具的生成流程,又能理解这些夹具在 Deno Node 兼容层中如何被op_node_load_pfx解析、如何被tls.createSecureContext消费,以及 Deno 在 PFX 加载路径上做了哪些防御性安全限制。
目录概览:fixtures 的用途与有效期
tests/testdata/tls/README.md 开头即给出关键事实:目录中所有证书的过期日期为 2118 年 9 月 27 日,通过 openssl 生成时统一指定了-days 36135(约 99 年),保证 CI 测试不会因证书过期而失败。README 还注明,这些证书参考了社区流传的 openssl 自签 CA 教程生成。
当前目录中的实际文件(与 README 描述一一对应,另含少量补充夹具):
| 文件 | 角色 |
|---|---|
RootCA.pem/RootCA.key/RootCA.crt | 自签根 CA(PEM 密钥 + PEM 证书 + DER 风格另存) |
localhost.key/localhost.crt | RSA 域证书私钥与证书(CN=localhost.local) |
localhost_ecc.key/localhost_ecc.csr/localhost_ecc.crt | EC(prime256v1)域证书私钥、CSR 与证书 |
localhost_sha1.pfx/localhost_sha256.pfx/localhost_sha384.pfx/localhost_sha512.pfx | 四种 MAC 算法的 PBE-SHA1-3DES 传统形态 PFX |
localhost_modern.pfx | OpenSSL 3.x 默认 PBES2 + PBKDF2 + AES-256-CBC 形态 PFX |
localhost_modern_nomac.pfx | 无 MAC(-nomac)的 PBES2 PFX |
localhost_modern_chain.pfx | 携带 CA 链(leaf + RootCA)的 PBES2 PFX |
localhost.pfx | 自签名(self-signed)单包 PFX |
invalid.crt/invalid.key | 无效证书/密钥负向夹具 |
这些 fixtures 被多个测试入口消费:tests/unit/tls_test.ts(Deno TLS 单元测试,L20 直接加载RootCA.pem,L1524-L1525 加载localhost_ecc.crt/localhost_ecc.key验证 ECC 握手)、tests/unit/tls_sni_test.ts(SNI 测试,L8-L10 加载RootCA.pem与 ECC 证书)、tests/unit_node/tls_test.ts(Nodetls模块兼容测试,L21-L26 以fromFileUrl(new URL("../testdata/tls", import.meta.url))定位目录并加载localhost.key/localhost.crt/RootCA.pem),以及 Rust 集成测试 tests/integration/serve_tests.rs(L97 使用../testdata/tls/RootCA.crt作为--cacert)。
第一步:生成本地根 CA
按 tests/testdata/tls/README.md 的原始命令,生成RootCA.key/RootCA.pem/RootCA.crt:
openssl req -x509 -nodes -new -sha256 -days 36135 -newkey rsa:2048 -keyout RootCA.key -out RootCA.pem -subj "/C=US/CN=Example-Root-CA" openssl x509 -outform pem -in RootCA.pem -out RootCA.crt参数要点:
-x509:直接输出自签证书(而非 CSR);-newkey rsa:2048:新建 2048 位 RSA 密钥;-nodes:不加密私钥(测试夹具简化处理);-days 36135:约 99 年有效期,对应 README 声明的 2118-09-27 过期日;-subj "/C=US/CN=Example-Root-CA":Subject 中 CN 即 CA 名称,README 注明Example-Root-CA仅为示例可自定义;- 第二条命令把 PEM 证书另存一份到
RootCA.crt,供以-CA签发时引用。
生成的RootCA.pem在 Deno 侧的用途是信任根:例如 tests/unit/tls_test.ts 多处将caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")]传给Deno.serve的客户端选项,使自签证书通过校验;L1279 则演示了deno run --cacert tests/testdata/tls/RootCA.crt的等价 CLI 用法。
第二步:签发域证书(RSA 与 ECC)
SAN 扩展文件 domains.txt
签发前先准备domains.txt,声明基本约束、密钥用途和 Subject Alternative Name(这里只含localhost,对应仓库中 tests/testdata/tls/domains.txt):
authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment subjectAltName = @alt_names [alt_names] DNS.1 = localhost其中basicConstraints=CA:FALSE明确该证书不是 CA;subjectAltName引用[alt_names]段,现代 TLS 校验都依赖 SAN 而非 CN,因此DNS.1 = localhost是本地测试握手成功的关键。
RSA 域证书
openssl req -new -nodes -newkey rsa:2048 -keyout localhost.key -out localhost.csr -subj "/C=US/ST=YourState/L=YourCity/O=Example-Certificates/CN=localhost.local" openssl x509 -req -sha256 -days 36135 -in localhost.csr -CA RootCA.pem -CAkey RootCA.key -CAcreateserial -extfile domains.txt -out localhost.crt- 第一条命令生成密钥 + CSR,Subject 的国家/州/城市/组织名可自定义(README 已注明);CN 为
localhost.local; - 第二条命令由 RootCA 签名(
-CA RootCA.pem -CAkey RootCA.key),-CAcreateserial生成序列号文件,-extfile domains.txt注入 SAN 等扩展。
ECC 域证书
openssl ecparam -genkey -name prime256v1 -noout --out localhost_ecc.key openssl req -new -key localhost_ecc.key -out localhost_ecc.csr -subj "/C=US/ST=YourState/L=YourCity/O=Example-Certificates/CN=localhost.local" openssl x509 -req -sha256 -days 36135 -in localhost_ecc.csr -CA RootCA.pem -CAkey RootCA.key -CAcreateserial -extfile domains.txt -out localhost_ecc.crt先以prime256v1(P-256)生成 EC 密钥,再走与 RSA 完全一致的 CSR + 签名流程。README 汇总测试所需的最终文件清单:
RootCA.crt、RootCA.key、RootCA.pemlocalhost.crt、localhost.keylocalhost_ecc.crt、localhost_ecc.key
ECC 证书的验证路径见 tests/unit/tls_sni_test.ts:SNI 场景下按主机名在 RSA 证书与localhost_ecc.crt/localhost_ecc.key之间切换,覆盖 ECDSA 握手的真实用例。
第三步:PKCS#12 (PFX) 测试包矩阵
README 的后半部分定义了五种 PFX 变体,它们不是随意生成的,而是分别瞄准 Node 兼容层 PFX 加载实现中的不同代码分支。所有包都打包localhost.crt+localhost.key。
1) 四种 MAC 算法的传统形态包
for alg in sha1 sha256 sha384 sha512; do openssl pkcs12 -export -macalg "$alg" \ -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES \ -inkey localhost.key -in localhost.crt \ -passout pass:secret \ -out "localhost_${alg}.pfx" done- 通配 passphrase 统一为
secret; - README 特别解释了
-keypbe/-certpbe的作用:把 bag 加密钉死在 legacy PBE-SHA1-3DES,让这些夹具专门覆盖 PFX 中的 PKCS#12 PBE 路径;而 OpenSSL 3.x 默认的 PBES2 + AES-256-CBC 路径由下一节单独覆盖; localhost_sha1.pfx对应 RFC 7292 默认 MAC 算法;localhost_sha256.pfx对应 OpenSSL 3.x 默认 MAC 算法;sha384/sha512覆盖其余受支持算法。
2) 现代形态包(PBES2/AES-256-CBC)
openssl pkcs12 -export \ -inkey localhost.key -in localhost.crt \ -passout pass:secret \ -out localhost_modern.pfx不指定-legacy时,OpenSSL 3.x 输出 PBES2 + PBKDF2 + AES-256-CBC 的 cert bag 与 shrouded key bag——这正是 Node 互操作的默认形态。README 注明这是针对 issue #34434 的回归覆盖:早期实现只认 legacy (SHA-1/RC2-40) 形态,而 Node 恰恰拒绝后者。
3) 无 MAC 变体(-nomac)
openssl pkcs12 -export \ -inkey localhost.key -in localhost.crt \ -passout pass:secret -nomac \ -out localhost_modern_nomac.pfx该变体用于验证两个行为:无 MAC 的 PFX 依然被接受;由于证书以明文存储、只有密钥被 shroude(加密保护),错误 passphrase 的表现是密钥解密失败而非 MAC 校验失败。
4) 证书链包(leaf + RootCA)
openssl pkcs12 -export \ -inkey localhost.key -in localhost.crt \ -certfile RootCA.pem \ -passout pass:secret \ -out localhost_modern_chain.pfx-certfile RootCA.pem使包内出现多个 cert bag,用于覆盖 leaf 与 CA 链的拆分逻辑(第一个 bag 是 leaf,其余进入ca链)以及解密包含多个 cert bag 的 EncryptedData 封装。
5) 自签名包(触发 DEPTH_ZERO_SELF_SIGNED_CERT)
openssl req -x509 -nodes -newkey rsa:2048 \ -keyout localhost_ss.key -out localhost_ss.crt \ -days 36135 -sha256 \ -subj "/C=US/CN=localhost" \ -addext "subjectAltName=DNS:localhost" openssl pkcs12 -export \ -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES \ -inkey localhost_ss.key -in localhost_ss.crt \ -passout pass:testpass \ -out localhost.pfx- passphrase 为
testpass(注意与其他包的secret不同); - 该自签名证书用于触发 TLS 握手中的
DEPTH_ZERO_SELF_SIGNED_CERT错误路径,tests/unit_node/tls_test.ts L920/L925 即断言该错误码。
源码纵深:op_node_load_pfx 如何解析这些 PFX
这些 fixtures 的消费终点是 ext/node_crypto/keys.rs 中的op_node_load_pfx(L4321-L4399)。它是 Deno 自研的 PFX 加载器(替代旧的p12crate),签名如下:
#[op2] #[serde] pub fn op_node_load_pfx( #[buffer] pfx: &[u8], #[string] passphrase: Option<String>, ) -> Result<LoadPfxResult, PfxLoadError>解析流程与 README 中各变体的设计意图一一对应:
- 结构解析:
Pkcs12::parse(pfx)失败映射为PfxLoadError::NotEnoughData,JS 侧抛出 "not enough data"(tests/unit_node/tls_test.ts L1323-L1333 用tls.createSecureContext({ pfx: "short" })验证此路径); - MAC 校验(可选):若包内含 MAC,则先校验。MAC 的 PBKDF 迭代数超过上限时直接返回
MacVerifyFailure,避免被恶意 PFX 耗尽 CPU;MAC 摘要不匹配同样返回MacVerifyFailure(JS 错误文案 "mac verify failure",对应上面localhost_sha256.pfx/localhost_modern.pfx传错 passphrase 的测试,L1349-L1358、L1375-L1384)。没有 MAC 的包被接受——这正是localhost_modern_nomac.pfx存在的意义,与 OpenSSL/Node 行为一致; - bag 遍历:只提取两类 bag——
X509证书 bag 与Pkcs8ShroudedKeyBag(加密私钥 bag,解密后是 PKCS#8 DER);未加密 KeyBag 等其他 bag 被忽略,源码注释解释了 TLS PFX 总是 shroude 密钥,因此这不是需要支持的形态。ShroudedKeyBag 解密失败抛KeyDecryptFailed(JS 文案 "failed to decrypt PFX private key",L1399-L1408 验证); - leaf 与 CA 链拆分:第一个 cert bag 作为 leaf,其余 cert bag 全部进入
ca数组,最后分别用der_to_pem转成-----BEGIN CERTIFICATE-----与-----BEGIN PRIVATE KEY-----的 PEM 文本返回(der_to_pem见 ext/node_crypto/keys.rs,按 64 字符折行)。localhost_modern_chain.pfx的测试(L1415-L1428)断言ca恰好含 1 张 RootCA 证书且与 leaf 不同。
针对不可信 PFX 的防御性上限
值得单独指出的是,Deno 在这里加了两道 Node/OpenSSL 没有的防御性上限(ext/node_crypto/keys.rs 的注释写得很清楚):
PFX_PBKDF_ITERATIONS_CAP = 600_000(L4308):PFX 中 MAC 的 PBKDF 迭代数是攻击者可控的 u32(最大约 40 亿),不加限制会让一次加载调用把 CPU 绑死几十秒。Deno 的场景里 PFX 常来自不可信输入(如服务端配置),故对齐 Mozilla NSS 的 MAC KDF 上限;OpenSSL 创建默认仅 2048 次,加固场景很少超过 10 万,该上限远超正常值;PFX_SCRYPT_MEMORY_CAP = 1 GiB(L4319):PBES2 允许 scrypt(RFC 7914),其内存开销128 * N * r字节同样攻击者可控,故限制工作集在 1 GiB(OWASP 推荐的 N=2^17、r=8 仅需约 128 MiB)。
这两处注释明确承认 "OpenSSL itself doesn't cap here, but Node trusts the caller's PFX",Deno 则因输入源不同而做了防御性收紧——从源码结构看,这是 Deno 相对 Node 原生行为的一处有意识的差异点。
JS 侧消费路径:tls.createSecureContext 的 pfx 选项
Rust op 的直接调用方是 Node 兼容层 ext/node/polyfills/_tls_common.ts(L564-L621):tls.createSecureContext(options)支持pfx为单个 Buffer 或 Buffer 数组,逐项调用op_node_load_pfx(pfxData, pfxPassphrase),把返回的cert/key注入 SecureContext,多个 pfx 的ca数组会被合并;最终cert/key取options.cert ?? pfxCert、options.key ?? pfxKey的形态,而ca在未显式指定时回退到 pfx 解出的链(const effectiveCa = options.ca != null ? options.ca : pfxCa,L615)。
端到端回归测试见 tests/unit_node/tls_test.ts 的 "tls PFX: cert+key from pfx are used for handshake"(针对 issue #34202:PFX 中的 cert/key 必须真正进入 SecureContext,否则握手会以 no-server-cert 失败):tls.createServer({ pfx, passphrase: "testpass", ... })与tls.connect({ ..., pfx, passphrase: "testpass" })双向使用localhost.pfx完成握手。
夹具与测试的完整映射
把 README 的五类 PFX 与断言放在一起,可以看到每个 fixture 都不是冗余的:
| Fixture | 生成参数要点 | 覆盖的op_node_load_pfx分支 | 测试断言 |
|---|---|---|---|
localhost_sha1/sha256/sha384/sha512.pfx | -macalg $alg+ PBE-SHA1-3DES bag | 四种 MAC 摘要算法 + legacy PBE 解密 | 均可用 passphrasesecret建出 SecureContext(tests/unit_node/tls_test.ts) |
localhost_modern.pfx | OpenSSL 3.x 默认 PBES2/AES-256-CBC | PBES2 路径;错密码在 MAC 阶段失败 | 正确密码可建 context;passphrase: "wrong"抛 "mac verify failure"(L1365-L1384) |
localhost_modern_nomac.pfx | -nomac | 无 MAC 接受 + 密钥 shroud 解密路径 | 可建 context;错密码抛 "failed to decrypt PFX private key"(L1391-L1408) |
localhost_modern_chain.pfx | -certfile RootCA.pem | 多 cert bag 拆分、多 bag EncryptedData 解密 | ca.length === 1且与 leaf 不同(L1415-L1428) |
localhost.pfx | 自签 + PBE-SHA1-3DES + passphrasetestpass | 真实 TLS 握手端到端 | 服务端/客户端双向 pfx 握手成功(L860 起);自签场景断言DEPTH_ZERO_SELF_SIGNED_CERT(L920-L925) |
适用前提与限制
- 全部 fixture 面向本地回环测试:SAN 只有
localhost,CN 为localhost.local/localhost,不要直接用于生产 TLS; - PFX 中证书与私钥的 passphrase 不统一:
localhost.pfx是testpass,其余 PFX 是secret,引用测试代码时勿混用; - README 中 "36135 天" 的有效期是相对生成时刻计算的,仓库中已生成文件的实际过期时间以证书自身为准(README 声明为 2118-09-27);
- 文中 MAC/PBE 行为描述以当前仓库的 ext/node_crypto/keys.rs 实现为准;OpenSSL 版本的默认形态(legacy vs PBES2)会随发行版变化,重新生成 fixture 时建议对照 README 的注释确认目标形态。
小结
tests/testdata/tls 表面是一份 openssl 命令备忘,实际是 Deno Node 兼容层 PFX 支持的行为规格书:五种 PFX 变体精确对应op_node_load_pfx中的 MAC 可选校验、PBE 与 PBES2 双解密路径、leaf/CA 链拆分、以及无 MAC 包错密码的降级报错路径;RSA 与 ECC 证书对则支撑 Deno 原生 TLS、SNI 与 Nodetls模块的握手测试。理解这套夹具的组织方式,能帮你快速定位 Deno TLS 相关回归测试失败是 fixture 问题还是加载器行为变化。
【免费下载链接】denoA modern runtime for JavaScript and TypeScript.项目地址: https://gitcode.com/GitHub_Trending/de/deno
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考