118 lines
5.6 KiB
JavaScript
118 lines
5.6 KiB
JavaScript
// 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')
|
||
})
|