feat: add node-signed system body write admission
This commit is contained in:
parent
e415d2ae32
commit
95e47e8e8f
14 changed files with 352 additions and 9 deletions
|
|
@ -0,0 +1,86 @@
|
|||
"use strict";
|
||||
|
||||
const crypto = require("node:crypto");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const DEFAULT_PERSONAS = path.resolve(__dirname, "../../identity/fifth-domain-subject-registry.json");
|
||||
const DEFAULT_NODES = path.resolve(__dirname, "../../routing/bingshuo-system-write-node-registry.json");
|
||||
const SIGNED_FIELDS = [
|
||||
"schema", "node_id", "key_id", "issued_at_unix", "nonce", "persona_id",
|
||||
"authorization_receipt_id", "language_event_id", "target", "action", "sealed_payload_sha256",
|
||||
];
|
||||
|
||||
function reject(errorCode) {
|
||||
return {
|
||||
schema: "guanghu.bingshuo-system-write-admission-decision/v1",
|
||||
decision: "DROP_BEFORE_INFERENCE",
|
||||
error_code: errorCode,
|
||||
invoke_model: false,
|
||||
activate_protocols: false,
|
||||
open_sealed_payload: false,
|
||||
persist_request_body: false,
|
||||
audit_policy: "COUNT_ERROR_CODE_ONLY_NO_REQUEST_BODY",
|
||||
};
|
||||
}
|
||||
|
||||
function canonicalRequest(request) {
|
||||
if (!request || JSON.stringify(Object.keys(request)) !== JSON.stringify(SIGNED_FIELDS)) return null;
|
||||
return Buffer.from(JSON.stringify(request));
|
||||
}
|
||||
|
||||
function verifyNodeSignature(publicKeyHex, canonical, signature) {
|
||||
if (!/^[0-9a-f]{64}$/i.test(String(publicKeyHex)) || !/^[A-Za-z0-9_-]{86}$/.test(String(signature))) return false;
|
||||
const prefix = Buffer.from("302a300506032b6570032100", "hex");
|
||||
const key = crypto.createPublicKey({
|
||||
key: Buffer.concat([prefix, Buffer.from(publicKeyHex, "hex")]),
|
||||
format: "der",
|
||||
type: "spki",
|
||||
});
|
||||
return crypto.verify(null, canonical, key, Buffer.from(signature, "base64url"));
|
||||
}
|
||||
|
||||
function evaluateWriteAdmission(envelope, options = {}) {
|
||||
const personaRegistry = options.personaRegistry || JSON.parse(fs.readFileSync(options.personaRegistryFile || DEFAULT_PERSONAS, "utf8"));
|
||||
const nodeRegistry = options.nodeRegistry || JSON.parse(fs.readFileSync(options.nodeRegistryFile || DEFAULT_NODES, "utf8"));
|
||||
const replayCache = options.replayCache || new Set();
|
||||
const now = Number(options.nowUnix ?? Math.floor(Date.now() / 1000));
|
||||
if (!envelope || envelope.schema !== "guanghu.bingshuo-system-node-write-envelope/v1") return reject("INVALID_MINIMAL_ENVELOPE");
|
||||
const canonical = canonicalRequest(envelope.request);
|
||||
if (!canonical) return reject("NODE_REQUEST_NOT_CANONICAL");
|
||||
const request = envelope.request;
|
||||
if (request.schema !== "guanghu.bingshuo-system-node-write-request/v1") return reject("NODE_REQUEST_SCHEMA_INVALID");
|
||||
const node = (nodeRegistry.nodes || []).find((item) => item.node_id === request.node_id && item.state === "ACTIVE");
|
||||
if (!node) return reject("ORIGIN_NODE_NOT_REGISTERED");
|
||||
const key = (node.transport_keys || []).find((item) => item.key_id === request.key_id && item.state === "ACTIVE");
|
||||
if (!key) return reject("ORIGIN_NODE_KEY_NOT_REGISTERED");
|
||||
if (!Number.isSafeInteger(request.issued_at_unix) || Math.abs(now - request.issued_at_unix) > 30) return reject("NODE_REQUEST_EXPIRED");
|
||||
if (!/^[A-Za-z0-9_-]{32}$/.test(String(request.nonce)) || !/^[0-9a-f]{64}$/.test(String(request.sealed_payload_sha256))) return reject("NODE_REQUEST_BINDING_INVALID");
|
||||
if (!verifyNodeSignature(key.public_key_hex, canonical, envelope.node_signature_base64url)) return reject("ORIGIN_NODE_SIGNATURE_INVALID");
|
||||
const replayDigest = crypto.createHash("sha256").update(canonical).digest("hex");
|
||||
if (replayCache.has(replayDigest)) return reject("NODE_REQUEST_REPLAYED");
|
||||
const persona = (personaRegistry.subjects || []).find((item) => item.id === request.persona_id && item.subject_kind === "persona_system");
|
||||
if (!persona) return reject("PERSONA_EXECUTOR_NOT_REGISTERED");
|
||||
if (!(node.persona_ids || []).includes(request.persona_id)) return reject("PERSONA_NOT_BOUND_TO_ORIGIN_NODE");
|
||||
if (!request.authorization_receipt_id || !request.language_event_id) return reject("HUMAN_LANGUAGE_AUTHORIZATION_RECEIPT_MISSING");
|
||||
if (!(node.allowed_targets || []).includes(request.target)) return reject("TARGET_NOT_ALLOWED_FOR_ORIGIN_NODE");
|
||||
if (!(node.allowed_actions || []).includes(request.action)) return reject("ACTION_NOT_ALLOWED_FOR_ORIGIN_NODE");
|
||||
replayCache.add(replayDigest);
|
||||
return {
|
||||
schema: "guanghu.bingshuo-system-write-admission-decision/v1",
|
||||
decision: "ENTER_WHOLE_BODY_REVIEW",
|
||||
error_code: null,
|
||||
invoke_model: true,
|
||||
activate_protocols: true,
|
||||
open_sealed_payload: "ONLY_IN_NEXT_GATE_AFTER_AUTHORIZATION_RECEIPT_VALIDATION",
|
||||
persist_request_body: "ONLY_AFTER_WHOLE_BODY_REVIEW_ALLOWS_WRITE",
|
||||
origin_node_id: node.node_id,
|
||||
persona_id: persona.id,
|
||||
authorization_receipt_id: request.authorization_receipt_id,
|
||||
language_event_id: request.language_event_id,
|
||||
sealed_payload_sha256: request.sealed_payload_sha256,
|
||||
next_gate: "VALIDATE_LANGUAGE_AUTHORIZATION_RECEIPT_THEN_BS-ORGAN-COLLECTIVE-VALIDATION-001",
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { SIGNED_FIELDS, canonicalRequest, evaluateWriteAdmission, verifyNodeSignature };
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
"use strict";
|
||||
const assert = require("node:assert/strict");
|
||||
const crypto = require("node:crypto");
|
||||
const test = require("node:test");
|
||||
const { canonicalRequest, evaluateWriteAdmission } = require("./write-admission");
|
||||
|
||||
function rawPublic(key) {
|
||||
return key.export({ format: "der", type: "spki" }).subarray(-32).toString("hex");
|
||||
}
|
||||
|
||||
function fixture() {
|
||||
const keys = crypto.generateKeyPairSync("ed25519");
|
||||
const nodeRegistry = { nodes: [{
|
||||
node_id: "TEST-NODE-001", state: "ACTIVE", persona_ids: ["ICE-P-ZY001"],
|
||||
allowed_targets: ["BS-TCS-SYSTEM-BODY-ORGAN-MAP-001"], allowed_actions: ["UPDATE_REGISTERED_RULE"],
|
||||
transport_keys: [{ key_id: "TEST-KEY-001", state: "ACTIVE", algorithm: "Ed25519", public_key_hex: rawPublic(keys.publicKey) }]
|
||||
}] };
|
||||
const request = {
|
||||
schema: "guanghu.bingshuo-system-node-write-request/v1",
|
||||
node_id: "TEST-NODE-001", key_id: "TEST-KEY-001", issued_at_unix: 1786363200,
|
||||
nonce: "0123456789abcdef0123456789abcdef", persona_id: "ICE-P-ZY001",
|
||||
authorization_receipt_id: "LANG-AUTH-TEST-001", language_event_id: "LANG-EVENT-TEST-001",
|
||||
target: "BS-TCS-SYSTEM-BODY-ORGAN-MAP-001", action: "UPDATE_REGISTERED_RULE",
|
||||
sealed_payload_sha256: "ab".repeat(32)
|
||||
};
|
||||
const envelope = {
|
||||
schema: "guanghu.bingshuo-system-node-write-envelope/v1",
|
||||
request,
|
||||
node_signature_base64url: crypto.sign(null, canonicalRequest(request), keys.privateKey).toString("base64url")
|
||||
};
|
||||
return { envelope, keys, nodeRegistry };
|
||||
}
|
||||
|
||||
test("a registered node signature is the first admission fact", () => {
|
||||
const { envelope, nodeRegistry } = fixture();
|
||||
const allowed = evaluateWriteAdmission(envelope, { nodeRegistry, nowUnix: 1786363200 });
|
||||
assert.equal(allowed.decision, "ENTER_WHOLE_BODY_REVIEW");
|
||||
assert.equal(allowed.origin_node_id, "TEST-NODE-001");
|
||||
assert.match(allowed.next_gate, /VALIDATE_LANGUAGE_AUTHORIZATION_RECEIPT/);
|
||||
assert.equal(evaluateWriteAdmission(envelope, { nodeRegistry: { nodes: [] }, nowUnix: 1786363200 }).error_code, "ORIGIN_NODE_NOT_REGISTERED");
|
||||
});
|
||||
|
||||
test("root-like possession without the node protocol key is noise", () => {
|
||||
const { envelope, nodeRegistry } = fixture();
|
||||
envelope.node_signature_base64url = "A".repeat(86);
|
||||
const decision = evaluateWriteAdmission(envelope, { nodeRegistry, nowUnix: 1786363200 });
|
||||
assert.equal(decision.error_code, "ORIGIN_NODE_SIGNATURE_INVALID");
|
||||
assert.equal(decision.invoke_model, false);
|
||||
assert.equal(decision.activate_protocols, false);
|
||||
assert.equal(decision.open_sealed_payload, false);
|
||||
assert.equal(decision.persist_request_body, false);
|
||||
});
|
||||
|
||||
test("registered persona, node binding, authorization receipt and replay gate all fail closed", () => {
|
||||
const first = fixture();
|
||||
first.envelope.request.persona_id = "CHENGLU-AGENT-001";
|
||||
first.envelope.node_signature_base64url = crypto.sign(null, canonicalRequest(first.envelope.request), first.keys.privateKey).toString("base64url");
|
||||
assert.equal(evaluateWriteAdmission(first.envelope, { nodeRegistry: first.nodeRegistry, nowUnix: 1786363200 }).error_code, "PERSONA_NOT_BOUND_TO_ORIGIN_NODE");
|
||||
const second = fixture();
|
||||
const replay = new Set();
|
||||
assert.equal(evaluateWriteAdmission(second.envelope, { nodeRegistry: second.nodeRegistry, replayCache: replay, nowUnix: 1786363200 }).decision, "ENTER_WHOLE_BODY_REVIEW");
|
||||
assert.equal(evaluateWriteAdmission(second.envelope, { nodeRegistry: second.nodeRegistry, replayCache: replay, nowUnix: 1786363200 }).error_code, "NODE_REQUEST_REPLAYED");
|
||||
});
|
||||
Loading…
Reference in a new issue