首页
/ Playwright 测试资产解析:重复 CN 属性证书(multi-value-rdn)与 Security Details 解析

Playwright 测试资产解析:重复 CN 属性证书(multi-value-rdn)与 Security Details 解析

2026-09-06 18:26:07作者:温玫谨Lighthearted

本篇技术指南围绕 Playwright 仓库中一组特殊的测试证书资产(tests/assets/multi-value-rdn/)展开:它是一张 subject 与 issuer 中包含两个 CN(Common Name)属性的自签名证书,专门用于验证 Playwright 的 API 层(playwright.request / response.securityDetails())在遇到 "multi-valued RDN" 证书时能否正确解析出主体名称。读完本文你将掌握:这张证书如何用 OpenSSL 生成、为什么双 CN 会给 TLS 证书解析带来坑、Playwright 底层如何在服务端裁剪多值 RDN 并只取第一个 CN,以及仓库中的对应测试用例与相关文件路径。

一、这是什么:一个为 TLS 边界场景准备的证书资产

tests/library/ 系列测试中,Playwright 需要验证浏览器与网络层对各类 TLS 服务器证书的处理。绝大多数测试使用的是单 CN 的本地自签名证书(如 playwright-test),而 multi-value-rdn 目录提供的则是一种极端的证书形态

tests/assets/multi-value-rdn/
├── README.md   # 生成说明(本指南对应的文档)
├── cert.pem    # 自签名证书,subject/issuer 含两个 CN
└── key.pem     # 对应的 RSA-2048 私钥

根据 README 的原始描述,这是一张 "self-signed certificate whose subject and issuer DNs contain two CN attributes",即 subject 与 issuer 的可辨识名(DN)中都同时包含两个 CN 属性。用 OpenSSL 实际查验 cert.pem 可以得到确凿的证书结构:

subject=CN = localhost, CN = secondary-name
issuer=CN = localhost, CN = secondary-name

也就是说,证书主体名与颁发者名都同时携带 localhostsecondary-name 两个公共名,这在语法上属于 X.509 的 multi-valued RDN(同一相对可辨识名集合中出现两个同类型属性)。正常浏览器与 Node.js 并不会因此拒绝连接,但它会对上层"提取一个字符串形式的 subjectName"的代码造成歧义——这正是 Playwright 想专门用一张资产来固化的场景。

二、如何生成这张证书:OpenSSL 命令行逐项拆解

README 给出了完整的生成命令:

openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
  -keyout key.pem -out cert.pem \
  -subj "/CN=localhost/CN=secondary-name" -addext "subjectAltName=DNS:localhost"

各参数含义与作用如下:

参数 含义 关键点
req -x509 直接生成自签名 X.509 证书(而非 CSR) 证书由自己签发,issuer 与 subject 一致
-newkey rsa:2048 同时生成 2048 位 RSA 密钥 与仓库内 key.pem 相符
-nodes 私钥不加密(no DES) 测试进程可无密码直接读取 key.pem
-days 3650 有效期约 10 年 查验 cert 有效期大致落在 2026 年 7 月至 2036 年 7 月,与 -days 3650 吻合
-keyout key.pem -out cert.pem 分别输出私钥与证书 对应仓库中的两个文件
-subj "/CN=localhost/CN=secondary-name" 以斜杠分隔的字符串形式指定 DN,且写入了两个 CN 核心所在:这是产生 multi-valued RDN 的直接原因
-addext "subjectAltName=DNS:localhost" 追加 SAN 扩展,声明 DNS 名 localhost 使证书能按域名被 TLS 校验使用

一个值得注意的实现细节是:-subj-addext 分别是旧版 OpenSSL 的 -subj/-extensions 写法的替代形态(在 1.1.1 起普遍可用)。其中 -subj 的值按 OpenSSL 的 DN 语法解析,CN= 连续出现两次不会互相覆盖,而是在同一 RDN 集合内生成两个 CN 属性,最终被序列化进证书的 subject 与(因自签名)issuer 字段。

三、双 CN 为什么会是问题:多值 RDN 的解析歧义

对大多数库而言,一个 DN 集合通常只含一个 CN,因此上层代码常写成 cert.subject.CN 并直接当作字符串使用。但当证书中出现两个 CN(如 CN=localhost, CN=secondary-name)时,Node.js 的 tls.TLSSocket.getPeerCertificate() 返回的对象中,subject.CN 会是一个字符串数组而非字符串:

// Node 对 multi-value RDN 的实际返回形态
{
  subject: { CN: ['localhost', 'secondary-name'], /* ... */ },
  issuer:  { CN: ['localhost', 'secondary-name'], /* ... */ }
}

此时,任何假设 CN 是标量的下游代码(例如直接 cert.subject.CN 赋给某字符串字段、或将其用于 trace 输出、断言比较)都可能出现类型错误或产生不可预期的值。Playwright 将该场景固化为回归测试,其注释指向一个上游问题跟踪编号(见 global-fetch.spec.tsannotation 字段),说明这确实来自真实用户报告。

四、在测试中的用法:Security Details 解析回归测试

这组资产被 tests/library/global-fetch.spec.ts 的用例 should return security details for certificate with multiple CN attributes(约第 322–344 行)使用,完整逻辑如下:

it('should return security details for certificate with multiple CN attributes', {
  annotation: { type: 'issue', description: /* 上游 issue 描述 */ },
}, async ({ playwright, asset }) => {
  const server = utils.createHttpsServer({
    key:  fs.readFileSync(asset('multi-value-rdn/key.pem')),
    cert: fs.readFileSync(asset('multi-value-rdn/cert.pem')),
  }, (req, res) => {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ ok: true }));
  });
  await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
  try {
    const request = await playwright.request.newContext({ ignoreHTTPSErrors: true });
    const response = await request.get(`https://localhost:${server.address().port}/`);
    expect(response.status()).toBe(200);
    const securityDetails = await response.securityDetails();
    expect(securityDetails.subjectName).toBe('localhost');
    expect(securityDetails.issuer).toBe('localhost');
    await request.dispose();
  } finally {
    await new Promise(resolve => server.close(resolve));
  }
});

逐段解读测试意图:

  1. 构造 HTTPS 服务端:通过测试工具 utils.createHttpsServer(其实现见 tests/config/testserver/index.ts)启动本地服务,并直接以 fs.readFileSync 读入 key.pemcert.pem 作为服务端凭证——这正是 asset('multi-value-rdn/...') 辅助函数把相对路径解析到 tests/assets/ 目录的用法。
  2. 发起真实 TLS 请求:用 playwright.request.newContext()(APIRequestContext)以 https://localhost:<port>/ 发起请求。此时 Node 会与服务端完成 TLS 握手,getPeerCertificate() 拿到的正是那张双 CN 证书。
  3. 断言取到第一个 CN:测试期望 securityDetails.subjectName === 'localhost'issuer === 'localhost'。也就是说,即使 DN 里还有 secondary-name,对外暴露的字符串字段也必须稳定取第一个 CN,而不允许出现数组、对象或任意顺序的值。

五、底层实现:服务端如何裁剪多值 RDN

测试背后对应的生产代码在 packages/playwright-core/src/server/fetch.tscaptureSecurityDetails 回调中(约第 609–629 行):

const captureSecurityDetails = (socket: net.Socket) => {
  if (!(socket instanceof TLSSocket))
    return;
  const protocol = socket.getProtocol() ?? undefined;
  const peerCertificate = socket.getPeerCertificate();
  if (!peerCertificate.valid_from) {
    // A resumed TLS session returns an empty getPeerCertificate(), and we use the cached data.
    securityDetails = { ...this._certificateDetails.get(certificateCacheKey), protocol };
    return;
  }
  // Multi-value RDNs are reported as string arrays, take the first common name.
  const commonName = (field: string | string[] | undefined) => Array.isArray(field) ? field[0] : field;
  securityDetails = {
    protocol,
    subjectName: commonName(peerCertificate.subject?.CN),
    validFrom: new Date(peerCertificate.valid_from).getTime() / 1000,
    validTo: new Date(peerCertificate.valid_to).getTime() / 1000,
    issuer: commonName(peerCertificate.issuer?.CN)
  };
  this._certificateDetails.set(certificateCacheKey, securityDetails);
};

可以从源码提炼三个实现事实:

  1. 显式处理数组型 CN:代码注释直接写明 "Multi-value RDNs are reported as string arrays, take the first common name.",并用 Array.isArray(field) ? field[0] : field 归一化。这既兼容单 CN(字符串)也兼容双 CN(数组),保证了 securityDetails.subjectName 永远是 string | undefined
  2. TLS 会话恢复时的兜底:当 !peerCertificate.valid_from(会话复用导致 Node 返回空证书对象)时,代码不会再次解析证书,而是复用按 certificateCacheKey 缓存的首次握手结果,再补上当前 getProtocol()
  3. 对外字段契约:最终产出的对象遵循 packages/playwright-core/src/server/network.tssubjectName?: string 的结构定义——这正是多值 RDN 需要被"取第一个"来保持字符串类型的原因。

六、数据如何流到用户侧:securityDetails API 链路

服务端拿到归一化后的 Security Details 后,会经 CDP / 各浏览器协议通道与 IPC 传输到客户端。客户端侧,页面 Response 与 APIRequestContext Response 都暴露了 securityDetails()

对用户而言,response.securityDetails() 返回形如 { subjectName, issuer, validFrom, validTo, protocol } 的对象;对 HTTP(非 TLS)响应则返回 nullglobal-fetch.spec.ts 中另有 should return null security details for http response 用例覆盖)。本测试资产的加入,保证了"TLS 正常、协议为 TLSv1.3、证书 DN 极不规整"时该 API 依然返回确定性的字符串。

七、与客户端证书测试资产的定位差异

不要把 multi-value-rdntests/assets/client-certificates/ 混淆。后者(如 tests/library/client-certificates.spec.ts 所使用)是双向 TLS(mTLS)场景:服务器要求浏览器出示客户端证书,测试通过 clientCertificates: [{ origin, certPath, keyPath }] 配置让 Playwright 在握手时提交自己的身份凭证;而 multi-value-rdn 这张资产始终扮演服务器证书的角色,其特殊性仅在于 DN 中重复的 CN 属性对上层 subjectName 提取逻辑构成压力。二者共同覆盖了 Playwright TLS 相关测试中"服务端证书身份解析"与"客户端证书提交"这两个正交维度。

八、总结与相关文件清单

multi-value-rdn 是一张"小而刁钻"的测试资产:一张含双 CN 属性的自签名服务器证书 + 私钥,配一份 OpenSSL 生成命令,用于回归验证 Playwright 在 securityDetails() 中对 multi-valued RDN 的归一化策略。全文要点如下:

  • 生成方法openssl req -x509 ... -subj "/CN=localhost/CN=secondary-name",双 CN 是刻意为之;
  • 技术痛点:Node getPeerCertificate() 对多值 RDN 返回数组型 CN,直接透传会破坏 subjectName: string 的契约;
  • 官方解法:服务端在 server/fetch.ts 统一取第一个 CN,并缓存结果以兼容 TLS 会话复用;
  • 验证入口global-fetch.spec.ts 断言 subjectNameissuer 均为 localhost

若需继续深入,可依次查看:资产目录 tests/assets/multi-value-rdn/、测试用例 tests/library/global-fetch.spec.ts、服务端实现 packages/playwright-core/src/server/fetch.ts,以及字段契约定义 packages/playwright-core/src/server/network.ts

登录后查看全文
热门项目推荐
相关项目推荐