diff --git a/server-tools/lake-lamp-authz/README.md b/server-tools/lake-lamp-authz/README.md index e749f6b..7113e78 100644 --- a/server-tools/lake-lamp-authz/README.md +++ b/server-tools/lake-lamp-authz/README.md @@ -43,23 +43,6 @@ HoloLake 手机端使用独立的邮箱验证码会话,不复用工单批准 `/etc/guanghu/secrets/hololake-ai-providers.json`,不得提交到仓库。知识仓库路径、模型 登记文件、session pepper 和会话状态路径由 `authorization.env` 固定;手机不能切换这些路径。 -## 企业 GHDR 邮件授权与服务器代签 - -企业原生布局使用独立的 `native-recovery / sign-native-layout-plan` -工单。该动作必须由京东主控的预登记邮箱完成本次批准;在线 HoloLake -广播面板、旧会话和未记录批准通道的会话都不能替代邮件批准。 - -批准后,京东只签发最长两分钟、单次使用、精确绑定控制器、目标节点、工单、 -布局摘要、资源和代次的 Ed25519 能力票据。广州和新加坡控制器通过出站 HTTPS -轮询取得各自票据,在本机固定用途签名器中完成代签,再把签名结果回送京东。 - -- 布局私钥与轮询传输私钥分离,均只存在于控制器服务器; -- 客户端、浏览器、Mac、企业目标机和代码仓库都不接收私钥; -- `/api/ghdr/authorizer-public-key` 只返回京东授权公钥和指纹; -- `/api/ghdr/controllers/poll` 与 `/result` 只接受控制器传输私钥签过的规范请求; -- `/api/ghdr/sign-layout` 必须收齐两个不同节点、不同故障域的签名才成功; -- 任何端点都不提供私钥导出、任意 URL、任意命令或单签降级。 - `request-workorder.js` 从临时环境变量读取 QQ 数字,在内存中补全邮箱并只发送 SHA-256 指纹;数字本身不会写入请求正文、状态文件或代码仓库。未提供私密 request credential 时,脚本自动切换到跨设备公开建单模式。 diff --git a/server-tools/lake-lamp-authz/ghdr-authorizer.js b/server-tools/lake-lamp-authz/ghdr-authorizer.js deleted file mode 100644 index cb340c9..0000000 --- a/server-tools/lake-lamp-authz/ghdr-authorizer.js +++ /dev/null @@ -1,75 +0,0 @@ -"use strict"; - -const crypto = require("node:crypto"); -const fs = require("node:fs"); -const path = require("node:path"); - -class GhdrAuthorizer { - constructor({ - privateKeyFile = "/var/lib/guanghu/lake-lamp-authz/ghdr-authorizer-private.pem", - now = () => Math.floor(Date.now() / 1000), - } = {}) { - this.privateKeyFile = privateKeyFile; - this.now = now; - this.privateKey = this.loadOrCreate(); - this.publicKey = crypto.createPublicKey(this.privateKey); - } - - loadOrCreate() { - if (fs.existsSync(this.privateKeyFile)) { - const metadata = fs.lstatSync(this.privateKeyFile); - if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.mode & 0o077) { - throw new Error("ghdr_authorizer_private_key_permissions_invalid"); - } - return crypto.createPrivateKey(fs.readFileSync(this.privateKeyFile)); - } - fs.mkdirSync(path.dirname(this.privateKeyFile), { recursive: true, mode: 0o700 }); - const { privateKey } = crypto.generateKeyPairSync("ed25519"); - const pem = privateKey.export({ type: "pkcs8", format: "pem" }); - const temporary = `${this.privateKeyFile}.${process.pid}.tmp`; - fs.writeFileSync(temporary, pem, { mode: 0o600, flag: "wx" }); - fs.renameSync(temporary, this.privateKeyFile); - return privateKey; - } - - publicBinding() { - const publicPem = this.publicKey.export({ type: "spki", format: "pem" }); - const fingerprint = crypto.createHash("sha256").update(publicPem).digest("hex"); - return { - schema: "guanghu.ghdr-authorizer-public-binding/v1", - authorizer_id: "JD-FD-PRIMARY-LAKE-LAMP", - algorithm: "Ed25519", - public_key_pem: publicPem, - public_key_sha256: fingerprint, - }; - } - - issue({ - controllerNodeId, - targetNodeId, - layoutPayloadSha256, - resource, - workorderId, - }) { - const issued = this.now(); - const capability = { - schema: "guanghu.ghdr-signing-capability/v1", - authorizer_id: "JD-FD-PRIMARY-LAKE-LAMP", - controller_node_id: controllerNodeId, - target_node_id: targetNodeId, - layout_payload_sha256: layoutPayloadSha256, - resource, - workorder_id: workorderId, - issued_at_unix: issued, - expires_at_unix: issued + 120, - nonce: crypto.randomBytes(24).toString("base64url"), - }; - const canonical = Buffer.from(JSON.stringify(capability)); - return { - capability, - capability_signature_base64url: crypto.sign(null, canonical, this.privateKey).toString("base64url"), - }; - } -} - -module.exports = { GhdrAuthorizer }; diff --git a/server-tools/lake-lamp-authz/ghdr-authorizer.test.js b/server-tools/lake-lamp-authz/ghdr-authorizer.test.js deleted file mode 100644 index 818245a..0000000 --- a/server-tools/lake-lamp-authz/ghdr-authorizer.test.js +++ /dev/null @@ -1,39 +0,0 @@ -"use strict"; - -const test = require("node:test"); -const assert = require("node:assert/strict"); -const crypto = require("node:crypto"); -const fs = require("node:fs"); -const os = require("node:os"); -const path = require("node:path"); -const { GhdrAuthorizer } = require("./ghdr-authorizer"); - -test("GHDR authorizer persists a private key without exposing it and signs a two-minute capability", () => { - const directory = fs.mkdtempSync(path.join(os.tmpdir(), "ghdr-authorizer-")); - try { - const privateKeyFile = path.join(directory, "authorizer.pem"); - const authorizer = new GhdrAuthorizer({ privateKeyFile, now: () => 1000 }); - const issued = authorizer.issue({ - controllerNodeId: "GH-CTRL-GZ-01", - targetNodeId: "GH-CVM-MAIN-PROD-01", - layoutPayloadSha256: "a".repeat(64), - resource: `GH-CVM-MAIN-PROD-01:${"a".repeat(64)}:1`, - workorderId: "00000000-0000-4000-8000-000000000001", - }); - assert.equal(fs.statSync(privateKeyFile).mode & 0o777, 0o600); - assert.equal(issued.capability.expires_at_unix, 1120); - const publicKey = crypto.createPublicKey(authorizer.publicBinding().public_key_pem); - assert.equal( - crypto.verify( - null, - Buffer.from(JSON.stringify(issued.capability)), - publicKey, - Buffer.from(issued.capability_signature_base64url, "base64url"), - ), - true, - ); - assert.doesNotMatch(JSON.stringify(authorizer.publicBinding()), /PRIVATE KEY/); - } finally { - fs.rmSync(directory, { recursive: true, force: true }); - } -}); diff --git a/server-tools/lake-lamp-authz/ghdr-controller-broker.js b/server-tools/lake-lamp-authz/ghdr-controller-broker.js deleted file mode 100644 index 8c55c1d..0000000 --- a/server-tools/lake-lamp-authz/ghdr-controller-broker.js +++ /dev/null @@ -1,195 +0,0 @@ -"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 }; diff --git a/server-tools/lake-lamp-authz/ghdr-controller-broker.test.js b/server-tools/lake-lamp-authz/ghdr-controller-broker.test.js deleted file mode 100644 index 3d8077d..0000000 --- a/server-tools/lake-lamp-authz/ghdr-controller-broker.test.js +++ /dev/null @@ -1,116 +0,0 @@ -"use strict"; - -const assert = require("node:assert/strict"); -const crypto = require("node:crypto"); -const fs = require("node:fs"); -const os = require("node:os"); -const path = require("node:path"); -const test = require("node:test"); -const { GhdrAuthorizer } = require("./ghdr-authorizer"); -const { GhdrControllerBroker } = require("./ghdr-controller-broker"); - -test("two outbound controllers poll one email-authorized job and return independent layout signatures", async () => { - const directory = fs.mkdtempSync(path.join(os.tmpdir(), "ghdr-broker-")); - const now = Math.floor(Date.now() / 1000); - const nodes = [ - makeController("GH-CTRL-GZ-01", "tencent/ap-guangzhou/BS-GZ-006"), - makeController("GH-CTRL-SG-01", "tencent/ap-singapore/ZY-SG-006"), - ]; - const authorizer = new GhdrAuthorizer({ - privateKeyFile: path.join(directory, "authorizer.pem"), - now: () => now, - }); - const broker = new GhdrControllerBroker({ - controllers: nodes.map(item => item.binding), - stateDir: path.join(directory, "jobs"), - authorizer, - now: () => now, - waitMs: 2000, - }); - const plan = { - schema: "guanghu.ghdr-signed-layout-plan/v1", - payload: { - node_id: "GH-CVM-MAIN-PROD-01", - generation: 1, - }, - signatures: [], - }; - const digest = crypto.createHash("sha256").update(JSON.stringify(plan.payload)).digest("hex"); - const binding = { - payload_sha256: digest, - resource: `GH-CVM-MAIN-PROD-01:${digest}:1`, - }; - try { - const pending = broker.queueAndWait({ - plan, - binding, - workorderId: "00000000-0000-4000-8000-000000000001", - }); - const submitted = []; - for (const node of nodes) { - const poll = { - schema: "guanghu.ghdr-controller-poll/v1", - node_id: node.binding.node_id, - issued_at_unix: now, - nonce: crypto.randomBytes(24).toString("base64url"), - }; - const body = { - request: poll, - request_signature_hex: crypto.sign(null, Buffer.from(JSON.stringify(poll)), node.transport.privateKey).toString("hex"), - }; - const job = broker.poll(body).job; - assert.ok(job); - assert.equal(broker.poll(body).error, "ghdr_controller_auth_replayed"); - const layoutSignatureHex = crypto.sign( - null, - Buffer.from(JSON.stringify(plan.payload)), - node.layout.privateKey, - ).toString("hex"); - const result = { - schema: "guanghu.ghdr-controller-result/v1", - node_id: node.binding.node_id, - job_id: job.job_id, - layout_payload_sha256: digest, - signature_hex: layoutSignatureHex, - issued_at_unix: now, - nonce: crypto.randomBytes(24).toString("base64url"), - }; - const signature = { - node_id: node.binding.node_id, - failure_domain: node.binding.failure_domain, - public_key_hex: node.binding.layout_public_key_hex, - signature_hex: layoutSignatureHex, - }; - assert.equal(broker.submit({ - request: result, - request_signature_hex: crypto.sign(null, Buffer.from(JSON.stringify(result)), node.transport.privateKey).toString("hex"), - signature, - }).ok, true); - submitted.push(signature); - } - const completed = await pending; - assert.equal(completed.ok, true); - assert.deepEqual(completed.signatures, submitted); - } finally { - fs.rmSync(directory, { recursive: true, force: true }); - } -}); - -function makeController(nodeId, failureDomain) { - const layout = crypto.generateKeyPairSync("ed25519"); - const transport = crypto.generateKeyPairSync("ed25519"); - return { - layout, - transport, - binding: { - node_id: nodeId, - failure_domain: failureDomain, - layout_public_key_hex: rawPublic(layout.publicKey), - transport_public_key_hex: rawPublic(transport.publicKey), - }, - }; -} - -function rawPublic(key) { - return key.export({ type: "spki", format: "der" }).subarray(-32).toString("hex"); -} diff --git a/server-tools/lake-lamp-authz/ghdr-controllers.json b/server-tools/lake-lamp-authz/ghdr-controllers.json deleted file mode 100644 index 3e395c4..0000000 --- a/server-tools/lake-lamp-authz/ghdr-controllers.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "schema": "guanghu.ghdr-controller-registry/v1", - "controllers": [ - { - "node_id": "GH-CTRL-GZ-01", - "failure_domain": "tencent/ap-guangzhou/BS-GZ-006", - "layout_public_key_hex": "d37d943381adeb215b5af769dd0cda79c37c289dedad9c52504f68aa71fcf353", - "transport_public_key_hex": "97d4e886276f3d06544dcd02422fd19a50142752218620bdde9c70e43ff374f0" - }, - { - "node_id": "GH-CTRL-SG-01", - "failure_domain": "tencent/ap-singapore/ZY-SG-006", - "layout_public_key_hex": "58b2fe26d90c26a09ecde6af2c742b5df35267d1c2b03af94468698d6bf2864f", - "transport_public_key_hex": "099e5963ae3956408fd6e0c7980b3c7813e20eae85972ffba4220976e7093395" - } - ] -} diff --git a/server-tools/lake-lamp-authz/install-architecture-provisioner.sh b/server-tools/lake-lamp-authz/install-architecture-provisioner.sh index 7c6c919..ec27820 100755 --- a/server-tools/lake-lamp-authz/install-architecture-provisioner.sh +++ b/server-tools/lake-lamp-authz/install-architecture-provisioner.sh @@ -13,7 +13,6 @@ secret_root=/etc/guanghu/secrets/lake-lamp authorization_env=$secret_root/authorization.env provider_registry=/etc/guanghu/secrets/hololake-ai-providers.json knowledge_repo=/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1/data/repositories/bingshuo/hololake-knowledge-base.git -ghdr_authorizer_key=/var/lib/guanghu/lake-lamp-authz/ghdr-authorizer-private.pem stamp=$(date -u +%Y%m%dT%H%M%SZ) backup_root=$state_root/manual-backups/lake-lamp-authz-$stamp @@ -31,9 +30,6 @@ required_source_files=( repo-push-broker.js hololake-session.js hololake-capabilities.js - ghdr-authorizer.js - ghdr-controller-broker.js - ghdr-controllers.json ) for file in "${required_source_files[@]}" lake-lamp-authz.service lake-lamp-architecture-provision.service lake-lamp-deployment-event-worker.service; do @@ -42,10 +38,6 @@ for file in "${required_source_files[@]}" lake-lamp-authz.service lake-lamp-arch exit 1 } done -[[ -f "$script_dir/navigation-maps/GH-CVM-MAIN-PROD-01.json" ]] || { - echo "missing required GHDR navigation map" >&2 - exit 1 -} install -d -m 0700 "$backup_root" [[ ! -e "$install_root" ]] || cp -a "$install_root" "$backup_root/install-root" @@ -54,8 +46,7 @@ for existing in \ /etc/systemd/system/lake-lamp-architecture-provision.service \ /etc/systemd/system/lake-lamp-deployment-event-worker.service \ "$authorization_env" \ - "$provider_registry" \ - "$ghdr_authorizer_key"; do + "$provider_registry"; do if [[ -e "$existing" ]]; then destination=$backup_root/existing${existing} install -d -m 0700 "$(dirname "$destination")" @@ -74,13 +65,12 @@ rollback() { /etc/systemd/system/lake-lamp-architecture-provision.service \ /etc/systemd/system/lake-lamp-deployment-event-worker.service \ "$authorization_env" \ - "$provider_registry" \ - "$ghdr_authorizer_key"; do + "$provider_registry"; do saved=$backup_root/existing${existing} if [[ -e "$saved" ]]; then install -d -m 0755 "$(dirname "$existing")" cp -a "$saved" "$existing" - elif [[ "$existing" = "$provider_registry" || "$existing" = "$ghdr_authorizer_key" ]]; then + elif [[ "$existing" = "$provider_registry" ]]; then rm -f -- "$existing" fi done @@ -95,10 +85,6 @@ install -d -m 0755 "$install_root" for file in "${required_source_files[@]}"; do install -m 0644 "$script_dir/$file" "$install_root/$file" done -install -d -m 0755 "$install_root/navigation-maps" -install -m 0644 \ - "$script_dir/navigation-maps/GH-CVM-MAIN-PROD-01.json" \ - "$install_root/navigation-maps/GH-CVM-MAIN-PROD-01.json" install -m 0644 "$script_dir/lake-lamp-authz.service" /etc/systemd/system/lake-lamp-authz.service install -m 0644 "$script_dir/lake-lamp-architecture-provision.service" /etc/systemd/system/lake-lamp-architecture-provision.service install -m 0644 "$script_dir/lake-lamp-deployment-event-worker.service" /etc/systemd/system/lake-lamp-deployment-event-worker.service @@ -234,16 +220,5 @@ const health = JSON.parse(process.argv[1]); if (!health.hololake_mobile || !health.hololake_mobile.ai_gateway) process.exit(1); ' "$health" fi -ghdr_public=$(/usr/bin/curl -fsS --max-time 10 http://127.0.0.1:3921/api/ghdr/authorizer-public-key) -/usr/bin/node -e ' -const response = JSON.parse(process.argv[1]); -const binding = response && response.binding; -if (!response.ok || !binding || binding.algorithm !== "Ed25519") process.exit(1); -if (!/^[-A-Za-z0-9+/=\r\n ]*PUBLIC KEY[-A-Za-z0-9+/=\r\n ]*$/.test(binding.public_key_pem)) process.exit(1); -if (!/^[0-9a-f]{64}$/.test(binding.public_key_sha256)) process.exit(1); -if (/PRIVATE KEY/.test(JSON.stringify(response))) process.exit(1); -' "$ghdr_public" -[[ -f "$ghdr_authorizer_key" && ! -L "$ghdr_authorizer_key" ]] -[[ $(stat -c '%a' "$ghdr_authorizer_key") = 600 ]] trap - EXIT -printf 'HOLOLAKE_MOBILE_AND_GHDR_CAPABILITIES_INSTALLED backup=%s\n' "$backup_root" +printf 'HOLOLAKE_MOBILE_CAPABILITIES_INSTALLED backup=%s\n' "$backup_root" diff --git a/server-tools/lake-lamp-authz/install-architecture-provisioner.test.js b/server-tools/lake-lamp-authz/install-architecture-provisioner.test.js index 661155f..f433189 100644 --- a/server-tools/lake-lamp-authz/install-architecture-provisioner.test.js +++ b/server-tools/lake-lamp-authz/install-architecture-provisioner.test.js @@ -17,9 +17,6 @@ test("bootstrap installer preserves secrets and deploys the complete HoloLake ca "repo-push-broker.js", "hololake-session.js", "hololake-capabilities.js", - "ghdr-authorizer.js", - "ghdr-controller-broker.js", - "ghdr-controllers.json", ]) { assert.match(source, new RegExp(`\\b${file.replaceAll(".", "\\.")}\\b`)); } @@ -32,10 +29,6 @@ test("bootstrap installer preserves secrets and deploys the complete HoloLake ca assert.match(source, /setfacl -R -m u:guanghu-authz:rX/); assert.doesNotMatch(source, /cat ["']?\$authorization_env/); assert.doesNotMatch(source, /set -x/); - assert.match(source, /navigation-maps\/GH-CVM-MAIN-PROD-01\.json/); - assert.match(source, /api\/ghdr\/authorizer-public-key/); - assert.match(source, /PRIVATE KEY/); - assert.match(source, /stat -c '%a'/); }); test("provider migration writes only a private registry and never prints API keys", () => { diff --git a/server-tools/lake-lamp-authz/map-gate.js b/server-tools/lake-lamp-authz/map-gate.js index d43f1c9..15ff1ec 100644 --- a/server-tools/lake-lamp-authz/map-gate.js +++ b/server-tools/lake-lamp-authz/map-gate.js @@ -4,22 +4,15 @@ const fs = require("node:fs"); const path = require("node:path"); class MapGate { - constructor({ mapsDir = "/etc/guanghu/navigation-maps", fallbackMapsDir = "", stateFile = "" } = {}) { + constructor({ mapsDir = "/etc/guanghu/navigation-maps", stateFile = "" } = {}) { this.mapsDir = mapsDir; - this.fallbackMapsDir = fallbackMapsDir; this.stateFile = stateFile; this.acks = new Map(); this.load(); } read(target) { if (!/^[A-Z0-9-]+$/.test(target)) throw new Error("invalid_target"); - const primary = path.join(this.mapsDir, `${target}.json`); - const fallback = this.fallbackMapsDir - ? path.join(this.fallbackMapsDir, `${target}.json`) - : ""; - const selected = fs.existsSync(primary) ? primary : fallback; - if (!selected || !fs.existsSync(selected)) throw new Error("navigation_map_not_found"); - const data = JSON.parse(fs.readFileSync(selected, "utf8")); + const data = JSON.parse(fs.readFileSync(path.join(this.mapsDir, `${target}.json`), "utf8")); const canonical = JSON.stringify(data); return { data, hash: crypto.createHash("sha256").update(canonical).digest("hex") }; } diff --git a/server-tools/lake-lamp-authz/navigation-maps/GH-CVM-MAIN-PROD-01.json b/server-tools/lake-lamp-authz/navigation-maps/GH-CVM-MAIN-PROD-01.json deleted file mode 100644 index 827bef2..0000000 --- a/server-tools/lake-lamp-authz/navigation-maps/GH-CVM-MAIN-PROD-01.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "schema": "guanghu.navigation-map/v1", - "node_id": "GH-CVM-MAIN-PROD-01", - "display_name": "光湖企业原生 OS 主节点", - "provider": "tencent_cloud", - "region": "ap-guangzhou", - "system_disk": "/dev/vda", - "authorized_entry": "JD-FD-PRIMARY email authorization", - "allowed_actions": [ - "read-navigation-map", - "sign-native-layout-plan" - ], - "safety": { - "private_keys_exportable": false, - "controller_signatures_required": 2, - "independent_failure_domains_required": 2, - "raw_disk_write": "blocked_until_valid_double_signature" - } -} diff --git a/server-tools/lake-lamp-authz/server.js b/server-tools/lake-lamp-authz/server.js index 3af5ea0..8362b57 100644 --- a/server-tools/lake-lamp-authz/server.js +++ b/server-tools/lake-lamp-authz/server.js @@ -11,8 +11,6 @@ const { executeRegisteredAction } = require("./action-client"); const { enqueueDeploymentEvent } = require("./deployment-event"); const { GuanghuRouter, loadDevices } = require("./guanghu-router"); const { HoloLakeSessionManager } = require("./hololake-session"); -const { GhdrAuthorizer } = require("./ghdr-authorizer"); -const { GhdrControllerBroker, loadControllers } = require("./ghdr-controller-broker"); const { HoloLakeAiGateway, HoloLakeKnowledgeProvider, @@ -50,7 +48,6 @@ const DEFAULT_ACTIONS = Object.freeze({ ], "repo-push": ["read-navigation-map", "push-repository"], "linked-node-ops": ["authorize-linked-node-session"], - "native-recovery": ["read-navigation-map", "sign-native-layout-plan"], }); function createApp(options = {}) { @@ -60,7 +57,6 @@ function createApp(options = {}) { const approvers = options.approvers || loadApprovers(options.approversFile || process.env.LAKE_LAMP_APPROVERS_FILE || "", ownerEmail); const publicBaseUrl = String(options.publicBaseUrl || process.env.LAKE_LAMP_PUBLIC_URL || "").replace(/\/$/, ""); const targets = new Set(options.targets || splitCsv(process.env.LAKE_LAMP_TARGETS || "JD-FD-PRIMARY,BS-GZ-006")); - targets.add("GH-CVM-MAIN-PROD-01"); const actions = options.actions || DEFAULT_ACTIONS; const devices = options.devices || loadDevices( options.devicesFile @@ -149,7 +145,6 @@ function createApp(options = {}) { ); const mapGate = options.mapGate || new MapGate({ mapsDir: options.mapsDir || process.env.LAKE_LAMP_MAPS_DIR || "/etc/guanghu/navigation-maps", - fallbackMapsDir: options.fallbackMapsDir || path.join(__dirname, "navigation-maps"), stateFile: Object.prototype.hasOwnProperty.call(options, "mapStateFile") ? options.mapStateFile : (process.env.LAKE_LAMP_MAP_STATE_FILE || "/var/lib/guanghu/lake-lamp-authz/map-acks.json"), }); const repoGrantDir = options.repoGrantDir || process.env.LAKE_LAMP_REPO_GRANT_DIR || "/var/lib/guanghu/repo-authorizations"; @@ -178,36 +173,6 @@ function createApp(options = {}) { const deploymentQueueDir = options.deploymentQueueDir || process.env.LAKE_LAMP_DEPLOYMENT_EVENT_DIR || "/var/lib/guanghu/deployment-events"; const deploymentRegistryFile = options.deploymentRegistryFile || process.env.LAKE_LAMP_DEPLOYMENT_REPOSITORIES || "/etc/guanghu/lake-lamp/deployment-repositories.json"; const executeAction = options.executeAction || executeRegisteredAction; - let ghdrAuthorizer = null; - const getGhdrAuthorizer = options.getGhdrAuthorizer || (() => { - if (!ghdrAuthorizer) { - ghdrAuthorizer = new GhdrAuthorizer({ - privateKeyFile: options.ghdrAuthorizerPrivateKeyFile - || process.env.GHDR_AUTHORIZER_PRIVATE_KEY_FILE - || "/var/lib/guanghu/lake-lamp-authz/ghdr-authorizer-private.pem", - }); - } - return ghdrAuthorizer; - }); - let ghdrControllerBroker = null; - const getGhdrControllerBroker = options.getGhdrControllerBroker || (() => { - if (!ghdrControllerBroker) { - const registryFile = options.ghdrControllerRegistryFile - || process.env.GHDR_CONTROLLER_REGISTRY_FILE - || path.join(__dirname, "ghdr-controllers.json"); - ghdrControllerBroker = new GhdrControllerBroker({ - controllers: loadControllers(registryFile), - stateDir: options.ghdrJobStateDir - || process.env.GHDR_JOB_STATE_DIR - || "/var/lib/guanghu/lake-lamp-authz/ghdr-jobs", - authorizer: getGhdrAuthorizer(), - }); - } - return ghdrControllerBroker; - }); - const signGhdrPlan = options.signGhdrPlan || ( - request => getGhdrControllerBroker().queueAndWait(request) - ); // Creating a powerless request must never become harder than the human mail // handoff. Keep at least three attempts per network each hour. const publicCreateLimit = Math.max(3, Number(options.publicCreateLimit || process.env.LAKE_LAMP_PUBLIC_CREATE_LIMIT || 24)); @@ -1075,103 +1040,6 @@ function createApp(options = {}) { return json(res, queued.state === "queued_for_resident_agent" ? 202 : 400, { ok: queued.state === "queued_for_resident_agent", deployment: queued, receipt: operationReceipt }); } - if (req.method === "POST" && url.pathname === "/api/ghdr/controllers/poll") { - const body = await readJson(req); - if (!body) return json(res, 400, failure("invalid_json")); - const result = getGhdrControllerBroker().poll(body); - return json(res, result.ok ? 200 : 403, result); - } - - if (req.method === "POST" && url.pathname === "/api/ghdr/controllers/result") { - const body = await readJson(req); - if (!body) return json(res, 400, failure("invalid_json")); - try { - const result = getGhdrControllerBroker().submit(body); - return json(res, result.ok ? 200 : 403, result); - } catch { - return json(res, 400, failure("ghdr_result_invalid")); - } - } - - if (req.method === "POST" && url.pathname === "/api/ghdr/sign-layout") { - const body = await readJson(req); - if (!body) return json(res, 400, failure("invalid_json")); - if (body.cmd || body.command || body.shell || body.args) { - return json(res, 400, failure("arbitrary_command_forbidden")); - } - const token = bearer(req); - const target = String(body.target || ""); - const scope = String(body.scope || "native-recovery"); - const resource = String(body.resource || ""); - const binding = validateGhdrLayoutRequest(body.plan, target, resource); - if (!binding.ok) return json(res, 400, failure(binding.error)); - const verified = manager.verifySession( - token, - { pid: String(body.persona_id || "") }, - target, - scope, - "sign-native-layout-plan", - Date.now() / 1000, - resource, - ); - if (!verified.ok) return json(res, 403, failure(verified.reason)); - if (verified.session.approvalChannel !== "email") { - return json(res, 403, failure( - "ghdr_email_approval_required", - "企业原生布局必须由京东主控的预登记邮箱完成本次批准。", - )); - } - const map = mapGate.read(target); - if (!mapGate.verify(token, target, map.hash).ok) { - return json(res, 423, failure("map_ack_required", "先读取并确认导航图。", { - required_action: "read-navigation-map", - })); - } - const result = await signGhdrPlan({ - plan: body.plan, - binding, - workorderId: verified.session.workorderId, - authorizer: getGhdrAuthorizer(), - }); - const signatures = Array.isArray(result.signatures) ? result.signatures : []; - const independent = signatures.length === 2 - && new Set(signatures.map(item => item && item.node_id)).size === 2 - && new Set(signatures.map(item => item && item.failure_domain)).size === 2; - const succeeded = Boolean(result.ok && independent); - const operationReceipt = receipt({ - state: succeeded ? "succeeded" : "failed", - diagnostic_code: succeeded - ? "ghdr_layout_double_signature_succeeded" - : String(result.error || "ghdr_layout_double_signature_failed"), - workorder_id: verified.session.workorderId, - target, - action: "sign-native-layout-plan", - evidence: { - layout_payload_sha256: binding.payload_sha256, - generation: binding.generation, - controller_count: signatures.length, - controller_ids: signatures.map(item => String(item && item.node_id || "")), - }, - next_step: succeeded - ? "把两份签名合并回同一份布局计划,并由企业目标机独立验证公钥、故障域、实时读回与有效期。" - : "读取 diagnostic_code;不得降级为单签、复制私钥或绕过邮件授权。", - }); - manager.recordReceipt(token, operationReceipt); - return json(res, succeeded ? 200 : 502, { - ok: succeeded, - layout_payload_sha256: binding.payload_sha256, - signatures: succeeded ? signatures : [], - receipt: operationReceipt, - }); - } - - if (req.method === "GET" && url.pathname === "/api/ghdr/authorizer-public-key") { - return json(res, 200, { - ok: true, - binding: getGhdrAuthorizer().publicBinding(), - }); - } - return json(res, 404, { error: "not_found" }); } catch (error) { process.stderr.write(`lake-lamp request error: ${String(error && error.message || "unknown").slice(0, 240)}\n`); @@ -1385,44 +1253,15 @@ function validateWorkorderBody(body, targets, actions) { const immutableResourceAction = action === "provision-approved-architecture" || action === "dispatch-approved-deployment"; const repoPushResourceAction = action === "push-repository"; const linkedNodeResourceAction = action === "authorize-linked-node-session"; - const ghdrLayoutResourceAction = action === "sign-native-layout-plan"; if (immutableResourceAction && !/^[A-Z0-9][A-Z0-9._-]{5,119}@[0-9a-f]{40}$/.test(resource)) return { ok: false, status: 400, error: "immutable_architecture_resource_required" }; if (repoPushResourceAction && resource && !/^bingshuo\/[a-z0-9._-]+@[a-z0-9][a-z0-9._/-]{0,199}$/.test(resource)) return { ok: false, status: 400, error: "repo_push_resource_invalid" }; if (linkedNodeResourceAction && !/^[A-Z0-9][A-Z0-9._-]{5,119}:[A-Za-z0-9._-]{3,120}$/.test(resource)) return { ok: false, status: 400, error: "linked_node_resource_required" }; - if (ghdrLayoutResourceAction && !/^GH-CVM-MAIN-PROD-01:[0-9a-f]{64}:[1-9][0-9]{0,19}$/.test(resource)) return { ok: false, status: 400, error: "ghdr_layout_resource_required" }; - if (!immutableResourceAction && !repoPushResourceAction && !linkedNodeResourceAction && !ghdrLayoutResourceAction && resource) return { ok: false, status: 400, error: "resource_not_allowed_for_action" }; + if (!immutableResourceAction && !repoPushResourceAction && !linkedNodeResourceAction && resource) return { ok: false, status: 400, error: "resource_not_allowed_for_action" }; if (body.owner_notify !== undefined && typeof body.owner_notify !== "boolean") return { ok: false, status: 400, error: "invalid_owner_notify" }; if (body.owner_notify === true && provenance.system_entry !== "光湖语言人格系统当前实例") return { ok: false, status: 400, error: "owner_notify_requires_language_system_provenance" }; if (Object.values(provenance).some(Boolean) && (provenance.system_entry !== "光湖语言人格系统当前实例" || Object.values(provenance).some(item => !item || item.length > 120))) return { ok: false, status: 400, error: "invalid_instance_provenance" }; return { ok: true, request: { persona: { pid: personaId, name: personaName }, provenance, target, scope, action, allowedActions: actions[scope], description, resource } }; } -function validateGhdrLayoutRequest(plan, target, resource) { - if (target !== "GH-CVM-MAIN-PROD-01" - || !plan - || plan.schema !== "guanghu.ghdr-signed-layout-plan/v1" - || !plan.payload - || !Array.isArray(plan.signatures) - || plan.signatures.length !== 0) { - return { ok: false, error: "ghdr_layout_plan_invalid" }; - } - const payload = plan.payload; - const generation = Number(payload.generation); - if (payload.node_id !== target - || payload.provider !== "tencent_cloud" - || payload.region !== "ap-guangzhou" - || payload.system_disk !== "/dev/vda" - || payload.operation !== "install_native_ab" - || !Number.isSafeInteger(generation) - || generation < 1) { - return { ok: false, error: "ghdr_layout_binding_mismatch" }; - } - const canonical = JSON.stringify(payload); - const payloadSha256 = crypto.createHash("sha256").update(canonical).digest("hex"); - if (resource !== `${target}:${payloadSha256}:${generation}`) { - return { ok: false, error: "ghdr_layout_resource_mismatch" }; - } - return { ok: true, payload_sha256: payloadSha256, generation, resource }; -} function originLabel(order) { const value = order && order.provenance || {}; return value.software || value.model || value.instance ? `${value.software || "未知软件"} · ${value.model || "未知模型"} · ${value.instance || "当前实例"}` : "旧版工单未记录"; @@ -1452,11 +1291,4 @@ if (require.main === module) { createApp().listen(port, host, () => process.stdout.write(`lake-lamp-authz listening on ${host}:${port}\n`)); } -module.exports = { - createApp, - DEFAULT_ACTIONS, - SlidingWindowLimiter, - loadApprovers, - selectApprover, - validateGhdrLayoutRequest, -}; +module.exports = { createApp, DEFAULT_ACTIONS, SlidingWindowLimiter, loadApprovers, selectApprover }; diff --git a/server-tools/lake-lamp-authz/server.test.js b/server-tools/lake-lamp-authz/server.test.js index 7f68723..3b2cbcc 100644 --- a/server-tools/lake-lamp-authz/server.test.js +++ b/server-tools/lake-lamp-authz/server.test.js @@ -597,253 +597,6 @@ test("failed server actions return a durable diagnosis instead of making a perso } finally { fs.rmSync(dir, { recursive: true, force: true }); } }); -test("GHDR double signing is email-authorized and bound to one canonical layout digest", async () => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "lake-lamp-ghdr-sign-")); - const mapsDir = path.join(dir, "maps"); fs.mkdirSync(mapsDir); - fs.writeFileSync(path.join(mapsDir, "GH-CVM-MAIN-PROD-01.json"), JSON.stringify({ - node_id: "GH-CVM-MAIN-PROD-01", - })); - const now = Math.floor(Date.now() / 1000); - const plan = { - schema: "guanghu.ghdr-signed-layout-plan/v1", - payload: { - node_id: "GH-CVM-MAIN-PROD-01", - provider: "tencent_cloud", - region: "ap-guangzhou", - target_probe_sha256: "11".repeat(32), - system_disk: "/dev/vda", - disk_sectors: 104857600, - logical_sector_bytes: 512, - disk_identity_sha256: "22".repeat(32), - recovery_evidence_sha256: "55".repeat(32), - first_partition_lba: 2048, - generation: 1, - operation: "install_native_ab", - issued_at_unix: now, - expires_at_unix: now + 300, - slots: [ - { name: "A", lba_start: 34, sector_count: 29, image_sha256: "33".repeat(32) }, - { name: "B", lba_start: 73, sector_count: 29, image_sha256: "44".repeat(32) }, - ], - }, - signatures: [], - }; - const digest = crypto.createHash("sha256").update(JSON.stringify(plan.payload)).digest("hex"); - const resource = `GH-CVM-MAIN-PROD-01:${digest}:1`; - const signatures = [ - { node_id: "GH-CTRL-GZ-01", failure_domain: "tencent/ap-guangzhou/BS-GZ-006", public_key_hex: "aa".repeat(32), signature_hex: "bb".repeat(64) }, - { node_id: "GH-CTRL-SG-01", failure_domain: "tencent/ap-singapore/ZY-SG-006", public_key_hex: "cc".repeat(32), signature_hex: "dd".repeat(64) }, - ]; - const calls = []; - try { - await withServer(async ({ base, mail }) => { - const malformed = await fetch(`${base}/api/public/workorders`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - persona_id: "ICE-GL-ZY001", - target: "GH-CVM-MAIN-PROD-01", - scope: "native-recovery", - action: "sign-native-layout-plan", - resource: "GH-CVM-MAIN-PROD-01:not-a-digest:1", - }), - }); - assert.equal(malformed.status, 400); - assert.equal((await malformed.json()).error, "ghdr_layout_resource_required"); - - const requested = await fetch(`${base}/api/public/workorders`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - persona_id: "ICE-GL-ZY001", - target: "GH-CVM-MAIN-PROD-01", - scope: "native-recovery", - action: "sign-native-layout-plan", - resource, - }), - }); - const order = await requested.json(); - await fetch(`${base}${new URL(order.request_url).pathname.replace("/authz", "")}`, ownerForm()); - await fetch(`${base}${new URL(mail[0].approvalUrl).pathname.replace("/authz", "")}`, { method: "POST" }); - const session = await (await fetch(`${base}/api/workorders/${order.workorder_id}/claim`, { - method: "POST", - headers: { authorization: `Bearer ${order.claim_token}` }, - })).json(); - const common = { - persona_id: "ICE-GL-ZY001", - target: "GH-CVM-MAIN-PROD-01", - scope: "native-recovery", - }; - const map = await (await fetch(`${base}/api/navigation-map/read`, { - method: "POST", - headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, - body: JSON.stringify(common), - })).json(); - await fetch(`${base}/api/navigation-map/ack`, { - method: "POST", - headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, - body: JSON.stringify({ ...common, map_hash: map.map_hash }), - }); - - const changed = structuredClone(plan); - changed.payload.generation = 2; - const refused = await fetch(`${base}/api/ghdr/sign-layout`, { - method: "POST", - headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, - body: JSON.stringify({ ...common, resource, plan: changed }), - }); - assert.equal(refused.status, 400); - assert.equal((await refused.json()).error, "ghdr_layout_resource_mismatch"); - - const signed = await fetch(`${base}/api/ghdr/sign-layout`, { - method: "POST", - headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, - body: JSON.stringify({ ...common, resource, plan }), - }); - assert.equal(signed.status, 200); - const payload = await signed.json(); - assert.equal(payload.signatures.length, 2); - assert.equal(payload.receipt.diagnostic_code, "ghdr_layout_double_signature_succeeded"); - assert.deepEqual(calls, [{ - plan, - binding: { - ok: true, - payload_sha256: digest, - generation: 1, - resource, - }, - workorderId: order.workorder_id, - authorizer: "test-authorizer", - }]); - }, { - mapsDir, - mapStateFile: path.join(dir, "acks.json"), - targets: ["GH-CVM-MAIN-PROD-01"], - actions: { "native-recovery": ["read-navigation-map", "sign-native-layout-plan"] }, - getGhdrAuthorizer: () => "test-authorizer", - signGhdrPlan: async request => { - calls.push(request); - return { ok: true, signatures }; - }, - }); - } finally { - fs.rmSync(dir, { recursive: true, force: true }); - } -}); - -test("GHDR controller transport endpoints expose jobs and public authority without private keys", async () => { - const calls = []; - await withServer(async ({ base }) => { - const publicResponse = await fetch(`${base}/api/ghdr/authorizer-public-key`); - assert.equal(publicResponse.status, 200); - const publicPayload = await publicResponse.json(); - assert.equal(publicPayload.binding.algorithm, "Ed25519"); - assert.equal(publicPayload.binding.public_key_sha256, "ab".repeat(32)); - assert.doesNotMatch(JSON.stringify(publicPayload), /PRIVATE KEY/); - - const pollBody = { request: { schema: "poll" }, request_signature_hex: "11" }; - const poll = await fetch(`${base}/api/ghdr/controllers/poll`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(pollBody), - }); - assert.equal(poll.status, 200); - assert.equal((await poll.json()).job.job_id, "job-1"); - - const resultBody = { - request: { schema: "result" }, - request_signature_hex: "22", - signature: { signature_hex: "33" }, - }; - const result = await fetch(`${base}/api/ghdr/controllers/result`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(resultBody), - }); - assert.equal(result.status, 403); - assert.equal((await result.json()).error, "controller_result_refused"); - assert.deepEqual(calls, [ - ["poll", pollBody], - ["submit", resultBody], - ]); - }, { - getGhdrAuthorizer: () => ({ - publicBinding: () => ({ - schema: "guanghu.ghdr-authorizer-public-binding/v1", - authorizer_id: "JD-FD-PRIMARY-LAKE-LAMP", - algorithm: "Ed25519", - public_key_pem: "-----BEGIN PUBLIC KEY-----\nTEST\n-----END PUBLIC KEY-----\n", - public_key_sha256: "ab".repeat(32), - }), - }), - getGhdrControllerBroker: () => ({ - poll: body => { - calls.push(["poll", body]); - return { ok: true, job: { job_id: "job-1" } }; - }, - submit: body => { - calls.push(["submit", body]); - return { ok: false, error: "controller_result_refused" }; - }, - }), - }); -}); - -test("GHDR layout signing refuses a Guanghu Router approval in place of email approval", async () => { - const now = Math.floor(Date.now() / 1000); - const plan = { - schema: "guanghu.ghdr-signed-layout-plan/v1", - payload: { - node_id: "GH-CVM-MAIN-PROD-01", - provider: "tencent_cloud", - region: "ap-guangzhou", - target_probe_sha256: "11".repeat(32), - system_disk: "/dev/vda", - disk_sectors: 104857600, - logical_sector_bytes: 512, - disk_identity_sha256: "22".repeat(32), - recovery_evidence_sha256: "55".repeat(32), - first_partition_lba: 2048, - generation: 1, - operation: "install_native_ab", - issued_at_unix: now, - expires_at_unix: now + 300, - slots: [ - { name: "A", lba_start: 34, sector_count: 29, image_sha256: "33".repeat(32) }, - { name: "B", lba_start: 73, sector_count: 29, image_sha256: "44".repeat(32) }, - ], - }, - signatures: [], - }; - const digest = crypto.createHash("sha256").update(JSON.stringify(plan.payload)).digest("hex"); - const resource = `GH-CVM-MAIN-PROD-01:${digest}:1`; - await withServer(async ({ base }) => { - const response = await fetch(`${base}/api/ghdr/sign-layout`, { - method: "POST", - headers: { - authorization: "Bearer router-approved-session", - "content-type": "application/json", - }, - body: JSON.stringify({ - persona_id: "ICE-GL-ZY001", - target: "GH-CVM-MAIN-PROD-01", - scope: "native-recovery", - resource, - plan, - }), - }); - assert.equal(response.status, 403); - assert.equal((await response.json()).error, "ghdr_email_approval_required"); - }, { - manager: { - verifySession: () => ({ - ok: true, - session: { approvalChannel: "guanghu_router" }, - }), - }, - }); -}); - test("deployment is dispatched only by an explicit approved second signal", async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "lake-lamp-deploy-dispatch-")); const mapsDir = path.join(dir, "maps"); fs.mkdirSync(mapsDir); diff --git a/server-tools/lake-lamp-authz/workorder-manager.js b/server-tools/lake-lamp-authz/workorder-manager.js index 0616558..361e517 100644 --- a/server-tools/lake-lamp-authz/workorder-manager.js +++ b/server-tools/lake-lamp-authz/workorder-manager.js @@ -40,7 +40,6 @@ class WorkOrderManager { approvalHash: "", claimHash: hash(claimToken), emailSentAt: 0, - approvalChannel: "", state: "pending", claimed: false, }); @@ -99,7 +98,6 @@ class WorkOrderManager { const order = this.workorders.get(inspected.order.id); order.state = "approved"; order.approvedAt = now; - order.approvalChannel = "email"; order.approvalHash = ""; order.handoffHash = ""; this.persist(); @@ -136,7 +134,6 @@ class WorkOrderManager { } order.state = "approved"; order.approvedAt = now; - order.approvalChannel = "guanghu_router"; order.approvalHash = ""; order.handoffHash = ""; this.persist(); @@ -160,7 +157,6 @@ class WorkOrderManager { actions: order.allowedActions || [order.action], resource: order.resource || "", authorizerId: order.authorizerId || "", - approvalChannel: order.approvalChannel || "legacy_unspecified", createdAt: now, expiresAt: now + this.sessionTtl, maxExpiresAt: now + this.maxSessionLifetime, diff --git a/server-tools/lake-lamp-authz/workorder-manager.test.js b/server-tools/lake-lamp-authz/workorder-manager.test.js index a558af6..6f8d725 100644 --- a/server-tools/lake-lamp-authz/workorder-manager.test.js +++ b/server-tools/lake-lamp-authz/workorder-manager.test.js @@ -31,10 +31,6 @@ test("approval link is single use and the session is claimed once", () => { const claimed = manager.claim(created.id, created.claimToken, 104); assert.equal(claimed.ok, true); assert.equal(claimed.expiresIn, 3600); - assert.equal( - manager.verifySession(claimed.sessionToken, persona, "JD-FD-PRIMARY", "server-login", "read-navigation-map", 106).session.approvalChannel, - "email", - ); assert.equal(manager.claim(created.id, created.claimToken, 105).ok, false); assert.equal(manager.verifySession(claimed.sessionToken, persona, "JD-FD-PRIMARY", "server-login", "read-navigation-map", 106).ok, true); assert.equal(manager.verifySession(claimed.sessionToken, persona, "JD-FD-PRIMARY", "server-login", "inspect-services", 106).ok, true); @@ -173,12 +169,7 @@ test("a broadcast console lists and approves only its bound pending cards", () = assert.equal(manager.approveById(first.id, "awen", 102).reason, "authorizer_mismatch"); assert.equal(manager.approveById(first.id, "owner", 102).ok, true); assert.equal(manager.pendingForApprover("owner", 103).length, 0); - const claimed = manager.claim(first.id, first.claimToken, 104); - assert.equal(claimed.ok, true); - assert.equal( - manager.verifySession(claimed.sessionToken, persona, "JD-FD-PRIMARY", "server-login", "read-navigation-map", 105).session.approvalChannel, - "guanghu_router", - ); + assert.equal(manager.claim(first.id, first.claimToken, 104).ok, true); }); test("session cannot switch an approved architecture resource", () => {