feat(overlay): 内容块级寻址 + 实例逐步拉起 + 骨干选路 + 组密钥加密(序24–㉛ 累积同步)
代码
- 内容分发块级寻址:新增 src/net/relay/content/{chunker,store,runtime,source,peer,crypto}.ts
- 组密钥(C 档)确定性加密:AES-256-GCM,块 id β′ = sha256(密文) 前 32 hex;双 epoch 过渡窗口
- 实例生命周期:三处 teardown() 不再杀实例(local/remote/leased-spawner);启动认领 + TCP 探活判孤儿
- 骨干选路:jitter 选路 + endpoint-target;relay client/server/wire/identity/directory/rendezvous/switcher 调整
- 工作台 src/web/server.ts、src/worker/relay-tunnel.ts 装配与候选链观测
脚本与测试
- scripts/overlay-{probe,keyring,jitter}.cjs 更新
- 探针新增 OBS-21(每连接候选数)/ OBS-22(teardown 静态守卫 + 认领面)/ OBS-23(组密钥加密)
- 新增 test/{orchestrator-teardown,orchestrator-rehydrate,overlay-content,overlay-jitter}.test.mjs;relay 两例更新
文档
- 新增交接单:覆盖网络-序24-内容分发块级寻址 / 序25-实例逐步拉起 / 序26-骨干稳定选路与加密
- INDEX.md、交接单/README.md、skills/dsh-auto-handoff-chain/SKILL.md 同步
验收(零回归,2026-09-18 08:0x 复核)
- npm test 201 tests / 200 pass / 0 fail / 1 skipped
- overlay-failover-drill --scene all --table 12 PASS / 0 SKIP / 0 FAIL
- overlay-probe --table 23 PASS / 0 SKIP / 0 FAIL (rc=0)
This commit is contained in:
1 parent
04776af4b1
commit
09ce76f3af
38 files changed
+9133
-211
No files matched your search
@@ -16,6 +16,9 @@
|
||||
* | `sign-revocations` | 在线签名者 | 撤单台(⛔ 撤**签名者**是"根重签一份 SignerSet"的事) |
|
||||
* | `verify-grant` | 任何地方 | 自检:凭据是不是受信签名者签的 |
|
||||
* | `recover-root` | 离线 | **恢复演练**:从纸质恢复码重建根私钥,再签一份 SignerSet 验通 |
|
||||
* | 🆕 `init-group-key` | **每个内容组一次**(结果 scp 到该组每台机器) | 生成**组密钥**(对称,32 B)落 `0600`;⛔ 密钥本体**永不进 stdout / 日志** |
|
||||
* | 🆕 `sign-group-key` | 在线签名者(47) | 签 `(network, group, epoch, keyId)` **三元组**(⛔ 载荷内**不含密钥本体**) |
|
||||
* | 🆕 `verify-group-key` | 任何地方 | 自检:三元组是不是受信签名者签的(篡改 `epoch` 必须失败) |
|
||||
*
|
||||
* ## 用法
|
||||
* ```bash
|
||||
@@ -30,10 +33,16 @@
|
||||
* # 演练三判据:--expect-pub 比公钥;
|
||||
* # 再给 --signers <hex,…> --network <n> --issued-at <iso> ⇒ 用重建的根签 SignerSet 验通(判据②);
|
||||
* # 再给 --expect-sig <b64>(原根对同一 doc 签出的)⇒ 逐字节比对(判据③)
|
||||
* # 🆕 序㉘ · 单 B(组密钥加密):
|
||||
* node scripts/overlay-keyring.cjs init-group-key --group relay --epoch 1 --out /etc/dshs/content-group-key.json [--network ops]
|
||||
* node scripts/overlay-keyring.cjs sign-group-key --signer-key <pem> --network ops --group relay --epoch 1 \
|
||||
* --key /etc/dshs/content-group-key.json --out <json>
|
||||
* node scripts/overlay-keyring.cjs verify-group-key --file <json> --signer-pub <hex> [--group relay --epoch 1]
|
||||
* ```
|
||||
*
|
||||
* 🔴 **所有落盘的私钥一律 `0600`**(`writeSecret`),且**stdout 只打印公钥 / 指纹** ——
|
||||
* 🔴 **所有落盘的私钥 / 密钥一律 `0600`**(`writeSecret`),且**stdout 只打印公钥 / 指纹** ——
|
||||
* 私钥进 stdout 就会进终端历史、`journalctl`、CI 日志。
|
||||
* ⚠️ 组密钥是**对称密钥** ⇒ 同一条铁律:stdout 只打印 `keyId`(`sha256(key)` 前 16 hex),⛔ 不打印 key。
|
||||
*
|
||||
* @module scripts/overlay-keyring
|
||||
*/
|
||||
@@ -44,6 +53,8 @@ const { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } = requir
|
||||
const { dirname } = require('node:path')
|
||||
|
||||
const id = require('../lib/net/relay/identity.js')
|
||||
// 🆕 序㉘ · 单 B:组密钥(对称)—— 生成 / 签名三元组 / 验签。⚠️ 只搬运算,判据全在模块里。
|
||||
const cfgx = require('../lib/net/relay/content/crypto.js')
|
||||
|
||||
const [, , cmd, ...rest] = process.argv
|
||||
|
||||
@@ -212,6 +223,90 @@ const COMMANDS = {
|
||||
process.stdout.write(`✓ 验签通过:${verdict.doc.network}/${verdict.doc.hostId} 指纹=${id.nodeKeyFingerprint(verdict.doc.nodeKey)}\n`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 🆕 序㉘ · 单 B:生成**组密钥**(对称,32 字节 = `crypto.KEY_LEN`)—— **每个内容组一次**。
|
||||
*
|
||||
* 三条纪律:
|
||||
* ① 落盘走 `writeSecret` ⇒ **`0600`**(密钥本体只准在文件里,⛔ **不经网络、不经 relay**);
|
||||
* ② stdout **只打印 `keyId`**(`sha256(key)` 前 16 hex)⇒ 可安全贴进回报 / 日志;
|
||||
* ③ 同一把密钥要铺到该组的**每台**机器(`scp` + `0600` + 属主正确)。
|
||||
*/
|
||||
'init-group-key': () => {
|
||||
const group = need('group')
|
||||
const epoch = Number(need('epoch'))
|
||||
if (!Number.isInteger(epoch) || epoch <= 0) throw new Error('--epoch 必须是正整数')
|
||||
const key = require('node:crypto').randomBytes(cfgx.KEY_LEN)
|
||||
const doc = {
|
||||
version: cfgx.CONTENT_CIPHER_VERSION,
|
||||
group,
|
||||
...(args.network === undefined ? {} : { network: String(args.network) }),
|
||||
epoch,
|
||||
key: key.toString('base64'),
|
||||
previous: [],
|
||||
}
|
||||
writeSecret(need('out'), `${JSON.stringify(doc, null, 2)}\n`, '组密钥文件')
|
||||
process.stdout.write(
|
||||
`✓ group=${group} epoch=${epoch} keyId=${cfgx.keyIdOf(key)}(⚠️ 密钥本体只在文件里,⛔ stdout 不打印)\n`,
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* 🆕 序㉘ · 单 B:签名者签一份**组密钥凭据** = `(network, group, epoch, keyId)` **三元组**。
|
||||
*
|
||||
* 🔴 **载荷内不含密钥本体** —— 它只回答"当前这一代是哪一把(`keyId`)",
|
||||
* 供节点做**版本错配检测**;密钥本体仍只走 `0600` 文件通道。
|
||||
*/
|
||||
'sign-group-key': () => {
|
||||
const spec = need('key').trim()
|
||||
let keyB64 = spec
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(spec, 'utf8'))
|
||||
if (raw !== null && typeof raw === 'object' && typeof raw.key === 'string') keyB64 = raw.key
|
||||
} catch {
|
||||
/* 不是文件 ⇒ 当 base64 用(也允许直接给 base64,便于夹具) */
|
||||
}
|
||||
const keyBuf = Buffer.from(keyB64, 'base64')
|
||||
if (keyBuf.length !== cfgx.KEY_LEN) {
|
||||
throw new Error(`--key 必须给出 ${cfgx.KEY_LEN} 字节密钥(含 key 字段的 JSON 文件,或裸 base64)`)
|
||||
}
|
||||
const epoch = Number(need('epoch'))
|
||||
if (!Number.isInteger(epoch) || epoch <= 0) throw new Error('--epoch 必须是正整数')
|
||||
const doc = {
|
||||
version: cfgx.CONTENT_CIPHER_VERSION,
|
||||
network: need('network'),
|
||||
group: need('group'),
|
||||
epoch,
|
||||
keyId: cfgx.keyIdOf(keyBuf),
|
||||
issuedAt: nowIso(),
|
||||
}
|
||||
const sig = id.signPayloadWith(readFileSync(need('signer-key'), 'utf8'), cfgx.groupKeyCredentialPayload(doc))
|
||||
writePublic(need('out'), { doc, sig })
|
||||
process.stdout.write(`✓ 已签发组密钥凭据:${doc.network}/${doc.group} epoch=${doc.epoch} keyId=${doc.keyId}\n`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 🆕 序㉘ · 单 B:自检组密钥凭据(**篡改 `epoch` 必须失败** —— 失败关闭)。
|
||||
*
|
||||
* 可选交叉核对:`--group` / `--epoch` / `--key`(给了就与凭据里的值**逐字比对**,
|
||||
* 防"签的是 A、装的是 B"这种静默错配)。
|
||||
*/
|
||||
'verify-group-key': () => {
|
||||
const raw = JSON.parse(readFileSync(need('file'), 'utf8'))
|
||||
const verdict = cfgx.verifyGroupKeyCredential(raw.doc, raw.sig, need('signer-pub').split(',').map((s) => s.trim()))
|
||||
if (!verdict.ok) {
|
||||
process.stderr.write(`✗ 验签失败:${verdict.reason}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
const d = verdict.doc
|
||||
if (typeof args.group === 'string' && args.group.trim() !== d.group) {
|
||||
throw new Error(`⛔ 组名不配:凭据 group=${d.group} ≠ 传入 ${args.group.trim()}`)
|
||||
}
|
||||
if (args.epoch !== undefined && Number(args.epoch) !== d.epoch) {
|
||||
throw new Error(`⛔ epoch 不配:凭据 epoch=${d.epoch} ≠ 传入 ${String(args.epoch)}`)
|
||||
}
|
||||
process.stdout.write(`✓ 验签通过:network=${d.network} group=${d.group} epoch=${d.epoch} keyId=${d.keyId}\n`)
|
||||
},
|
||||
|
||||
/**
|
||||
* **根密钥恢复演练**:从纸质恢复码重建根私钥(⛔ 不用原文件),再签一份 SignerSet 并验通。
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user