/** * 覆盖网络 · **序③ 一机一钥 + 信任根** 单测 —— 身份层是不是"真的挡得住"。 * * ## 这个文件要回答的四个问题(= `交接单_一机一钥与信任根_20260917.md §5/§6`) * 1. **四层模型**:根(离线)→ 签名者(在线)→ 节点(每机一把)→ 会话(复用 TLS) * 的载体能不能**真签发、真验签**?(A 组) * 2. **入网 = 签名**:篡改过的入网凭据**建不起会话**,且原因**结构化**?(B 组 + D 组) * 3. **relay 准入按 `hostId` + 成员资格**:拿 A 网的凭据进 B 网必须被拒?(C 组 + D1) * 4. **撤销单台 ≠ 全网换密钥**:撤 w106 之后,w47 照旧在线?(D6) * * ## 🔴 "先红后绿"在本文件里的落点(⛔ 不是口头声明) * 身份校验**默认不强制**(`requireIdentity=false`,存量过渡)⇒ 同一份**被搬家的凭据**, * 在 `verifyIdentity:false` 的服务器上**能注册进去**(= 没有这道门时的攻击面,**红**), * 在 `verifyIdentity:true` 的服务器上**被拒**(**绿**)。两组用**同一份坏凭据、同一个 hostId**, * 唯一变量就是那一道门 ⇒ 证明"挡住它的正是这道门",而不是别的巧合(D5)。 * * 运行:`node --test test/overlay-identity.test.mjs`(Node ≥ 22;测的是 `lib/` 产物,先 `npm run build`)。 * * @module test/overlay-identity */ import assert from 'node:assert/strict' import { createHmac, randomBytes } from 'node:crypto' import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { test } from 'node:test' import { RelayClient } from '../lib/net/relay/client.js' import { generateAuthorityKey, generateNodeKey, nodeKeyFingerprint, publicKeyOfPrivate, signNodeGrant, signProof, signRevocations, signSignerSet, verifyNodeGrant, verifyPeerGrant, verifyProof, verifyRevocations, verifySignerSet, } from '../lib/net/relay/identity.js' import { describeKeyEntry, loadKeysFile, parseKeysInline } from '../lib/net/relay/keys.js' import { OPS_NETWORK } from '../lib/net/relay/network.js' import { RelayServer } from '../lib/net/relay/server.js' import { MUX, decodeMux, encodeJsonFrame } from '../lib/net/relay/wire.js' const BASE = 47100 const SPAN = 200 const PATH = '/dshs-relay' const U_A = 'u:alpha' const U_B = 'u:beta' const NOW = Date.parse('2026-09-17T00:00:00Z') /* ─────────────────────────── 工具 ─────────────────────────── */ async function waitFor(fn, ms = 4000) { const until = Date.now() + ms while (Date.now() < until) { if (fn()) return true await new Promise((r) => setTimeout(r, 20)) } return false } /** 一套"根 → 签名者 → 节点"的密钥与凭据(**建网的标准动作**:测试与部署脚本同一形状)。 */ function buildAuthority(network = OPS_NETWORK) { const root = generateAuthorityKey() const signer = generateAuthorityKey() const signerSet = { version: 1, network, issuedAt: new Date(NOW).toISOString(), signers: [signer.publicKey], } const signerSetSig = signSignerSet(signerSet, root.privateKeyPem) const issue = (hostId, opts = {}) => { const node = generateNodeKey() const grant = { version: 1, network: opts.network ?? network, hostId, nodeKey: node.publicKey, issuedAt: opts.issuedAt ?? new Date(NOW).toISOString(), expiresAt: opts.expiresAt ?? '', } return { node, grant, sig: signNodeGrant(grant, signer.privateKeyPem) } } return { root, signer, signerSet, signerSetSig, issue } } /** 吊销清单(签名者签发)。 */ function revocationOf(signerPem, hosts, nodeKeys = []) { const doc = { version: 1, network: OPS_NETWORK, issuedAt: new Date(NOW).toISOString(), hosts, nodeKeys } return { doc, sig: signRevocations(doc, signerPem) } } /** * 手工发一个 `HELLO` 并取回**首个回帧**(用于构造"正常 client 不会发"的帧)。 * * ⚠️ MAC 输入串与 `client.ts` 逐字同源(**不含身份字段**)—— 身份是**追加**字段、不动 MAC; * 这一条本身就是被测对象之一(旧客户端算出的 MAC 必须仍被接受)。 */ async function rawHello(wsUrl, opts) { const { hostId, secret, portsCsv = '', network, identity } = opts const ws = new WebSocket(wsUrl) ws.binaryType = 'arraybuffer' await new Promise((resolve, reject) => { ws.addEventListener('open', resolve, { once: true }) ws.addEventListener('error', () => reject(new Error('ws open failed')), { once: true }) }) const ts = Date.now() const nonce = randomBytes(16).toString('hex') const challenge = `${hostId}|${ts}|${nonce}|${portsCsv}` const mac = createHmac('sha256', Buffer.from(secret, 'hex')).update(challenge).digest('hex') const payload = { v: 1, hostId, ts, nonce, portsCsv, mac } if (network !== undefined) payload.network = network if (identity !== undefined) { const { nodePrivateKeyPem, nodeSigOverride, ...rest } = identity Object.assign(payload, rest) // 默认按**正确的挑战串**签(走真实路径);给了 override 就按 override(用来构造"签名错")。 if (nodeSigOverride !== undefined) payload.nodeSig = nodeSigOverride else if (nodePrivateKeyPem !== undefined) payload.nodeSig = signProof(nodePrivateKeyPem, challenge) } ws.send(encodeJsonFrame(MUX.HELLO, 0, payload)) const frame = await new Promise((resolve) => { const timer = setTimeout(() => resolve(undefined), 3000) ws.addEventListener('message', (ev) => { clearTimeout(timer) resolve(decodeMux(Buffer.from(ev.data))) }) ws.addEventListener('close', () => { clearTimeout(timer) resolve(undefined) }) }) return { ws, frame } } function reasonOf(frame) { assert.ok(frame !== undefined, '必须得到结构化回帧') return JSON.parse(frame.payload.toString('utf8')).reason } /** 建一个 relay:所有 host 都是"拨号方"(端口表为空)⇒ 不需要任何 TCP 回显服务。 */ async function startRelay(t, opts = {}) { const hosts = opts.hosts ?? {} const srv = new RelayServer({ port: 0, // ⚠️ 必须是 `Map`(服务端按 `keys.get(hostId)` 取;传普通对象会得到 // `this.opts.keys.get is not a function` 这种**与判据无关**的崩 —— 会淹掉真正的结论)。 keys: new Map(Object.entries(hosts)), instancePortBase: BASE, instancePortSpan: SPAN, dialers: new Map([[OPS_NETWORK, new Set(Object.keys(hosts))]]), trustedSignerKeys: opts.trustedSignerKeys ?? [], revocations: opts.revocations, requireIdentity: opts.requireIdentity ?? false, verifyIdentity: opts.verifyIdentity ?? true, log: opts.log ?? (() => {}), }) await srv.start() t.after(async () => { await srv.stop() }) return { srv, url: `ws://127.0.0.1:${srv.boundPort}${PATH}` } } /* ─────────── A:四层密钥模型(根 → 签名者) ─────────── */ test('A1 根签名的签名者集合:受信根验得过', () => { const { root, signer, signerSet, signerSetSig } = buildAuthority() const verdict = verifySignerSet(signerSet, signerSetSig, [root.publicKey]) assert.equal(verdict.ok, true, `根签的集合必须验得过,实测:${JSON.stringify(verdict)}`) assert.deepEqual(verdict.doc.signers, [signer.publicKey]) }) test('A2 篡改签名者集合(加一把签名者后沿用旧签名)⇒ **signature-mismatch**', () => { const { root, signerSet, signerSetSig } = buildAuthority() const evil = generateAuthorityKey() const tampered = { ...signerSet, signers: [...signerSet.signers, evil.publicKey] } const verdict = verifySignerSet(tampered, signerSetSig, [root.publicKey]) assert.equal(verdict.ok, false) assert.equal(verdict.reason, 'signature-mismatch', '加签名者 = 加攻击面 ⇒ 必须失败关闭') }) test('A3 **不可验 = 不接受**:一把受信根都没有 ⇒ no-trusted-keys(不"先用着")', () => { const { signerSet, signerSetSig } = buildAuthority() const verdict = verifySignerSet(signerSet, signerSetSig, []) assert.equal(verdict.ok, false) assert.equal(verdict.reason, 'no-trusted-keys') }) test('A4 换一把没被授权的根来签 ⇒ 不认(根是唯一信任源)', () => { const a = buildAuthority() const b = buildAuthority() const crossSig = signSignerSet(a.signerSet, b.root.privateKeyPem) const verdict = verifySignerSet(a.signerSet, crossSig, [a.root.publicKey]) assert.equal(verdict.ok, false) assert.equal(verdict.reason, 'signature-mismatch') }) /* ─────────── B:节点凭据 + 本地校验(D2 的判据落点) ─────────── */ test('B1 合法凭据:受信签名者签发 ⇒ 过,且能取出 hostId / 网 / 节点公钥', () => { const auth = buildAuthority() const { grant, sig } = auth.issue('w106') const verdict = verifyNodeGrant(grant, sig, [auth.signer.publicKey]) assert.equal(verdict.ok, true) assert.equal(verdict.doc.hostId, 'w106') assert.equal(verdict.doc.network, OPS_NETWORK) assert.equal(verdict.doc.nodeKey, grant.nodeKey) }) test('B2 🔴 核心:**篡改 hostId 后沿用原签名** ⇒ signature-mismatch(凭据不可搬家)', () => { const auth = buildAuthority() const { grant, sig } = auth.issue('w106') const moved = { ...grant, hostId: 'w47' } const verdict = verifyNodeGrant(moved, sig, [auth.signer.publicKey]) assert.equal(verdict.ok, false) assert.equal(verdict.reason, 'signature-mismatch', '凭据被搬到别的 hostId 上必须拒(签名覆盖 hostId)') }) test('B3 期望网不符 ⇒ network-mismatch(跨网签发进不来)', () => { const auth = buildAuthority() const { grant, sig } = auth.issue('w106') const verdict = verifyPeerGrant(grant, sig, { trustedSignerKeys: [auth.signer.publicKey], network: U_B }) assert.equal(verdict.ok, false) assert.equal(verdict.reason, 'network-mismatch') }) test('B4/B5 期望 hostId 不符 ⇒ host-mismatch;期望公钥不符 ⇒ key-mismatch', () => { const auth = buildAuthority() const { grant, sig } = auth.issue('w106') const other = generateNodeKey() assert.equal( verifyPeerGrant(grant, sig, { trustedSignerKeys: [auth.signer.publicKey], hostId: 'w47' }).reason, 'host-mismatch', ) assert.equal( verifyPeerGrant(grant, sig, { trustedSignerKeys: [auth.signer.publicKey], nodeKey: other.publicKey }).reason, 'key-mismatch', '私钥被换过(公钥对不上)⇒ 拒', ) // 对得上的那一组必须**过** —— 否则上面两条可能只是"恒拒",测了个寂寞 assert.equal( verifyPeerGrant(grant, sig, { trustedSignerKeys: [auth.signer.publicKey], network: OPS_NETWORK, hostId: 'w106', nodeKey: grant.nodeKey, }).ok, true, ) }) test('B6 过期凭据 ⇒ expired(带过期时间的节点会真的过期)', () => { const auth = buildAuthority() const { grant, sig } = auth.issue('w106', { issuedAt: new Date(NOW - 120_000).toISOString(), expiresAt: new Date(NOW - 60_000).toISOString(), }) const verdict = verifyPeerGrant(grant, sig, { trustedSignerKeys: [auth.signer.publicKey], nowMs: NOW }) assert.equal(verdict.ok, false) assert.equal(verdict.reason, 'expired') }) test('B7/B8 吊销:撤 hostId ⇒ revoked-host;撤节点公钥 ⇒ revoked-node-key;撤单台不牵连别的', () => { const auth = buildAuthority() const { grant, sig } = auth.issue('w106') const byHost = { ...revocationOf(auth.signer.privateKeyPem, ['w106']).doc } const byKey = { ...revocationOf(auth.signer.privateKeyPem, [], [grant.nodeKey]).doc } const other = { ...revocationOf(auth.signer.privateKeyPem, ['manager']).doc } assert.equal( verifyPeerGrant(grant, sig, { trustedSignerKeys: [auth.signer.publicKey], revocations: byHost }).reason, 'revoked-host', ) assert.equal( verifyPeerGrant(grant, sig, { trustedSignerKeys: [auth.signer.publicKey], revocations: byKey }).reason, 'revoked-node-key', '设备被盗场景:hostId 可能被复用,公钥不会 ⇒ 两条都要能撤', ) assert.equal( verifyPeerGrant(grant, sig, { trustedSignerKeys: [auth.signer.publicKey], revocations: other }).ok, true, '撤销单台不得牵动其它节点', ) }) test('B9 吊销清单本身也要**验签**(清单被改坏 ⇒ 撤销失效 ⇒ 不能当没事)', () => { const auth = buildAuthority() const rogue = generateAuthorityKey() const good = revocationOf(auth.signer.privateKeyPem, ['w106']) assert.equal(verifyRevocations(good.doc, good.sig, [auth.signer.publicKey]).ok, true) const forged = revocationOf(rogue.privateKeyPem, ['w106']) assert.equal(verifyRevocations(forged.doc, forged.sig, [auth.signer.publicKey]).reason, 'signature-mismatch') // 篡改内容后沿用原签名 const tampered = { ...good.doc, hosts: ['w47'] } assert.equal(verifyRevocations(tampered, good.sig, [auth.signer.publicKey]).reason, 'signature-mismatch') }) test('B10 会话证明(proof):握有私钥才签得出;换公钥 / 改挑战 ⇒ 验不过', () => { const auth = buildAuthority() const { node } = auth.issue('w106') const challenge = 'w106|123|abc|' const sig = signProof(node.privateKeyPem, challenge) assert.equal(verifyProof(node.publicKey, challenge, sig), true) const other = generateNodeKey() assert.equal(verifyProof(other.publicKey, challenge, sig), false, '别的公钥验不过 ⇒ 证明绑定了具体私钥') assert.equal(verifyProof(node.publicKey, `${challenge}x`, sig), false, '挑战被改 ⇒ 验不过(防重放 / 防挪用)') }) test('B11 非受信签名者签发的凭据 ⇒ signature-mismatch(名单之外一律不认)', () => { const auth = buildAuthority() const rogue = generateAuthorityKey() const node = generateNodeKey() const grant = { version: 1, network: OPS_NETWORK, hostId: 'w106', nodeKey: node.publicKey, issuedAt: new Date(NOW).toISOString(), expiresAt: '', } const sig = signNodeGrant(grant, rogue.privateKeyPem) const verdict = verifyNodeGrant(grant, sig, [auth.signer.publicKey]) assert.equal(verdict.ok, false) assert.equal(verdict.reason, 'signature-mismatch') }) test('B12 指纹:每机一把 ⇒ 指纹互不相同;解析不出来就 undefined(不猜)', () => { const a = generateNodeKey() const b = generateNodeKey() assert.notEqual(nodeKeyFingerprint(a.publicKey), nodeKeyFingerprint(b.publicKey), '每机一把 ⇒ 指纹必须不同') assert.equal(nodeKeyFingerprint(a.publicKey), nodeKeyFingerprint(a.publicKey)) assert.equal(nodeKeyFingerprint('not-a-key'), undefined) }) /* ─────────── C:keys 表带"网"(成员资格的唯一判据) ─────────── */ test('C1 旧写法(裸 hostId + hex 串)⇒ 归入运维网 ops(现网配置一字不改)', () => { const s = randomBytes(32).toString('hex') const map = parseKeysInline(`w-1:${s}`) // 键 = **逻辑名**(序③)⇒ 裸 `w-1` 归一成 `ops/w-1` assert.deepEqual(map.get('ops/w-1'), { network: OPS_NETWORK, secret: s }) assert.equal(describeKeyEntry('w-1', map.get('ops/w-1')), 'w-1', 'ops 下省略网络前缀(日志读法与 R5 一致)') }) test('C2 新写法:`网/hostId:secret` 与 `网:hostId:secret` 都切得出网络(切点是**最后一个冒号**)', () => { const s = randomBytes(32).toString('hex') const slash = parseKeysInline(`${U_A}/pc-1:${s}`) assert.deepEqual(slash.get(U_A + '/pc-1'), { network: U_A, secret: s }) // ⚠️ 网络 id 自己含 `:`(`u:5`)⇒ 用**第一个**冒号切会切出网络段 `u`, // 于是"配置写错"就长得像"这张网不存在"(静默拒绝)——这正是本线反复要根治的病。 const colon = parseKeysInline(`u:5:pc-1:${s}`) assert.deepEqual(colon.get('u:5/pc-1'), { network: 'u:5', secret: s }) assert.equal(describeKeyEntry('pc-1', colon.get('u:5/pc-1')), 'u:5/pc-1') }) test('C3 网络段非法 / 密钥长度不对 ⇒ **抛**(不静默回落:那会让"配错"伪装成"网络不通")', () => { const s = randomBytes(32).toString('hex') assert.throws(() => parseKeysInline(`u/pc-1:${s}`), /非法/, '裸 `u` 是保留字,不是合法网名') assert.throws(() => parseKeysInline(`pc-1:deadbeef`), /64 hex/) assert.throws(() => parseKeysInline('no-colon'), /malformed/) }) /* ─────────── D:端到端(relay 准入 + 身份) ─────────── */ test('D1 成员资格:声称别的网 ⇒ **network-mismatch**(失败关闭,不回落 ops)', async (t) => { const s = randomBytes(32).toString('hex') const { url } = await startRelay(t, { hosts: { w106: s } }) const { ws, frame } = await rawHello(url, { hostId: 'w106', secret: s, network: U_B }) t.after(() => ws.close()) assert.equal(frame.type, MUX.HELLO_ERR) assert.equal(reasonOf(frame), 'network-mismatch', '密钥表说它在 ops,声称别张网 ⇒ 必须拒') }) test('D2 未登记的 hostId ⇒ unknown-host(默认拒绝)', async (t) => { const s = randomBytes(32).toString('hex') const { url } = await startRelay(t, { hosts: { w106: s } }) const { ws, frame } = await rawHello(url, { hostId: 'nobody', secret: s }) t.after(() => ws.close()) assert.equal(reasonOf(frame), 'unknown-host') }) test('D3 强制身份但客户端**不带**凭据 ⇒ identity-incomplete(明确拒,不放行)', async (t) => { const s = randomBytes(32).toString('hex') const auth = buildAuthority() const { url } = await startRelay(t, { hosts: { w106: s }, trustedSignerKeys: [auth.signer.publicKey], requireIdentity: true, }) const { ws, frame } = await rawHello(url, { hostId: 'w106', secret: s, network: OPS_NETWORK }) t.after(() => ws.close()) assert.equal(reasonOf(frame), 'identity-incomplete') }) test('D4 relay 侧**没有**受信签名者时,带了凭据也拒(不可验 = 不接受)', async (t) => { const s = randomBytes(32).toString('hex') const auth = buildAuthority() const { grant, sig, node } = auth.issue('w106') const { url } = await startRelay(t, { hosts: { w106: s }, trustedSignerKeys: [] }) const { ws, frame } = await rawHello(url, { hostId: 'w106', secret: s, network: OPS_NETWORK, identity: { nodeKey: node.publicKey, grant, grantSig: sig, nodePrivateKeyPem: node.privateKeyPem }, }) t.after(() => ws.close()) assert.equal(reasonOf(frame), 'identity-no-trusted-signers') }) test('D5 🔴 **先红后绿**:同一份"被搬家的凭据" —— 没有这道门时能注册(红),有门时被拒(绿)', async (t) => { const s = randomBytes(32).toString('hex') const auth = buildAuthority() const good = auth.issue('w106') // 篡改形态:把 w106 的凭据"搬家"到 w47 并沿用原签名 —— 这正是"控制面被攻破后插入节点"的形状。 const evilGrant = { ...good.grant, hostId: 'w47' } const identity = { nodeKey: good.node.publicKey, grant: evilGrant, grantSig: good.sig } const hosts = { w106: s, w47: s } /* ① 红:门关着(verifyIdentity:false)⇒ **同一份坏凭据确实注册进去了** */ const open = await startRelay(t, { hosts, trustedSignerKeys: [auth.signer.publicKey], verifyIdentity: false }) const red = await rawHello(open.url, { hostId: 'w47', secret: s, network: OPS_NETWORK, identity }) t.after(() => red.ws.close()) assert.equal(red.frame.type, MUX.HELLO_ACK, '【红】门关着时坏凭据能注册 —— 这就是被治的攻击面,必须有实测') assert.ok(open.srv.status().counters.authed > 0) /* ② 绿:门开着(verifyIdentity:true)⇒ 同一份坏凭据被拒 */ const closed = await startRelay(t, { hosts, trustedSignerKeys: [auth.signer.publicKey] }) const green = await rawHello(closed.url, { hostId: 'w47', secret: s, network: OPS_NETWORK, identity }) t.after(() => green.ws.close()) assert.equal(reasonOf(green.frame), 'identity-signature-mismatch', '搬家的凭据必须被签名校验挡住') assert.equal(closed.srv.status().counters.identityOk, 0) assert.equal(closed.srv.status().sessions.length, 0, '被拒的节点不得留下任何会话') }) test('D6 凭据合法但**节点证明**是错的 ⇒ identity-bad-proof(有证书 ≠ 有私钥)', async (t) => { const s = randomBytes(32).toString('hex') const auth = buildAuthority() const { grant, sig, node } = auth.issue('w106') const attacker = generateNodeKey() const { url } = await startRelay(t, { hosts: { w106: s }, trustedSignerKeys: [auth.signer.publicKey] }) const { ws, frame } = await rawHello(url, { hostId: 'w106', secret: s, network: OPS_NETWORK, identity: { nodeKey: node.publicKey, // 声明的是**真**公钥(凭据也对得上) grant, grantSig: sig, nodeSigOverride: signProof(attacker.privateKeyPem, 'anything'), // 但签名不是那把私钥签的 }, }) t.after(() => ws.close()) assert.equal(reasonOf(frame), 'identity-bad-proof', '凭据是公开可转发的 ⇒ 必须再证"握有私钥"') }) test('D7 合法身份 ⇒ 真 `RelayClient` 连得上;**撤销单台不牵动全网**', async (t) => { const sA = randomBytes(32).toString('hex') const sB = randomBytes(32).toString('hex') const auth = buildAuthority() const a = auth.issue('w106') const b = auth.issue('w47') const rev = revocationOf(auth.signer.privateKeyPem, ['w106']) assert.equal(verifyRevocations(rev.doc, rev.sig, [auth.signer.publicKey]).ok, true, '清单本身先要验得过') const { srv, url } = await startRelay(t, { hosts: { w106: sA, w47: sB }, trustedSignerKeys: [auth.signer.publicKey], revocations: rev.doc, requireIdentity: true, }) /** 用**真** `RelayClient` 连(证明客户端侧字段拼得对,不是只有 rawHello 能过)。 */ const connect = (hostId, secret, issued) => { const c = new RelayClient({ url, hostId, secret, ports: [], dialer: true, identity: { privateKeyPem: issued.node.privateKeyPem, grant: issued.grant, grantSig: issued.sig }, log: () => {}, }) c.start() return c } const cA = connect('w106', sA, a) const cB = connect('w47', sB, b) t.after(() => { cA.stop() cB.stop() }) assert.ok(await waitFor(() => cB.status().state === 'up'), '未被撤的 w47 必须连上(撤销不得牵动全网)') assert.equal(await waitFor(() => cA.status().state === 'up', 1500), false, '被撤的 w106 不得连上') assert.equal(srv.status().counters.identityOk, 1, '只有一台通过身份校验') assert.equal( srv.status().sessions.filter((x) => x.hostId === 'w106').length, 0, '被撤节点不得在 relay 侧留下会话', ) assert.equal(publicKeyOfPrivate(a.node.privateKeyPem), a.grant.nodeKey, '客户端带的 nodeKey 必须与凭据里那把一致') }) /* ─────────── E:文件面(密钥落点与原地升级) ─────────── */ test('E1 keys 文件:新旧写法可**共存**(原地升级 ⇒ 可原子替换 + 可回滚)', (t) => { const dir = mkdtempSync(join(tmpdir(), 'dshs-keys-')) t.after(() => rmSync(dir, { recursive: true, force: true })) const s1 = randomBytes(32).toString('hex') const s2 = randomBytes(32).toString('hex') const file = join(dir, 'relay-keys.json') writeFileSync(file, JSON.stringify({ 'w-1': s1, w106: { network: OPS_NETWORK, secret: s2 } })) const map = loadKeysFile(file) assert.deepEqual(map.get('ops/w-1'), { network: OPS_NETWORK, secret: s1 }) assert.deepEqual(map.get('ops/w106'), { network: OPS_NETWORK, secret: s2 }) // 非法网络段 ⇒ **装载即抛**("配置错就炸",不变成运行期的静默拒绝) writeFileSync(file, JSON.stringify({ bad: { network: 'u', secret: s1 } })) assert.throws(() => loadKeysFile(file), /非法/) })