"use strict"; const crypto = require("node:crypto"); const fs = require("node:fs"); const path = require("node:path"); const POLL_FIELDS = [ "schema", "node_id", "issued_at_unix", "nonce", ]; const RESULT_FIELDS = [ "schema", "node_id", "job_id", "layout_payload_sha256", "signature_hex", "issued_at_unix", "nonce", ]; class GhdrControllerBroker { constructor({ controllers, stateDir = "/var/lib/guanghu/lake-lamp-authz/ghdr-jobs", authorizer, now = () => Math.floor(Date.now() / 1000), waitMs = 45000, }) { if (!Array.isArray(controllers) || controllers.length !== 2) { throw new Error("ghdr_controller_registry_requires_exactly_two"); } this.controllers = new Map(controllers.map(item => [item.node_id, validateController(item)])); if (this.controllers.size !== 2 || new Set(controllers.map(item => item.failure_domain)).size !== 2) { throw new Error("ghdr_controller_registry_not_independent"); } this.stateDir = stateDir; this.authorizer = authorizer; this.now = now; this.waitMs = waitMs; this.replay = new Map(); fs.mkdirSync(this.stateDir, { recursive: true, mode: 0o700 }); } async queueAndWait({ plan, binding, workorderId }) { const jobId = crypto.randomUUID(); const job = { schema: "guanghu.ghdr-controller-job/v1", job_id: jobId, created_at_unix: this.now(), expires_at_unix: this.now() + 120, layout_payload_sha256: binding.payload_sha256, resource: binding.resource, workorder_id: workorderId, plan, authorizations: {}, results: {}, }; for (const controller of this.controllers.values()) { job.authorizations[controller.node_id] = this.authorizer.issue({ controllerNodeId: controller.node_id, targetNodeId: "GH-CVM-MAIN-PROD-01", layoutPayloadSha256: binding.payload_sha256, resource: binding.resource, workorderId, }); } this.writeJob(job); const deadline = Date.now() + this.waitMs; while (Date.now() < deadline) { const current = this.readJob(jobId); if (Object.keys(current.results).length === 2) { return { ok: true, signatures: [...this.controllers.keys()].map(nodeId => current.results[nodeId]), }; } await new Promise(resolve => setTimeout(resolve, 250)); } return { ok: false, error: "ghdr_controller_poll_timeout" }; } poll(body) { const authenticated = this.authenticate(body && body.request, body && body.request_signature_hex, POLL_FIELDS, "guanghu.ghdr-controller-poll/v1"); if (!authenticated.ok) return authenticated; const nodeId = authenticated.node_id; const names = fs.readdirSync(this.stateDir).filter(name => /^[0-9a-f-]{36}\.json$/.test(name)).sort(); for (const name of names) { const job = this.readJob(name.slice(0, -5)); if (job.expires_at_unix <= this.now() || job.results[nodeId]) continue; return { ok: true, job: { job_id: job.job_id, expires_at_unix: job.expires_at_unix, layout_payload_sha256: job.layout_payload_sha256, authorization: job.authorizations[nodeId], plan: job.plan, }, }; } return { ok: true, job: null }; } submit(body) { const authenticated = this.authenticate(body && body.request, body && body.request_signature_hex, RESULT_FIELDS, "guanghu.ghdr-controller-result/v1"); if (!authenticated.ok) return authenticated; const request = body.request; const job = this.readJob(request.job_id); if (job.expires_at_unix <= this.now()) return { ok: false, error: "ghdr_job_expired" }; if (job.layout_payload_sha256 !== request.layout_payload_sha256) { return { ok: false, error: "ghdr_result_layout_mismatch" }; } const controller = this.controllers.get(request.node_id); const signature = body.signature; if (!signature || signature.node_id !== controller.node_id || signature.failure_domain !== controller.failure_domain || signature.public_key_hex !== controller.layout_public_key_hex || signature.signature_hex !== request.signature_hex) { return { ok: false, error: "ghdr_result_binding_mismatch" }; } const payload = Buffer.from(JSON.stringify(job.plan.payload)); if (!verifyHex(controller.layout_public_key_hex, payload, request.signature_hex)) { return { ok: false, error: "ghdr_layout_signature_invalid" }; } if (job.results[controller.node_id]) { return { ok: false, error: "ghdr_result_already_submitted" }; } job.results[controller.node_id] = signature; this.writeJob(job); return { ok: true }; } authenticate(request, signatureHex, fields, schema) { if (!request || typeof request !== "object" || JSON.stringify(Object.keys(request)) !== JSON.stringify(fields)) { return { ok: false, error: "ghdr_controller_auth_not_canonical" }; } if (request.schema !== schema || !this.controllers.has(request.node_id)) { return { ok: false, error: "ghdr_controller_auth_unknown" }; } if (!Number.isSafeInteger(request.issued_at_unix) || Math.abs(this.now() - request.issued_at_unix) > 30 || !/^[A-Za-z0-9_-]{32}$/.test(String(request.nonce || ""))) { return { ok: false, error: "ghdr_controller_auth_expired" }; } const controller = this.controllers.get(request.node_id); const canonical = Buffer.from(JSON.stringify(request)); if (!verifyHex(controller.transport_public_key_hex, canonical, signatureHex)) { return { ok: false, error: "ghdr_controller_auth_invalid" }; } const replay = crypto.createHash("sha256").update(canonical).digest("hex"); for (const [digest, expires] of this.replay) { if (expires < this.now()) this.replay.delete(digest); } if (this.replay.has(replay)) return { ok: false, error: "ghdr_controller_auth_replayed" }; this.replay.set(replay, this.now() + 35); return { ok: true, node_id: request.node_id }; } readJob(jobId) { if (!/^[0-9a-f-]{36}$/.test(String(jobId))) throw new Error("ghdr_job_id_invalid"); return JSON.parse(fs.readFileSync(path.join(this.stateDir, `${jobId}.json`), "utf8")); } writeJob(job) { const destination = path.join(this.stateDir, `${job.job_id}.json`); const temporary = `${destination}.${process.pid}.tmp`; fs.writeFileSync(temporary, JSON.stringify(job), { mode: 0o600 }); fs.renameSync(temporary, destination); } } function validateController(controller) { if (!controller || !/^GH-CTRL-(GZ|SG)-01$/.test(String(controller.node_id)) || !/^tencent\/ap-(guangzhou|singapore)\/[A-Z0-9-]+$/.test(String(controller.failure_domain)) || !/^[0-9a-f]{64}$/.test(String(controller.layout_public_key_hex)) || !/^[0-9a-f]{64}$/.test(String(controller.transport_public_key_hex))) { throw new Error("ghdr_controller_registry_invalid"); } return Object.freeze({ ...controller }); } function verifyHex(publicKeyHex, message, signatureHex) { if (!/^[0-9a-f]{128}$/.test(String(signatureHex || ""))) return false; const prefix = Buffer.from("302a300506032b6570032100", "hex"); const publicKey = crypto.createPublicKey({ key: Buffer.concat([prefix, Buffer.from(publicKeyHex, "hex")]), format: "der", type: "spki", }); return crypto.verify(null, message, publicKey, Buffer.from(signatureHex, "hex")); } function loadControllers(file) { const parsed = JSON.parse(fs.readFileSync(file, "utf8")); return parsed.controllers; } module.exports = { GhdrControllerBroker, loadControllers, verifyHex };