// F1.5 落地后的新路径验收(node:test)—— 三条原先翻红的用例走**带序号判定的新入口**。 // 判据(执行单 §6.1/§6.2):T4 / T5 / T9b 必须从 FAIL 翻成 PASS 且拒因 = stale-serial; // 另附序号守卫真语义。全部直接 import 生产编译产物,⛔ 不复制纯函数。 import { test } from 'node:test' import assert from 'node:assert/strict' import { mkdtempSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { OverlaySerialGuard, generateAuthorityKey, generateNodeKey, signNodeGrant, signSignerSet, verifyPeerGrant, verifySignerSet, } from '../lib/net/relay/identity.js' const NET = 'ops' const SERIAL_KEY = `signerset|${NET}` const REVOKE_KEY = `revocation|${NET}` function freshGuard() { const dir = mkdtempSync(join(tmpdir(), 'f15-guard-')) return new OverlaySerialGuard({ file: join(dir, 'serial.json') }) } function fixtures() { const R1 = generateAuthorityKey() const S1 = generateAuthorityKey() const S2 = generateAuthorityKey() const v1 = { version: 1, network: NET, issuedAt: '2026-09-01T00:00:00.000Z', signers: [S1.publicKey, S2.publicKey], serial: 1 } const v2 = { version: 1, network: NET, issuedAt: '2026-09-26T00:00:00.000Z', signers: [S2.publicKey], serial: 2 } return { R1, S1, S2, v1, v2, sigV1: signSignerSet(v1, R1.privateKeyPem), sigV2: signSignerSet(v2, R1.privateKeyPem) } } // ── 三条原先翻红的用例:走新入口(验签 → 序号守卫)──────────────────────────── test('F1.5/T4 重放旧名单:验签通过但序号守卫拒(≤ 已见水位)⇒ 整体拒', async () => { const { R1, v1, v2, sigV1, sigV2 } = fixtures() const guard = freshGuard() const fresh = verifySignerSet(v2, sigV2, [R1.publicKey]) assert.equal(fresh.ok, true, 'v2 本身必须验签通过') assert.deepEqual(await guard.check(SERIAL_KEY, v2.serial), { ok: true }) const replayed = verifySignerSet(v1, sigV1, [R1.publicKey]) assert.equal(replayed.ok, true, '重放的签名本身是真的 —— 这正是 G1 的病根') const g = await guard.check(SERIAL_KEY, v1.serial) assert.equal(g.ok, false) assert.equal(g.reason, 'stale-serial', '拒因必须是 stale-serial') }) test('F1.5/T5 整链重放:重放被拒 ⇒ 名单停在 v2 ⇒ 已撤签名者所签凭据仍被拒', async () => { const { R1, S1, v1, v2, sigV1, sigV2 } = fixtures() const node = generateNodeKey() const grant = { version: 1, network: NET, hostId: 'w106', nodeKey: node.publicKey, issuedAt: '2026-09-01T00:00:00.000Z', expiresAt: '' } const grantSig = signNodeGrant(grant, S1.privateKeyPem) const guard = freshGuard() assert.deepEqual(await guard.check(SERIAL_KEY, v2.serial), { ok: true }) const replayed = verifySignerSet(v1, sigV1, [R1.publicKey]) const g = await guard.check(SERIAL_KEY, v1.serial) assert.equal(g.ok, false) // 只有重放**被接受**时才可能把 S1 装回来;被拒 ⇒ 采用仍停在 v2 const trusted = replayed.ok && g.ok ? replayed.doc.signers : v2.signers const denied = verifyPeerGrant(grant, grantSig, { trustedSignerKeys: trusted, network: NET, hostId: 'w106' }) assert.equal(denied.ok, false) }) test('F1.5/T9b 重放撤销之前的旧吊销清单 ⇒ 守卫拒 ⇒ 撤销不被抹掉', async () => { const S2 = generateAuthorityKey() const node = generateNodeKey() const grant = { version: 1, network: NET, hostId: 'w106', nodeKey: node.publicKey, issuedAt: '2026-09-01T00:00:00.000Z', expiresAt: '' } const grantSig = signNodeGrant(grant, S2.privateKeyPem) const rev0 = { version: 1, network: NET, issuedAt: '2026-09-01T00:00:00.000Z', hosts: [], nodeKeys: [], serial: 1 } const rev1 = { version: 1, network: NET, issuedAt: '2026-09-26T00:00:00.000Z', hosts: ['w106'], nodeKeys: [], serial: 2 } const guard = freshGuard() assert.deepEqual(await guard.check(REVOKE_KEY, rev1.serial), { ok: true }) const replayed = await guard.check(REVOKE_KEY, rev0.serial) assert.equal(replayed.ok, false) assert.equal(replayed.reason, 'stale-serial') // 撤销没被抹掉:仍按 rev1 判 ⇒ 该节点被拒 const denied = verifyPeerGrant(grant, grantSig, { trustedSignerKeys: [S2.publicKey], network: NET, hostId: 'w106', revocations: rev1, }) assert.equal(denied.ok, false) assert.equal(denied.reason, 'revoked-host') }) // ── 序号守卫真语义(执行单 §6.2 的负控 ①② 与 T-10)────────────────────────── test('F1.5/守卫语义:更高序号接受;更小与相等一律 stale;无序号老文档在水位≥1 时 stale;非法序号 bad-serial', async () => { const g = freshGuard() assert.deepEqual(await g.check(SERIAL_KEY, 13), { ok: true }) assert.equal((await g.check(SERIAL_KEY, 11)).reason, 'stale-serial') assert.equal((await g.check(SERIAL_KEY, 13)).reason, 'stale-serial', '等于也拒') assert.deepEqual(await g.check(SERIAL_KEY, 14), { ok: true }) assert.equal((await g.check(SERIAL_KEY, 0)).reason, 'stale-serial', '无序号老文档在水位≥1 时被拒') assert.equal((await g.check(SERIAL_KEY, -1)).reason, 'bad-serial') }) test('F1.5/守卫语义:状态文件损坏 ⇒ 失败关闭(serial-state-unreadable)', async () => { const dir = mkdtempSync(join(tmpdir(), 'f15-guard-')) const file = join(dir, 'serial.json') writeFileSync(file, '{ not json') const g = new OverlaySerialGuard({ file }) const r = await g.check(SERIAL_KEY, 99) assert.equal(r.ok, false) assert.equal(r.reason, 'serial-state-unreadable') })