Files
dsh_shenxian/test/f15-replay.test.mjs
T
admin 5ec99ce227
build / build-and-scan (push) Canceled after 0s
chore: 文档库全量重新入库(2026-10-20)
2026-10-20 15:05:55 +08:00

118 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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')
})