diff --git a/deployment/receipts/GH-HOLOLAKE-NODE-POSSESSION-PROOF-20260810-001.json b/deployment/receipts/GH-HOLOLAKE-NODE-POSSESSION-PROOF-20260810-001.json deleted file mode 100644 index 6818ff6a6..000000000 --- a/deployment/receipts/GH-HOLOLAKE-NODE-POSSESSION-PROOF-20260810-001.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "schema": "guanghu.hololake-stage-delivery-receipt/v1", - "receipt_id": "GH-HOLOLAKE-NODE-POSSESSION-PROOF-20260810-001", - "development_id": "DEV-20260809-007", - "issued_at": "2026-08-10T00:10:25Z", - "source": { - "repository": "REPO-014", - "branch": "main", - "implementation_commit": "64062fadaced14ac4e180a7f75fa6ad27b413dc5", - "implementation_tree": "40bf0f0aa7331bf5d365d67fc6457b38d4b87ca7" - }, - "stage": { - "id": "NODE-POSSESSION-PROOF-003", - "state": "IMPLEMENTED_AND_LOCALLY_DEPLOYED", - "acceptance": [ - "challenge_is_created_only_from_an_in_process_cryptographically_verified_registration", - "challenge_binds_account_claim_domain_node_type_fingerprint_nonce_and_expiry", - "response_ed25519_public_key_spki_sha256_matches_the_registered_fingerprint", - "challenge_is_single_use_and_expires_within_60_seconds", - "unknown_fields_wrong_keys_replay_expiry_and_copied_provenance_fail_closed", - "domain_handoff_is_not_requested_without_current_node_private_key_possession" - ] - }, - "verification": { - "targeted_tests": "38_PASS", - "desktop_tests": "84_PASS", - "knowledge_base_production_build": "PASS", - "desktop_production_build": "PASS", - "ghnqg": "GHNQG_PASS_100", - "ghnqg_receipt_sha256": "aaa5fbbe0d37d0dbd6ee442beac69bf8dc1bc583093e165202c8d7bc0018cdb3", - "candidate_codesign": "PASS_DEEP_STRICT", - "installed_codesign": "PASS_DEEP_STRICT", - "installed_app_asar_sha256": "870ec9afb0f62f9917d968cfe68aa564f97510716ddc4788b71985a4d44823a3", - "installed_executable_sha256": "c423c2db1708579189814d02f86f474db723bce836e8beb9ba449f2e90ca1287", - "installed_runtime_pid": 40029, - "notarization": "SKIPPED_OPTIONS_UNAVAILABLE" - }, - "recovery": { - "previous_desktop_bundle": "/Users/bingshuolingdianyuanhe/Documents/Codex/2026-08-09/new-chat/work/DEV-20260809-007/desktop-backup-node-possession-20260810-0811/HoloLake Era.app" - }, - "reality_boundary": { - "repo_012_main": "34e1949a113be8d4871e61c094f2ae06e0bc5a45", - "production_node_registration_registry": "NOT_EXISTS_0", - "production_node_keystore_bridge": "NOT_EXISTS_0", - "production_node_possession_receipt": "NOT_EXISTS_0", - "production_domain_session": "NOT_EXISTS_0", - "production_behavior": "NO_REGISTRY_MEANS_NO_CLAIM_REQUEST_AND_NO_POSSESSION_CHALLENGE" - } -} diff --git a/product-source/guanghu-knowledge-base/server/domain-access-orchestrator.test.ts b/product-source/guanghu-knowledge-base/server/domain-access-orchestrator.test.ts index e2e53afe6..6157958c3 100644 --- a/product-source/guanghu-knowledge-base/server/domain-access-orchestrator.test.ts +++ b/product-source/guanghu-knowledge-base/server/domain-access-orchestrator.test.ts @@ -4,7 +4,6 @@ import { createHash, generateKeyPairSync, sign } from 'node:crypto'; import { domainManifestSigningBytes } from './domain-access.js'; import { DomainAccessOrchestrator } from './domain-access-orchestrator.js'; import { nodeRegistrationClaimSigningBytes } from './node-registration.js'; -import { nodePossessionChallengeSigningBytes } from './node-possession-proof.js'; import { NodeRegistrationSnapshotLoader } from './node-registration-snapshot.js'; import { TrustedSignerSnapshotLoader } from './trusted-signer-snapshot.js'; @@ -19,11 +18,6 @@ const SIGNER_REGISTRY_URL = `https://guanghulab.com/code/bingshuo/guanghu-ice-he const NODE_REGISTRY_URL = `https://guanghulab.com/code/bingshuo/guanghu-ice-heart/raw/commit/${COMMIT}/routing/node-registration-endpoints.json`; const manifestKeys = generateKeyPairSync('ed25519'); const registrationKeys = generateKeyPairSync('ed25519'); -const nodeKeys = generateKeyPairSync('ed25519'); -const nodePublicKeyPem = nodeKeys.publicKey.export({ format: 'pem', type: 'spki' }).toString(); -const nodeKeyFingerprint = createHash('sha256') - .update(nodeKeys.publicKey.export({ format: 'der', type: 'spki' })) - .digest('hex'); function anchor() { return { @@ -124,7 +118,7 @@ function validRegistrationClaim() { expiresAt: NOW + 60_000, issuedAt: NOW - 1_000, nodeId: NODE_ID, - nodeKeyFingerprint, + nodeKeyFingerprint: 'c'.repeat(64), nodeType: NODE_TYPE, schema: 'gh-aios.node-registration-claim/v1' as const, signerId: 'GH-NODE-REG-SIGNER-001', @@ -174,16 +168,6 @@ function validHandoff() { const loggedOutIdentity = { async read() { return { accountId: '', accountVerified: false, nodeId: '' }; } }; const loggedInIdentity = { async read() { return { accountId: ACCOUNT_ID, accountVerified: true, nodeId: NODE_ID }; } }; const validRegistrationSource = { async read() { return validRegistrationClaim(); } }; -const validPossessionSource = { - async prove(challenge: Parameters[0]) { - return { - challengeId: challenge.challengeId, - publicKeyPem: nodePublicKeyPem, - schema: 'gh-aios.node-possession-response/v1', - signature: sign(null, nodePossessionChallengeSigningBytes(challenge), nodeKeys.privateKey).toString('base64'), - }; - }, -}; test('projects both trust-source states without asking for claims or handoff before login', async () => { let claimReads = 0; @@ -193,7 +177,6 @@ test('projects both trust-source states without asking for claims or handoff bef registrationLoader(), loggedOutIdentity, { async read() { claimReads += 1; return null; } }, - validPossessionSource, { async read() { handoffReads += 1; return null; } }, ); @@ -217,7 +200,6 @@ test('a signed node registration without a handoff remains identity-only', async observedEndpointId = request.endpoint.endpointId; return validRegistrationClaim(); } }, - validPossessionSource, { async read() { return null; } }, ); @@ -230,24 +212,6 @@ test('a signed node registration without a handoff remains identity-only', async assert.ok(status.blockers.includes('verified_domain_manifest_missing')); }); -test('a signed registration without a fresh node-key proof cannot reach the handoff route', async () => { - let handoffReads = 0; - const orchestrator = new DomainAccessOrchestrator( - signerLoader(), - registrationLoader(), - loggedInIdentity, - validRegistrationSource, - { async prove() { return null; } }, - { async read() { handoffReads += 1; return null; } }, - ); - - const status = await orchestrator.domainAccess(DOMAIN_ID, NODE_TYPE, NOW); - - assert.equal(status.stage, 'login-required'); - assert.ok(status.blockers.includes('verified_node_possession_missing')); - assert.equal(handoffReads, 0); -}); - test('a code-channel account cannot supply registration and never contacts a handoff route', async () => { let handoffReads = 0; const orchestrator = new DomainAccessOrchestrator( @@ -255,7 +219,6 @@ test('a code-channel account cannot supply registration and never contacts a han registrationLoader(), loggedInIdentity, { async read() { return null; } }, - validPossessionSource, { async read() { handoffReads += 1; return validHandoff() as never; } }, ); @@ -272,7 +235,6 @@ test('current registries, signed node claim and verified handoff project runtime registrationLoader(), loggedInIdentity, validRegistrationSource, - validPossessionSource, { async read() { return { handoff: validHandoff(), @@ -297,7 +259,6 @@ test('source failures are reduced to safe status without raw details', async () unavailableRegistration, loggedInIdentity, { async read() { claimReads += 1; throw new Error('secret claim detail'); } }, - { async prove() { throw new Error('secret possession detail'); } }, { async read() { throw new Error('secret handoff detail'); } }, ); @@ -321,7 +282,6 @@ test('a degraded signer may preserve handoff verification while node registratio registrationLoader(), loggedInIdentity, validRegistrationSource, - validPossessionSource, { async read() { return { handoff: validHandoff(), @@ -348,7 +308,6 @@ test('a degraded node-registration registry cannot authorize a claim', async () currentNodeLoader, loggedInIdentity, { async read() { claimReads += 1; return validRegistrationClaim(); } }, - validPossessionSource, { async read() { handoffReads += 1; return null; } }, ); diff --git a/product-source/guanghu-knowledge-base/server/domain-access-orchestrator.ts b/product-source/guanghu-knowledge-base/server/domain-access-orchestrator.ts index ceeb2ae53..1790bad57 100644 --- a/product-source/guanghu-knowledge-base/server/domain-access-orchestrator.ts +++ b/product-source/guanghu-knowledge-base/server/domain-access-orchestrator.ts @@ -19,11 +19,6 @@ import { type NodeRegistrationSnapshotReceipt, } from './node-registration-snapshot.js'; import type { NodeRegistrationClaimSource } from './node-registration-client.js'; -import { - createNodePossessionChallenge, - verifyNodePossessionResponse, - type NodePossessionProofSource, -} from './node-possession-proof.js'; const IDENTIFIER_PATTERN = /^[A-Z0-9][A-Z0-9._:-]{1,159}$/; @@ -86,13 +81,10 @@ export class DomainAccessOrchestrator { private readonly nodeRegistrationSnapshots: NodeRegistrationSnapshotLoader, private readonly identities: DomainIdentitySource, private readonly nodeRegistrations: NodeRegistrationClaimSource, - private readonly nodePossessions: NodePossessionProofSource, private readonly handoffs: DomainRuntimeHandoffSource, - private readonly clock: () => number = Date.now, ) {} - async domainAccess(domainId: string, nodeType: DomainNodeType, at?: number): Promise> { - let now = at ?? this.clock(); + async domainAccess(domainId: string, nodeType: DomainNodeType, now = Date.now()): Promise> { if (!IDENTIFIER_PATTERN.test(domainId)) throw new Error('domain_access_domain_invalid'); if (nodeType !== 'local-terminal' && nodeType !== 'cloud-resident') throw new Error('domain_access_node_type_invalid'); const [snapshot, nodeRegistrationSnapshot, identity] = await Promise.all([ @@ -101,7 +93,6 @@ export class DomainAccessOrchestrator { this.identities.read(domainId, nodeType).then(safeIdentity, () => safeIdentity(null)), ]); let nodeRegistrationVerified = false; - let nodePossessionVerified = false; if (identity.accountVerified && nodeRegistrationSnapshot.receipt.status === 'CURRENT') { try { const loaded = assertLoadedNodeRegistrationSnapshot(nodeRegistrationSnapshot); @@ -115,20 +106,13 @@ export class DomainAccessOrchestrator { nodeType, })); if (claim) { - const registration = verifyNodeRegistrationClaim(claim, { + verifyNodeRegistrationClaim(claim, { accountId: identity.accountId, domainId, nodeId: identity.nodeId, nodeType, }, endpoint, now); nodeRegistrationVerified = true; - const challenge = createNodePossessionChallenge(registration, now); - const response = await this.nodePossessions.prove(challenge); - if (response) { - if (at === undefined) now = this.clock(); - verifyNodePossessionResponse(response, challenge, registration, now); - nodePossessionVerified = true; - } } } } catch { @@ -139,12 +123,11 @@ export class DomainAccessOrchestrator { accountVerified: identity.accountVerified, domainId, nodeId: identity.nodeId, - nodePossessionVerified, nodeRegistrationVerified, nodeType, }; - if (identity.accountVerified && nodeRegistrationVerified && nodePossessionVerified) { + if (identity.accountVerified && nodeRegistrationVerified) { let candidate: DomainRuntimeHandoffCandidate | null = null; try { candidate = await this.handoffs.read(domainId, identity.nodeId, nodeType); diff --git a/product-source/guanghu-knowledge-base/server/domain-access.test.ts b/product-source/guanghu-knowledge-base/server/domain-access.test.ts index 7f2949b81..43879c59b 100644 --- a/product-source/guanghu-knowledge-base/server/domain-access.test.ts +++ b/product-source/guanghu-knowledge-base/server/domain-access.test.ts @@ -147,21 +147,21 @@ function snapshotLoader(readRegistry: () => unknown) { } test('a local workspace remains available without claiming domain runtime access', () => { - const status = evaluateDomainAccess({ accountVerified: false, domainId: 'DOM-FIFTH-0001', nodeId: '', nodePossessionVerified: false, nodeRegistrationVerified: false, nodeType: NODE_TYPE }, NOW); + const status = evaluateDomainAccess({ accountVerified: false, domainId: 'DOM-FIFTH-0001', nodeId: '', nodeRegistrationVerified: false, nodeType: NODE_TYPE }, NOW); assert.equal(status.localWorkspaceAllowed, true); assert.equal(status.runtimeReady, false); assert.equal(status.stage, 'login-required'); }); test('a code-channel account cannot substitute for verified node registration', () => { - const status = evaluateDomainAccess({ accountVerified: true, domainId: 'DOM-FIFTH-0001', nodeId: 'JD-FD-PRIMARY', nodePossessionVerified: false, nodeRegistrationVerified: false, nodeType: NODE_TYPE }, NOW); + const status = evaluateDomainAccess({ accountVerified: true, domainId: 'DOM-FIFTH-0001', nodeId: 'JD-FD-PRIMARY', nodeRegistrationVerified: false, nodeType: NODE_TYPE }, NOW); assert.equal(status.stage, 'login-required'); assert.equal(status.blockers.includes('account_identity_missing'), false); assert.ok(status.blockers.includes('verified_node_registration_missing')); }); test('a separately verified node is still not a domain runtime session', () => { - const status = evaluateDomainAccess({ accountVerified: true, domainId: 'DOM-FIFTH-0001', nodeId: 'JD-FD-PRIMARY', nodePossessionVerified: true, nodeRegistrationVerified: true, nodeType: NODE_TYPE }, NOW); + const status = evaluateDomainAccess({ accountVerified: true, domainId: 'DOM-FIFTH-0001', nodeId: 'JD-FD-PRIMARY', nodeRegistrationVerified: true, nodeType: NODE_TYPE }, NOW); assert.equal(status.stage, 'identity-verified'); assert.deepEqual(status.blockers, [ 'verified_domain_manifest_missing', @@ -170,26 +170,12 @@ test('a separately verified node is still not a domain runtime session', () => { ]); }); -test('a signed registration without a current private-key proof remains login-required', () => { - const status = evaluateDomainAccess({ - accountVerified: true, - domainId: 'DOM-FIFTH-0001', - nodeId: 'JD-FD-PRIMARY', - nodePossessionVerified: false, - nodeRegistrationVerified: true, - nodeType: NODE_TYPE, - }, NOW); - assert.equal(status.stage, 'login-required'); - assert.ok(status.blockers.includes('verified_node_possession_missing')); -}); - test('runtime access requires matching manifest, scoped capability and online receipt', () => { const handoff = parseDomainAccessHandoff(validHandoff, 'DOM-FIFTH-0001', 'LOCAL-001', NODE_TYPE, trustedSigner, NOW); const status = evaluateDomainAccess({ accountVerified: true, domainId: 'DOM-FIFTH-0001', nodeId: 'LOCAL-001', - nodePossessionVerified: true, nodeRegistrationVerified: true, nodeType: NODE_TYPE, ...handoff, @@ -217,7 +203,6 @@ test('the runtime handoff entry resolves its signer only from a loaded snapshot' accountVerified: true, domainId: 'DOM-FIFTH-0001', nodeId: 'LOCAL-001', - nodePossessionVerified: true, nodeRegistrationVerified: true, nodeType: NODE_TYPE, ...verified.handoff, @@ -285,7 +270,6 @@ test('mismatched or expired evidence fails closed', () => { domainId: 'DOM-FIFTH-0001', manifest: verifiedHandoff.manifest, nodeId: 'LOCAL-001', - nodePossessionVerified: true, nodeRegistrationVerified: true, nodeType: NODE_TYPE, sessionCapability: { capabilityId: 'CAP-001', domainId: 'DOM-FIFTH-0001', expiresAt: NOW, nodeId: 'LOCAL-001', nodeType: NODE_TYPE, scopes: ['domain:enter'] }, @@ -359,7 +343,6 @@ test('copying verified-looking fields cannot bypass the in-process verifier boun verifierReceipt: { ...verifiedHandoff.manifest.verifierReceipt }, }, nodeId: 'LOCAL-001', - nodePossessionVerified: true, nodeRegistrationVerified: true, nodeType: NODE_TYPE, sessionCapability: verifiedHandoff.sessionCapability, diff --git a/product-source/guanghu-knowledge-base/server/domain-access.ts b/product-source/guanghu-knowledge-base/server/domain-access.ts index 3223430c7..491765469 100644 --- a/product-source/guanghu-knowledge-base/server/domain-access.ts +++ b/product-source/guanghu-knowledge-base/server/domain-access.ts @@ -79,7 +79,6 @@ export interface DomainAccessEvidence { domainId: string; manifest?: DomainManifestEvidence; nodeId: string; - nodePossessionVerified: boolean; nodeRegistrationVerified: boolean; nodeType: DomainNodeType; sessionCapability?: DomainSessionCapability; @@ -245,7 +244,6 @@ export function evaluateDomainAccess(evidence: DomainAccessEvidence, now = Date. const blockers: string[] = []; if (!evidence.accountVerified) blockers.push('account_identity_missing'); if (!evidence.nodeRegistrationVerified || !evidence.nodeId) blockers.push('verified_node_registration_missing'); - if (!evidence.nodePossessionVerified) blockers.push('verified_node_possession_missing'); const manifest = evidence.manifest; const verifier = manifest?.verifierReceipt; if (!manifest @@ -287,10 +285,6 @@ export function evaluateDomainAccess(evidence: DomainAccessEvidence, now = Date. nodeId: evidence.nodeId, nodeType: evidence.nodeType, runtimeReady, - stage: runtimeReady - ? 'runtime-ready' - : evidence.accountVerified && evidence.nodeRegistrationVerified && evidence.nodePossessionVerified && Boolean(evidence.nodeId) - ? 'identity-verified' - : 'login-required', + stage: runtimeReady ? 'runtime-ready' : evidence.accountVerified && evidence.nodeRegistrationVerified && Boolean(evidence.nodeId) ? 'identity-verified' : 'login-required', }; } diff --git a/product-source/guanghu-knowledge-base/server/node-possession-proof.test.ts b/product-source/guanghu-knowledge-base/server/node-possession-proof.test.ts deleted file mode 100644 index 58b5618b2..000000000 --- a/product-source/guanghu-knowledge-base/server/node-possession-proof.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import { createHash, generateKeyPairSync, sign } from 'node:crypto'; -import { - createNodePossessionChallenge, - nodePossessionChallengeSigningBytes, - 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 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 responseFor(challenge: ReturnType) { - return { - challengeId: challenge.challengeId, - publicKeyPem: nodePublicKeyPem, - schema: 'gh-aios.node-possession-response/v1', - signature: sign(null, nodePossessionChallengeSigningBytes(challenge), nodeKeys.privateKey).toString('base64'), - }; -} - -test('verifies a fresh proof from the key fingerprint bound into the signed registration', () => { - const registration = verifiedRegistration(); - const challenge = createNodePossessionChallenge( - registration, - NOW, - 'NODE-CHALLENGE-001', - 'A'.repeat(43), - ); - const response = responseFor(challenge); - assert.equal(verifyNodePossessionResponse(response, challenge, registration, NOW + 1), true); - assert.throws(() => verifyNodePossessionResponse(response, challenge, registration, NOW + 2), /proof_invalid/); -}); - -test('rejects a forged challenge that was not issued by the verifier', () => { - const registration = verifiedRegistration(); - const challenge = { - ...createNodePossessionChallenge(registration, NOW, 'NODE-CHALLENGE-001', 'A'.repeat(43)), - nonce: 'B'.repeat(43), - }; - assert.throws(() => verifyNodePossessionResponse(responseFor(challenge), challenge, registration, NOW + 1), /proof_invalid/); -}); - -test('rejects a response signed by a different node key', () => { - const registration = verifiedRegistration(); - const challenge = createNodePossessionChallenge(registration, NOW, 'NODE-CHALLENGE-001', 'A'.repeat(43)); - const otherKeys = generateKeyPairSync('ed25519'); - const response = { - challengeId: challenge.challengeId, - publicKeyPem: otherKeys.publicKey.export({ format: 'pem', type: 'spki' }).toString(), - schema: 'gh-aios.node-possession-response/v1', - signature: sign(null, nodePossessionChallengeSigningBytes(challenge), otherKeys.privateKey).toString('base64'), - }; - assert.throws(() => verifyNodePossessionResponse(response, challenge, registration, NOW + 1), /proof_invalid/); -}); - -test('rejects expired, replayed-to-another-challenge and unknown-field responses', () => { - const registration = verifiedRegistration(); - const first = createNodePossessionChallenge(registration, NOW, 'NODE-CHALLENGE-001', 'A'.repeat(43)); - const second = createNodePossessionChallenge(registration, NOW, 'NODE-CHALLENGE-002', 'B'.repeat(43)); - assert.throws(() => verifyNodePossessionResponse(responseFor(first), second, registration, NOW + 1), /proof_invalid/); - assert.throws(() => verifyNodePossessionResponse({ ...responseFor(first), extra: true }, first, registration, NOW + 1), /proof_invalid/); - assert.throws(() => verifyNodePossessionResponse(responseFor(first), first, registration, NOW + 60_000), /proof_invalid/); -}); - -test('does not accept a structurally copied registration as verified provenance', () => { - const copied = { ...verifiedRegistration() }; - assert.throws(() => createNodePossessionChallenge(copied, NOW, 'NODE-CHALLENGE-001', 'A'.repeat(43)), /proof_invalid/); -}); diff --git a/product-source/guanghu-knowledge-base/server/node-possession-proof.ts b/product-source/guanghu-knowledge-base/server/node-possession-proof.ts deleted file mode 100644 index 2cd568222..000000000 --- a/product-source/guanghu-knowledge-base/server/node-possession-proof.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { - createHash, - createPublicKey, - randomBytes, - randomUUID, - verify as verifySignature, -} from 'node:crypto'; -import { - assertVerifiedNodeRegistration, - type VerifiedNodeRegistration, -} from './node-registration.js'; - -const CHALLENGE_SCHEMA = 'gh-aios.node-possession-challenge/v1' as const; -const RESPONSE_SCHEMA = 'gh-aios.node-possession-response/v1' as const; -const CHALLENGE_LIFETIME_MS = 60_000; -const IDENTIFIER_PATTERN = /^[A-Z0-9][A-Z0-9._:-]{1,159}$/; -const NONCE_PATTERN = /^[A-Za-z0-9_-]{43}$/; -const issuedChallenges = new WeakSet(); -const consumedChallenges = new WeakSet(); - -export interface NodePossessionChallenge { - accountId: string; - challengeId: string; - claimId: string; - domainId: string; - expiresAt: number; - issuedAt: number; - nodeId: string; - nodeKeyFingerprint: string; - nodeType: VerifiedNodeRegistration['nodeType']; - nonce: string; - schema: typeof CHALLENGE_SCHEMA; -} - -export interface NodePossessionResponse { - challengeId: string; - publicKeyPem: string; - schema: typeof RESPONSE_SCHEMA; - signature: string; -} - -export interface NodePossessionProofSource { - prove(challenge: Readonly): Promise; -} - -function invalidProof(): never { - throw new Error('node_possession_proof_invalid'); -} - -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]); -} - -export function nodePossessionChallengeSigningBytes(challenge: NodePossessionChallenge): Buffer { - return Buffer.from(JSON.stringify({ - accountId: challenge.accountId, - challengeId: challenge.challengeId, - claimId: challenge.claimId, - domainId: challenge.domainId, - expiresAt: challenge.expiresAt, - issuedAt: challenge.issuedAt, - nodeId: challenge.nodeId, - nodeKeyFingerprint: challenge.nodeKeyFingerprint, - nodeType: challenge.nodeType, - nonce: challenge.nonce, - schema: challenge.schema, - }), 'utf8'); -} - -export function createNodePossessionChallenge( - registration: VerifiedNodeRegistration, - now = Date.now(), - challengeId = `NODE-CHALLENGE-${randomUUID().toUpperCase()}`, - nonce = randomBytes(32).toString('base64url'), -): Readonly { - try { - assertVerifiedNodeRegistration(registration); - } catch { - invalidProof(); - } - if (!Number.isSafeInteger(now) - || now < registration.issuedAt - || now >= registration.expiresAt - || !IDENTIFIER_PATTERN.test(challengeId) - || !NONCE_PATTERN.test(nonce)) invalidProof(); - const expiresAt = Math.min(now + CHALLENGE_LIFETIME_MS, registration.expiresAt); - if (expiresAt <= now) invalidProof(); - const challenge = Object.freeze({ - accountId: registration.accountId, - challengeId, - claimId: registration.claimId, - domainId: registration.domainId, - expiresAt, - issuedAt: now, - nodeId: registration.nodeId, - nodeKeyFingerprint: registration.nodeKeyFingerprint, - nodeType: registration.nodeType, - nonce, - schema: CHALLENGE_SCHEMA, - }); - issuedChallenges.add(challenge); - return challenge; -} - -export function verifyNodePossessionResponse( - input: unknown, - challenge: NodePossessionChallenge, - registration: VerifiedNodeRegistration, - now = Date.now(), -): true { - try { - assertVerifiedNodeRegistration(registration); - } catch { - invalidProof(); - } - if (!issuedChallenges.has(challenge) - || consumedChallenges.has(challenge) - || !Number.isSafeInteger(now) - || now < challenge.issuedAt - || now >= challenge.expiresAt - || registration.expiresAt <= now - || challenge.accountId !== registration.accountId - || challenge.claimId !== registration.claimId - || challenge.domainId !== registration.domainId - || challenge.nodeId !== registration.nodeId - || challenge.nodeKeyFingerprint !== registration.nodeKeyFingerprint - || challenge.nodeType !== registration.nodeType - || !isRecord(input) - || !hasExactKeys(input, ['challengeId', 'publicKeyPem', 'schema', 'signature']) - || input.schema !== RESPONSE_SCHEMA - || input.challengeId !== challenge.challengeId - || typeof input.publicKeyPem !== 'string' - || input.publicKeyPem.length > 4096 - || input.publicKeyPem.includes('PRIVATE KEY') - || typeof input.signature !== 'string') invalidProof(); - - try { - const publicKey = createPublicKey({ key: input.publicKeyPem, format: 'pem' }); - if (publicKey.asymmetricKeyType !== 'ed25519') invalidProof(); - const fingerprint = createHash('sha256') - .update(publicKey.export({ format: 'der', type: 'spki' })) - .digest('hex'); - if (fingerprint !== registration.nodeKeyFingerprint) invalidProof(); - const signature = Buffer.from(input.signature, 'base64'); - if (signature.length !== 64 || signature.toString('base64') !== input.signature) invalidProof(); - if (!verifySignature(null, nodePossessionChallengeSigningBytes(challenge), publicKey, signature)) invalidProof(); - } catch { - invalidProof(); - } - consumedChallenges.add(challenge); - return true; -} diff --git a/product-source/guanghu-knowledge-base/server/node-registration.ts b/product-source/guanghu-knowledge-base/server/node-registration.ts index 7f4c572b1..0cbb002c9 100644 --- a/product-source/guanghu-knowledge-base/server/node-registration.ts +++ b/product-source/guanghu-knowledge-base/server/node-registration.ts @@ -11,7 +11,6 @@ const SHA256_PATTERN = /^[a-f0-9]{64}$/; const MAX_CLAIM_LIFETIME_MS = 24 * 60 * 60 * 1000; const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000; const registeredEndpoints = new WeakSet(); -const verifiedClaims = new WeakSet(); export interface NodeRegistrationEndpoint { algorithm: 'Ed25519'; @@ -58,13 +57,6 @@ export interface VerifiedNodeRegistration extends NodeRegistrationClaimPayload { verifiedAt: number; } -export function assertVerifiedNodeRegistration( - claim: VerifiedNodeRegistration, -): VerifiedNodeRegistration { - if (!verifiedClaims.has(claim)) invalidClaim(); - return claim; -} - export interface NodeRegistrationLookup { domainId: string; nodeType: DomainNodeType; @@ -278,11 +270,9 @@ export function verifyNodeRegistrationClaim( } catch { invalidClaim(); } - const verified = Object.freeze({ + return Object.freeze({ ...payload, signature: input.signature, verifiedAt: now, }); - verifiedClaims.add(verified); - return verified; } diff --git a/product-source/hololake-desktop/electron/main.ts b/product-source/hololake-desktop/electron/main.ts index 9de9395c8..d132b1763 100644 --- a/product-source/hololake-desktop/electron/main.ts +++ b/product-source/hololake-desktop/electron/main.ts @@ -361,11 +361,6 @@ const domainAccessOrchestrator = new DomainAccessOrchestrator( }, }, nodeRegistrationClaims, - { - // 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; }, - }, { async read() { // 当前灯塔尚未发布签名运行体交接端点。保持关闭,不从账号身份推导运行体权限。 diff --git a/product-source/hololake-platform/architecture/HOLOLAKE-NODE-REGISTRATION-CONTRACT-20260810.md b/product-source/hololake-platform/architecture/HOLOLAKE-NODE-REGISTRATION-CONTRACT-20260810.md index ab72b5965..976dc9c05 100644 --- a/product-source/hololake-platform/architecture/HOLOLAKE-NODE-REGISTRATION-CONTRACT-20260810.md +++ b/product-source/hololake-platform/architecture/HOLOLAKE-NODE-REGISTRATION-CONTRACT-20260810.md @@ -58,24 +58,3 @@ 当前公共锚点仍未登记端点,所以该传输在生产运行中不可达,也不会发出请求;实现存在不等于 端点服务、节点登记或域运行体已经存在。 - -## 节点私钥持有证明 · 阶段 003 - -签名登记声明只证明灯塔曾登记“节点编号—节点公钥指纹”,不能证明当前发起登录的终端此刻 -仍持有对应私钥。HoloLake 必须在声明验证以后创建一次性 -`gh-aios.node-possession-challenge/v1`:精确绑定账号、登记声明、域、节点编号、节点类型、 -公钥指纹、随机 256 位 nonce、挑战编号、签发时间与到期时间;有效期最多 60 秒且不得超过 -登记声明自身到期时间。 - -节点返回 `gh-aios.node-possession-response/v1`,只包含挑战编号、Ed25519 公钥和对完整挑战 -规范字节的签名。验证器必须确认: - -1. 挑战确由当前 HoloLake 进程创建,调用方复制相同字段不能伪造来源; -2. 响应公钥的 SPKI DER SHA-256 与签名登记声明中的 `nodeKeyFingerprint` 完全一致; -3. Ed25519 签名、挑战编号、账号、声明、域、节点、类型、nonce 和有效期全部匹配; -4. 未知字段、错误公钥、过期挑战、跨挑战重放、复制的登记对象或任一解析错误全部失败关闭。 - -只有“账号验证 + 签名节点登记 + 当前节点私钥持有证明”三者同时成立,客户端才允许请求域 -运行体交接。当前桌面尚无生产节点密钥库桥,正式运行源固定返回无证明,因此即使未来出现一份 -有效登记声明,也不能越过 `verified_node_possession_missing`。代码验证器存在不等于节点密钥、 -生产密钥库桥、灯塔会话签发或域连接已经存在。 diff --git a/product-source/hololake-platform/architecture/README.md b/product-source/hololake-platform/architecture/README.md index 37241d6cc..f7979dc43 100644 --- a/product-source/hololake-platform/architecture/README.md +++ b/product-source/hololake-platform/architecture/README.md @@ -8,7 +8,7 @@ - [`HOLOLAKE-INTENT-REASONING-MAP-20260809.md`](HOLOLAKE-INTENT-REASONING-MAP-20260809.md):当前产品意图、纠正、思维推理与因果链。 - [`HOLOLAKE-SYSTEM-ARCHITECTURE-20260809.md`](HOLOLAKE-SYSTEM-ARCHITECTURE-20260809.md):`HLP-CURRENT-ARCH-001` 当前完整系统蓝图,覆盖 GH-AIOS 灯塔、五域独立入口、光湖本源域、节点登录与光湖桥。 - [`HOLOLAKE-DESKTOP-0.8.0-CURRENT-CAPABILITY-GAP-20260809.md`](HOLOLAKE-DESKTOP-0.8.0-CURRENT-CAPABILITY-GAP-20260809.md):桌面 0.8.0 已观察能力与最终规划的逐项差距;同时登记线上产品源码仍为 0.4.6 的分叉事实。 -- [`HOLOLAKE-NODE-REGISTRATION-CONTRACT-20260810.md`](HOLOLAKE-NODE-REGISTRATION-CONTRACT-20260810.md):账号、签名节点登记与当前节点私钥持有证明分离后的端点注册表、无凭据声明读取、60 秒一次性 Ed25519 挑战、失败关闭和当前生产缺口验收基线。 +- [`HOLOLAKE-NODE-REGISTRATION-CONTRACT-20260810.md`](HOLOLAKE-NODE-REGISTRATION-CONTRACT-20260810.md):账号验证与节点登记分离后的签名声明、端点注册表、无凭据声明读取传输、失败关闭和当前生产缺口验收基线。 - [`HOLOLAKE-LANGUAGE-PERSONA-OS.md`](HOLOLAKE-LANGUAGE-PERSONA-OS.md):HoloLake 作为 AI 语言人格驱动操作系统的当前正式产品定位、操作系统映射、原生应用边界与实现缺口。 - [`HOLOLAKE-INTENT-REASONING-MAP-20260807.md`](HOLOLAKE-INTENT-REASONING-MAP-20260807.md):2026-08-07 历史意图层,继续保存形成过程;不再作为当前默认入口。