diff --git a/deployment/receipts/GH-HOLOLAKE-NODE-KEYSTORE-BRIDGE-20260810-001.json b/deployment/receipts/GH-HOLOLAKE-NODE-KEYSTORE-BRIDGE-20260810-001.json deleted file mode 100644 index 04d1a3a59..000000000 --- a/deployment/receipts/GH-HOLOLAKE-NODE-KEYSTORE-BRIDGE-20260810-001.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "schema": "gh-aios.deployment-receipt/v1", - "receiptId": "GH-HOLOLAKE-NODE-KEYSTORE-BRIDGE-20260810-001", - "developmentId": "DEV-20260809-007", - "stage": "NODE-KEYSTORE-BRIDGE-001", - "recordedAt": "2026-08-10T01:45:34+08:00", - "implementation": { - "commit": "b4d6e7a4f9eea81fc7fd3e373afe5f7df6ff9af6", - "tree": "ecd5833f212b18d19d69306c3195e55a8d0bb9ec", - "contract": "product-source/hololake-platform/architecture/HOLOLAKE-NODE-KEYSTORE-BRIDGE-CONTRACT-20260810.md" - }, - "verification": { - "targetedTests": "18/18 PASS_100", - "desktopAndServerTests": "101/101 PASS_100", - "typescriptNoEmit": "PASS_100", - "desktopBuild": "PASS_100", - "ghnqg": "PASS_100", - "ghnqgReceiptSha256": "a2bcf31bd5ff08d7c0dcf182bfbbe187c60eb830bb0eeb3fc416f5aa57b9b804" - }, - "desktopDeployment": { - "path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake Era.app", - "version": "0.8.0", - "bundleId": "com.guanghu.hololake-desktop", - "teamId": "825A9L3G7Q", - "codesign": "PASS_100_DEEP_STRICT", - "notarization": "NOT_PERFORMED", - "appAsarSha256": "e68d9851d553dc2f926673a42148d1e590c616890168821234db2e6eceb6cbc4", - "executableSha256": "f5b075ddbeea3d16fe0ce0bf114a33d40ff860896d54c8fca2ae92e9bdcb4daf", - "runtimePid": 90137, - "health": "ONLINE_GIT_ENGINE_127.0.0.1:3890", - "recoveryCopy": "/tmp/HoloLake Era.pre-stage-006.app" - }, - "productionBoundary": { - "nodeKeystoreRecord": "NOT_ENROLLED_0", - "productionKeyCreated": false, - "keychainOrPrivacyPromptConfirmed": false, - "nodeRegistrationEndpoint": "NOT_EXISTS_0", - "domainRuntimeHandoffEndpoint": "NOT_EXISTS_0", - "domainSession": "NOT_EXISTS_0" - }, - "result": "PASS_100_LOCAL_IMPLEMENTATION_DEPLOYED_PRODUCTION_IDENTITY_ABSENT" -} diff --git a/product-source/guanghu-knowledge-base/server/node-keystore-bridge.test.ts b/product-source/guanghu-knowledge-base/server/node-keystore-bridge.test.ts deleted file mode 100644 index c2ed5293d..000000000 --- a/product-source/guanghu-knowledge-base/server/node-keystore-bridge.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import { createHash, generateKeyPairSync, sign } from 'node:crypto'; -import { EncryptedNodeKeyStoreProofSource, type NodeKeyStoreAdapter } from './node-keystore-bridge.js'; -import { createNodePossessionChallenge, verifyNodePossessionResponse } from './node-possession-proof.js'; -import { - nodeRegistrationClaimSigningBytes, - parseNodeRegistrationEndpointRegistry, - resolveNodeRegistrationEndpoint, - verifyNodeRegistrationClaim, -} from './node-registration.js'; - -const NOW = 1_786_291_200_000; -const DOMAIN_ID = 'DOM-FIFTH-0001'; -const NODE_ID = 'LOCAL-001'; -const registrationKeys = generateKeyPairSync('ed25519'); -const nodeKeys = generateKeyPairSync('ed25519'); -const nodePublicKeyPem = nodeKeys.publicKey.export({ format: 'pem', type: 'spki' }).toString(); -const nodePrivateKeyPkcs8 = nodeKeys.privateKey.export({ format: 'der', type: 'pkcs8' }); -const fingerprint = createHash('sha256') - .update(nodeKeys.publicKey.export({ format: 'der', type: 'spki' })) - .digest('hex'); - -function verifiedRegistration() { - const registry = parseNodeRegistrationEndpointRegistry({ - endpoints: [{ - algorithm: 'Ed25519', - domainIds: [DOMAIN_ID], - endpointId: 'GH-NODE-REG-001', - nodeTypes: ['local-terminal'], - publicKeyPem: registrationKeys.publicKey.export({ format: 'pem', type: 'spki' }).toString(), - signerId: 'GH-NODE-REG-SIGNER-001', - status: 'ACTIVE', - url: 'https://guanghulab.com/api/ai/v1/node-registrations/claims', - }], - registryId: 'GH-AIOS-NODE-REGISTRATION-ENDPOINTS-001', - schema: 'gh-aios.node-registration-endpoints/v1', - state: 'CURRENT', - version: '1.0.0', - }, { - repositoryId: 'REPO-012', - sourceCommit: 'a'.repeat(40), - sourceUrl: `https://guanghulab.com/code/bingshuo/guanghu-ice-heart/raw/commit/${'a'.repeat(40)}/routing/node-registration-endpoints.json`, - }); - const endpoint = resolveNodeRegistrationEndpoint(registry, { domainId: DOMAIN_ID, nodeType: 'local-terminal' }); - assert.ok(endpoint); - const payload = { - accountId: 'bingshuo', - claimId: 'NODE-CLAIM-001', - domainId: DOMAIN_ID, - endpointId: endpoint.endpointId, - expiresAt: NOW + 120_000, - issuedAt: NOW - 1_000, - nodeId: NODE_ID, - nodeKeyFingerprint: fingerprint, - nodeType: 'local-terminal' as const, - schema: 'gh-aios.node-registration-claim/v1' as const, - signerId: endpoint.signerId, - }; - return verifyNodeRegistrationClaim({ - ...payload, - signature: sign(null, nodeRegistrationClaimSigningBytes(payload), registrationKeys.privateKey).toString('base64'), - }, { - accountId: payload.accountId, - domainId: DOMAIN_ID, - nodeId: NODE_ID, - nodeType: 'local-terminal', - }, endpoint, NOW); -} - -function keyRecord(overrides: Record = {}) { - return { - encryptedPrivateKeyPkcs8: Buffer.from('encrypted-test-record').toString('base64'), - nodeId: NODE_ID, - nodeKeyFingerprint: fingerprint, - nodeType: 'local-terminal', - publicKeyPem: nodePublicKeyPem, - schema: 'gh-aios.node-keystore-record/v1', - ...overrides, - }; -} - -function adapter(overrides: Partial = {}): NodeKeyStoreAdapter { - return { - async decryptPrivateKeyPkcs8() { return Buffer.from(nodePrivateKeyPkcs8); }, - isAvailable() { return true; }, - async readRecord() { return keyRecord(); }, - ...overrides, - }; -} - -function challenge(id = 'NODE-CHALLENGE-KEYSTORE-001') { - return createNodePossessionChallenge(verifiedRegistration(), NOW, id, 'A'.repeat(43)); -} - -test('signs only an active challenge with the enrolled matching Ed25519 key', async () => { - const issued = challenge(); - const source = new EncryptedNodeKeyStoreProofSource(adapter(), () => NOW + 1); - const response = await source.prove(issued); - assert.ok(response); - assert.deepEqual(Object.keys(response).sort(), ['challengeId', 'publicKeyPem', 'schema', 'signature']); - assert.ok(verifyNodePossessionResponse(response, issued, verifiedRegistration(), NOW + 2)); -}); - -test('returns no proof when storage is unavailable or no enrollment exists', async () => { - const issued = challenge('NODE-CHALLENGE-KEYSTORE-002'); - assert.equal(await new EncryptedNodeKeyStoreProofSource(adapter({ isAvailable: () => false }), () => NOW + 1).prove(issued), null); - assert.equal(await new EncryptedNodeKeyStoreProofSource(adapter({ readRecord: async () => null }), () => NOW + 1).prove(issued), null); -}); - -test('rejects copied challenges, unknown record fields and node binding mismatches', async () => { - const issued = challenge('NODE-CHALLENGE-KEYSTORE-003'); - assert.equal(await new EncryptedNodeKeyStoreProofSource(adapter(), () => NOW + 1).prove({ ...issued }), null); - assert.equal(await new EncryptedNodeKeyStoreProofSource(adapter({ readRecord: async () => keyRecord({ extra: true }) }), () => NOW + 1).prove(issued), null); - assert.equal(await new EncryptedNodeKeyStoreProofSource(adapter({ readRecord: async () => keyRecord({ nodeId: 'OTHER-NODE' }) }), () => NOW + 1).prove(issued), null); -}); - -test('rejects malformed encryption payloads and mismatched private keys', async () => { - const issued = challenge('NODE-CHALLENGE-KEYSTORE-004'); - assert.equal(await new EncryptedNodeKeyStoreProofSource(adapter({ readRecord: async () => keyRecord({ encryptedPrivateKeyPkcs8: '***' }) }), () => NOW + 1).prove(issued), null); - const other = generateKeyPairSync('ed25519').privateKey.export({ format: 'der', type: 'pkcs8' }); - assert.equal(await new EncryptedNodeKeyStoreProofSource(adapter({ decryptPrivateKeyPkcs8: async () => Buffer.from(other) }), () => NOW + 1).prove(issued), null); -}); - -test('wipes the decrypted temporary key bytes after signing', async () => { - const issued = challenge('NODE-CHALLENGE-KEYSTORE-005'); - const decrypted = Buffer.from(nodePrivateKeyPkcs8); - const source = new EncryptedNodeKeyStoreProofSource(adapter({ decryptPrivateKeyPkcs8: async () => decrypted }), () => NOW + 1); - assert.ok(await source.prove(issued)); - assert.ok(decrypted.every(byte => byte === 0)); -}); diff --git a/product-source/guanghu-knowledge-base/server/node-keystore-bridge.ts b/product-source/guanghu-knowledge-base/server/node-keystore-bridge.ts deleted file mode 100644 index 168f6a470..000000000 --- a/product-source/guanghu-knowledge-base/server/node-keystore-bridge.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { - createHash, - createPrivateKey, - createPublicKey, - sign, -} from 'node:crypto'; -import { - assertActiveNodePossessionChallenge, - nodePossessionChallengeSigningBytes, - type NodePossessionChallenge, - type NodePossessionProofSource, - type NodePossessionResponse, -} from './node-possession-proof.js'; - -const RECORD_SCHEMA = 'gh-aios.node-keystore-record/v1' as const; -const RESPONSE_SCHEMA = 'gh-aios.node-possession-response/v1' as const; -const IDENTIFIER_PATTERN = /^[A-Z0-9][A-Z0-9._:-]{1,159}$/; -const FINGERPRINT_PATTERN = /^[a-f0-9]{64}$/; -const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; - -interface NodeKeyRecord { - encryptedPrivateKeyPkcs8: string; - nodeId: string; - nodeKeyFingerprint: string; - nodeType: 'local-terminal' | 'cloud-resident'; - publicKeyPem: string; - schema: typeof RECORD_SCHEMA; -} - -export interface NodeKeyStoreAdapter { - decryptPrivateKeyPkcs8(encrypted: string): Promise; - isAvailable(): boolean; - readRecord(): Promise; -} - -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} - -function hasExactKeys(record: Record, keys: string[]): boolean { - const expected = [...keys].sort(); - const actual = Object.keys(record).sort(); - return actual.length === expected.length && actual.every((key, index) => key === expected[index]); -} - -function parseRecord(input: unknown): NodeKeyRecord | null { - if (!isRecord(input) - || !hasExactKeys(input, [ - 'encryptedPrivateKeyPkcs8', - 'nodeId', - 'nodeKeyFingerprint', - 'nodeType', - 'publicKeyPem', - 'schema', - ]) - || input.schema !== RECORD_SCHEMA - || typeof input.nodeId !== 'string' - || !IDENTIFIER_PATTERN.test(input.nodeId) - || (input.nodeType !== 'local-terminal' && input.nodeType !== 'cloud-resident') - || typeof input.nodeKeyFingerprint !== 'string' - || !FINGERPRINT_PATTERN.test(input.nodeKeyFingerprint) - || typeof input.publicKeyPem !== 'string' - || input.publicKeyPem.length > 4096 - || input.publicKeyPem.includes('PRIVATE KEY') - || typeof input.encryptedPrivateKeyPkcs8 !== 'string' - || input.encryptedPrivateKeyPkcs8.length < 4 - || input.encryptedPrivateKeyPkcs8.length > 65_536 - || !BASE64_PATTERN.test(input.encryptedPrivateKeyPkcs8)) return null; - return input as unknown as NodeKeyRecord; -} - -function publicKeyFingerprint(publicKeyPem: string): string | null { - try { - const publicKey = createPublicKey({ key: publicKeyPem, format: 'pem' }); - if (publicKey.asymmetricKeyType !== 'ed25519') return null; - return createHash('sha256') - .update(publicKey.export({ format: 'der', type: 'spki' })) - .digest('hex'); - } catch { - return null; - } -} - -export class EncryptedNodeKeyStoreProofSource implements NodePossessionProofSource { - constructor( - private readonly adapter: NodeKeyStoreAdapter, - private readonly clock: () => number = Date.now, - ) {} - - async prove(challenge: Readonly): Promise | null> { - let privateKeyPkcs8: Buffer | null = null; - try { - assertActiveNodePossessionChallenge(challenge, this.clock()); - if (!this.adapter.isAvailable()) return null; - const record = parseRecord(await this.adapter.readRecord()); - if (!record - || record.nodeId !== challenge.nodeId - || record.nodeType !== challenge.nodeType - || record.nodeKeyFingerprint !== challenge.nodeKeyFingerprint - || publicKeyFingerprint(record.publicKeyPem) !== record.nodeKeyFingerprint) return null; - - privateKeyPkcs8 = await this.adapter.decryptPrivateKeyPkcs8(record.encryptedPrivateKeyPkcs8); - if (!privateKeyPkcs8 || privateKeyPkcs8.length === 0 || privateKeyPkcs8.length > 16_384) return null; - const privateKey = createPrivateKey({ key: privateKeyPkcs8, format: 'der', type: 'pkcs8' }); - if (privateKey.asymmetricKeyType !== 'ed25519') return null; - const derivedPublicKey = createPublicKey(privateKey); - const derivedFingerprint = createHash('sha256') - .update(derivedPublicKey.export({ format: 'der', type: 'spki' })) - .digest('hex'); - if (derivedFingerprint !== record.nodeKeyFingerprint) return null; - - return Object.freeze({ - challengeId: challenge.challengeId, - publicKeyPem: record.publicKeyPem, - schema: RESPONSE_SCHEMA, - signature: sign(null, nodePossessionChallengeSigningBytes(challenge), privateKey).toString('base64'), - }); - } catch { - return null; - } finally { - privateKeyPkcs8?.fill(0); - } - } -} diff --git a/product-source/guanghu-knowledge-base/server/node-possession-proof.ts b/product-source/guanghu-knowledge-base/server/node-possession-proof.ts index bc7bd64b8..3315a6700 100644 --- a/product-source/guanghu-knowledge-base/server/node-possession-proof.ts +++ b/product-source/guanghu-knowledge-base/server/node-possession-proof.ts @@ -81,18 +81,6 @@ export function nodePossessionChallengeSigningBytes(challenge: NodePossessionCha }), 'utf8'); } -export function assertActiveNodePossessionChallenge( - challenge: NodePossessionChallenge, - now = Date.now(), -): NodePossessionChallenge { - if (!issuedChallenges.has(challenge) - || consumedChallenges.has(challenge) - || !Number.isSafeInteger(now) - || now < challenge.issuedAt - || now >= challenge.expiresAt) invalidProof(); - return challenge; -} - export function createNodePossessionChallenge( registration: VerifiedNodeRegistration, now = Date.now(), diff --git a/product-source/hololake-desktop/electron/main.ts b/product-source/hololake-desktop/electron/main.ts index 87cf8d2be..266509202 100644 --- a/product-source/hololake-desktop/electron/main.ts +++ b/product-source/hololake-desktop/electron/main.ts @@ -22,7 +22,6 @@ import { DomainAccessOrchestrator } from '../../guanghu-knowledge-base/server/do import { TrustedSignerSnapshotLoader } from '../../guanghu-knowledge-base/server/trusted-signer-snapshot.js'; import { NodeRegistrationSnapshotLoader } from '../../guanghu-knowledge-base/server/node-registration-snapshot.js'; import { HttpNodeRegistrationClaimSource } from '../../guanghu-knowledge-base/server/node-registration-client.js'; -import { EncryptedNodeKeyStoreProofSource } from '../../guanghu-knowledge-base/server/node-keystore-bridge.js'; import { DomainRuntimeHandoffSnapshotLoader } from '../../guanghu-knowledge-base/server/domain-runtime-handoff-snapshot.js'; import { AnchoredDomainRuntimeHandoffSource, @@ -41,7 +40,6 @@ const KB_REPO_PATH = path.join(DATA_DIR, 'knowledge-base'); const MODEL_CONFIG_PATH = path.join(app.getPath('userData'), 'model-config.json'); const SERVER_AUTH_PATH = path.join(app.getPath('userData'), 'server-auth.json'); const SERVER_PROFILES_PATH = path.join(app.getPath('userData'), 'server-profiles.json'); -const NODE_KEYSTORE_PATH = path.join(app.getPath('userData'), 'node-keystore.json'); const AGENT_STATE_PATH = path.join(app.getPath('userData'), 'agent-conversations.json'); const GIT_ASKPASS_PATH = path.join(app.getPath('userData'), 'hololake-git-askpass.sh'); interface ServerProfileDefinition { @@ -368,37 +366,6 @@ const domainRuntimeHandoffs = new AnchoredDomainRuntimeHandoffSource( new HttpDomainRuntimeHandoffTransport(), ); -const nodeKeyStoreProofs = new EncryptedNodeKeyStoreProofSource({ - isAvailable() { - return safeStorage.isEncryptionAvailable(); - }, - async readRecord() { - try { - const stat = fs.statSync(NODE_KEYSTORE_PATH); - if (!stat.isFile() || (stat.mode & 0o077) !== 0) return null; - return JSON.parse(fs.readFileSync(NODE_KEYSTORE_PATH, 'utf8')) as unknown; - } catch { - return null; - } - }, - async decryptPrivateKeyPkcs8(encrypted) { - try { - if (!safeStorage.isEncryptionAvailable()) return null; - const ciphertext = Buffer.from(encrypted, 'base64'); - if (ciphertext.length === 0 || ciphertext.toString('base64') !== encrypted) return null; - const plaintextBase64 = safeStorage.decryptString(ciphertext); - const plaintext = Buffer.from(plaintextBase64, 'base64'); - if (plaintext.length === 0 || plaintext.toString('base64') !== plaintextBase64) { - plaintext.fill(0); - return null; - } - return plaintext; - } catch { - return null; - } - }, -}); - const domainAccessOrchestrator = new DomainAccessOrchestrator( trustedSignerSnapshots, nodeRegistrationSnapshots, @@ -421,7 +388,11 @@ const domainAccessOrchestrator = new DomainAccessOrchestrator( }, }, nodeRegistrationClaims, - nodeKeyStoreProofs, + { + // The production node-keystore bridge is not registered yet. Returning no + // proof keeps the domain gate closed even if a future signed claim appears. + async prove() { return null; }, + }, domainRuntimeHandoffs, ); diff --git a/product-source/hololake-platform/architecture/HOLOLAKE-NODE-KEYSTORE-BRIDGE-CONTRACT-20260810.md b/product-source/hololake-platform/architecture/HOLOLAKE-NODE-KEYSTORE-BRIDGE-CONTRACT-20260810.md deleted file mode 100644 index fd3d7b870..000000000 --- a/product-source/hololake-platform/architecture/HOLOLAKE-NODE-KEYSTORE-BRIDGE-CONTRACT-20260810.md +++ /dev/null @@ -1,44 +0,0 @@ -# HoloLake 节点密钥库桥契约 · 2026-08-10 - -> 阶段:`DEV-20260809-007 / NODE-KEYSTORE-BRIDGE-001` -> -> 上游:`HOLOLAKE-NODE-REGISTRATION-CONTRACT-20260810` 阶段 003 -> -> 状态:`IMPLEMENTATION_STAGE_ACCEPTANCE_BASELINE` - -## 验收标准 - -1. 正式域登录只读取本机明确登记的节点密钥记录;记录缺失、加密存储不可用、解密失败、 - 字段不完整或任一绑定不一致时返回无证明,禁止自动生成或自动登记生产节点身份。 -2. 节点密钥记录必须精确绑定 `nodeId`、`nodeType`、Ed25519 公钥与其 SPKI DER SHA-256 - 指纹;挑战中的节点编号、类型和指纹必须与记录完全一致。 -3. 私钥以 PKCS#8 DER 形式进入签名边界,只允许 Ed25519;实现必须从私钥重新导出公钥并 - 核对登记指纹,签名结束后立即覆写临时私钥字节。 -4. 桥对外只能返回 `gh-aios.node-possession-response/v1` 的挑战编号、公钥和签名;不得返回 - 私钥、解密结果、加密载荷、密钥路径或底层错误细节。 -5. 未知字段、复制的挑战对象、过期挑战、节点绑定错误、公私钥不配对、无效 Base64、错误 - 密钥类型及签名异常全部失败关闭为 `null`,不得抛出秘密相关错误进入产品投影。 -6. Electron 适配层只在密钥记录已经存在时读取;当前阶段不得创建真实生产密钥文件、不得 - 发起钥匙串或隐私确认,也不得把测试密钥安装到桌面数据目录。 -7. 当前正式数据目录没有已登记节点密钥记录,因此本阶段完成后的生产判断仍为 - `NODE_KEYSTORE_BRIDGE_NOT_ENROLLED_0`;代码可运行不等于生产节点身份、灯塔登记、域会话或 - 运行体连接已经存在。 - -## 本地记录边界 - -记录 schema 固定为 `gh-aios.node-keystore-record/v1`,只允许: - -- `nodeId`、`nodeType`; -- `nodeKeyFingerprint`、`publicKeyPem`; -- `encryptedPrivateKeyPkcs8`; -- `schema`。 - -加密私钥字段由宿主安全存储保护,明文是严格 Base64 的 PKCS#8 DER。记录文件本身必须以 -`0600` 权限保存,但本阶段只实现已有记录的读取与证明,不实现登记写入。 - -## 当前现实边界 - -- 当前公共锚点仍没有节点登记端点与域运行体交接端点映射; -- 当前桌面没有本阶段 schema 的生产节点密钥记录; -- 当前没有生产节点登记声明、节点证明、域会话能力或在线连接回执; -- 因而域入口继续停留在登录/登记门,这是正确的失败关闭结果。