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