Bind runtime handoff to node proof
This commit is contained in:
parent
ca67ba92d2
commit
46d19aeb45
7 changed files with 249 additions and 55 deletions
|
|
@ -23,6 +23,7 @@ import {
|
|||
createNodePossessionChallenge,
|
||||
verifyNodePossessionResponse,
|
||||
type NodePossessionProofSource,
|
||||
type VerifiedNodePossession,
|
||||
} from './node-possession-proof.js';
|
||||
|
||||
const IDENTIFIER_PATTERN = /^[A-Z0-9][A-Z0-9._:-]{1,159}$/;
|
||||
|
|
@ -46,7 +47,7 @@ export interface DomainRuntimeHandoffCandidate {
|
|||
}
|
||||
|
||||
export interface DomainRuntimeHandoffSource {
|
||||
read(domainId: string, nodeId: string, nodeType: DomainNodeType): Promise<DomainRuntimeHandoffCandidate | null>;
|
||||
read(possession: VerifiedNodePossession): Promise<DomainRuntimeHandoffCandidate | null>;
|
||||
}
|
||||
|
||||
export interface DomainAccessProjection {
|
||||
|
|
@ -102,6 +103,7 @@ export class DomainAccessOrchestrator {
|
|||
]);
|
||||
let nodeRegistrationVerified = false;
|
||||
let nodePossessionVerified = false;
|
||||
let possession: VerifiedNodePossession | null = null;
|
||||
if (identity.accountVerified && nodeRegistrationSnapshot.receipt.status === 'CURRENT') {
|
||||
try {
|
||||
const loaded = assertLoadedNodeRegistrationSnapshot(nodeRegistrationSnapshot);
|
||||
|
|
@ -126,7 +128,7 @@ export class DomainAccessOrchestrator {
|
|||
const response = await this.nodePossessions.prove(challenge);
|
||||
if (response) {
|
||||
if (at === undefined) now = this.clock();
|
||||
verifyNodePossessionResponse(response, challenge, registration, now);
|
||||
possession = verifyNodePossessionResponse(response, challenge, registration, now);
|
||||
nodePossessionVerified = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -144,10 +146,10 @@ export class DomainAccessOrchestrator {
|
|||
nodeType,
|
||||
};
|
||||
|
||||
if (identity.accountVerified && nodeRegistrationVerified && nodePossessionVerified) {
|
||||
if (identity.accountVerified && nodeRegistrationVerified && nodePossessionVerified && possession) {
|
||||
let candidate: DomainRuntimeHandoffCandidate | null = null;
|
||||
try {
|
||||
candidate = await this.handoffs.read(domainId, identity.nodeId, nodeType);
|
||||
candidate = await this.handoffs.read(possession);
|
||||
} catch {
|
||||
candidate = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,75 +1,163 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash, generateKeyPairSync, sign } from 'node:crypto';
|
||||
import { StrictDomainRuntimeHandoffSource } from './domain-runtime-handoff-source.js';
|
||||
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 = 'JD-FD-PRIMARY';
|
||||
const NODE_TYPE = 'cloud-resident' as const;
|
||||
const registrationKeys = generateKeyPairSync('ed25519');
|
||||
const nodeKeys = generateKeyPairSync('ed25519');
|
||||
const publicKeyPem = nodeKeys.publicKey.export({ format: 'pem', type: 'spki' }).toString();
|
||||
const fingerprint = createHash('sha256')
|
||||
.update(nodeKeys.publicKey.export({ format: 'der', type: 'spki' }))
|
||||
.digest('hex');
|
||||
|
||||
function response(overrides: Record<string, unknown> = {}) {
|
||||
function verifiedPossession() {
|
||||
const registry = parseNodeRegistrationEndpointRegistry({
|
||||
endpoints: [{
|
||||
algorithm: 'Ed25519',
|
||||
domainIds: [DOMAIN_ID],
|
||||
endpointId: 'GH-NODE-REG-001',
|
||||
nodeTypes: [NODE_TYPE],
|
||||
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: NODE_TYPE });
|
||||
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: NODE_TYPE,
|
||||
schema: 'gh-aios.node-registration-claim/v1' as const,
|
||||
signerId: endpoint.signerId,
|
||||
};
|
||||
const registration = verifyNodeRegistrationClaim({
|
||||
...payload,
|
||||
signature: sign(null, nodeRegistrationClaimSigningBytes(payload), registrationKeys.privateKey).toString('base64'),
|
||||
}, {
|
||||
accountId: payload.accountId,
|
||||
domainId: DOMAIN_ID,
|
||||
nodeId: NODE_ID,
|
||||
nodeType: NODE_TYPE,
|
||||
}, endpoint, NOW);
|
||||
const challenge = createNodePossessionChallenge(
|
||||
registration,
|
||||
NOW,
|
||||
'NODE-CHALLENGE-001',
|
||||
'A'.repeat(43),
|
||||
);
|
||||
const response = {
|
||||
challengeId: challenge.challengeId,
|
||||
publicKeyPem,
|
||||
schema: 'gh-aios.node-possession-response/v1' as const,
|
||||
signature: sign(null, nodePossessionChallengeSigningBytes(challenge), nodeKeys.privateKey).toString('base64'),
|
||||
};
|
||||
return verifyNodePossessionResponse(response, challenge, registration, NOW + 1);
|
||||
}
|
||||
|
||||
function response(requestId: string, overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
schema: 'gh-aios.domain-runtime-handoff-response/v1',
|
||||
handoff: { manifest: { digest: 'untrusted-until-orchestrator-verifies' } },
|
||||
request_id: requestId,
|
||||
signer_lookup: {
|
||||
repository_id: 'REPO-014',
|
||||
signer_id: 'GH-LIGHTHOUSE-001',
|
||||
},
|
||||
status: 'ISSUED',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('adapts one exact endpoint-neutral response without interpreting signed evidence', async () => {
|
||||
test('forwards one exact cryptographic authorization bundle without private material', async () => {
|
||||
const requests: unknown[] = [];
|
||||
const source = new StrictDomainRuntimeHandoffSource({
|
||||
async request(input) {
|
||||
requests.push(input);
|
||||
return response();
|
||||
return response(input.requestId);
|
||||
},
|
||||
});
|
||||
}, () => NOW + 2, () => 'HANDOFF-REQ-001');
|
||||
|
||||
const candidate = await source.read(DOMAIN_ID, NODE_ID, NODE_TYPE);
|
||||
const candidate = await source.read(verifiedPossession());
|
||||
|
||||
assert.deepEqual(requests, [{ domainId: DOMAIN_ID, nodeId: NODE_ID, nodeType: NODE_TYPE }]);
|
||||
assert.equal(requests.length, 1);
|
||||
assert.deepEqual(Object.keys(requests[0] as object).sort(), [
|
||||
'accountId', 'challenge', 'domainId', 'issuedAt', 'nodeId', 'nodeType',
|
||||
'registration', 'requestId', 'response', 'schema',
|
||||
]);
|
||||
assert.equal(JSON.stringify(requests[0]).includes('PRIVATE KEY'), false);
|
||||
assert.deepEqual(candidate, {
|
||||
handoff: { manifest: { digest: 'untrusted-until-orchestrator-verifies' } },
|
||||
signerLookup: { repositoryId: 'REPO-014', signerId: 'GH-LIGHTHOUSE-001' },
|
||||
});
|
||||
assert.equal(Object.isFrozen(candidate), true);
|
||||
assert.equal(Object.isFrozen(candidate?.signerLookup), true);
|
||||
});
|
||||
|
||||
test('rejects malformed, extended and self-authorizing response envelopes', async () => {
|
||||
test('binds the response to the generated request and rejects malformed envelopes', async () => {
|
||||
const samples = [
|
||||
null,
|
||||
response({ extra: true }),
|
||||
response({ schema: 'other/v1' }),
|
||||
response({ signer_lookup: { repository_id: 'REPO-014', signer_id: 'bad signer' } }),
|
||||
response({ signer_lookup: { repository_id: 'REPO-014', signer_id: 'GH-LIGHTHOUSE-001', public_key: 'forbidden' } }),
|
||||
response({ handoff: null }),
|
||||
response('OTHER-REQ'),
|
||||
response('HANDOFF-REQ-001', { extra: true }),
|
||||
response('HANDOFF-REQ-001', { schema: 'other/v1' }),
|
||||
response('HANDOFF-REQ-001', { signer_lookup: { repository_id: 'REPO-014', signer_id: 'bad signer' } }),
|
||||
response('HANDOFF-REQ-001', { signer_lookup: { repository_id: 'REPO-014', signer_id: 'GH-LIGHTHOUSE-001', public_key: 'forbidden' } }),
|
||||
response('HANDOFF-REQ-001', { handoff: null }),
|
||||
response('HANDOFF-REQ-001', { handoff: null, signer_lookup: null, status: 'NOT_AUTHORIZED' }),
|
||||
];
|
||||
let index = 0;
|
||||
const source = new StrictDomainRuntimeHandoffSource({
|
||||
async request() { return samples[index++]; },
|
||||
});
|
||||
|
||||
for (const _sample of samples) {
|
||||
assert.equal(await source.read(DOMAIN_ID, NODE_ID, NODE_TYPE), null);
|
||||
for (const sample of samples) {
|
||||
const source = new StrictDomainRuntimeHandoffSource({ async request() { return sample; } }, () => NOW + 2, () => 'HANDOFF-REQ-001');
|
||||
assert.equal(await source.read(verifiedPossession()), null);
|
||||
}
|
||||
});
|
||||
|
||||
test('fails closed on invalid request identity and transport errors without leaking details', async () => {
|
||||
test('rejects copied, expired and replayed possession authorizations before transport', async () => {
|
||||
let requests = 0;
|
||||
const possession = verifiedPossession();
|
||||
const source = new StrictDomainRuntimeHandoffSource({
|
||||
async request() {
|
||||
requests += 1;
|
||||
throw new Error('secret remote detail');
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(await source.read('bad domain', NODE_ID, NODE_TYPE), null);
|
||||
assert.equal(requests, 0);
|
||||
assert.equal(await source.read(DOMAIN_ID, NODE_ID, NODE_TYPE), null);
|
||||
assert.equal(requests, 1);
|
||||
assert.equal(await source.read(DOMAIN_ID, NODE_ID, 'invalid' as never), null);
|
||||
async request(input) { requests += 1; return response(input.requestId); },
|
||||
}, () => NOW + 2, () => 'HANDOFF-REQ-001');
|
||||
assert.ok(await source.read(possession));
|
||||
assert.equal(await source.read(possession), null);
|
||||
assert.equal(await source.read({ ...verifiedPossession() }), null);
|
||||
const expired = new StrictDomainRuntimeHandoffSource({
|
||||
async request(input) { requests += 1; return response(input.requestId); },
|
||||
}, () => NOW + 60_000, () => 'HANDOFF-REQ-002');
|
||||
assert.equal(await expired.read(verifiedPossession()), null);
|
||||
assert.equal(requests, 1);
|
||||
});
|
||||
|
||||
test('fails closed on transport errors without exposing remote details', async () => {
|
||||
const source = new StrictDomainRuntimeHandoffSource({
|
||||
async request() { throw new Error('secret remote detail'); },
|
||||
}, () => NOW + 2, () => 'HANDOFF-REQ-001');
|
||||
assert.equal(await source.read(verifiedPossession()), null);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,14 +2,31 @@ import type {
|
|||
DomainRuntimeHandoffCandidate,
|
||||
DomainRuntimeHandoffSource,
|
||||
} from './domain-access-orchestrator.js';
|
||||
import type { DomainNodeType } from './domain-access.js';
|
||||
import {
|
||||
assertVerifiedNodePossession,
|
||||
type NodePossessionChallenge,
|
||||
type NodePossessionResponse,
|
||||
type VerifiedNodePossession,
|
||||
} from './node-possession-proof.js';
|
||||
import type { VerifiedNodeRegistration } from './node-registration.js';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
const IDENTIFIER_PATTERN = /^[A-Z0-9][A-Z0-9._:-]{1,159}$/;
|
||||
const REQUEST_SCHEMA = 'gh-aios.domain-runtime-handoff-request/v1' as const;
|
||||
const RESPONSE_SCHEMA = 'gh-aios.domain-runtime-handoff-response/v1' as const;
|
||||
const consumedPossessions = new WeakSet<object>();
|
||||
|
||||
export interface DomainRuntimeHandoffRequest {
|
||||
accountId: string;
|
||||
challenge: Readonly<NodePossessionChallenge>;
|
||||
domainId: string;
|
||||
issuedAt: number;
|
||||
nodeId: string;
|
||||
nodeType: DomainNodeType;
|
||||
nodeType: VerifiedNodeRegistration['nodeType'];
|
||||
registration: Readonly<VerifiedNodeRegistration>;
|
||||
requestId: string;
|
||||
response: Readonly<NodePossessionResponse>;
|
||||
schema: typeof REQUEST_SCHEMA;
|
||||
}
|
||||
|
||||
export interface DomainRuntimeHandoffTransport {
|
||||
|
|
@ -26,19 +43,28 @@ function hasExactKeys(value: Record<string, unknown>, expected: readonly string[
|
|||
return actual.length === keys.length && actual.every((key, index) => key === keys[index]);
|
||||
}
|
||||
|
||||
function parseResponse(input: unknown): Readonly<DomainRuntimeHandoffCandidate> | null {
|
||||
function parseResponse(
|
||||
input: unknown,
|
||||
request: DomainRuntimeHandoffRequest,
|
||||
): Readonly<DomainRuntimeHandoffCandidate> | null {
|
||||
if (!isRecord(input)
|
||||
|| !hasExactKeys(input, ['handoff', 'schema', 'signer_lookup'])
|
||||
|| input.schema !== 'gh-aios.domain-runtime-handoff-response/v1'
|
||||
|| !isRecord(input.handoff)
|
||||
|| !hasExactKeys(input, ['handoff', 'request_id', 'schema', 'signer_lookup', 'status'])
|
||||
|| input.schema !== RESPONSE_SCHEMA
|
||||
|| input.request_id !== request.requestId
|
||||
|| (input.status !== 'ISSUED' && input.status !== 'NOT_AUTHORIZED')) {
|
||||
return null;
|
||||
}
|
||||
if (input.status === 'NOT_AUTHORIZED') {
|
||||
if (input.handoff !== null || input.signer_lookup !== null) return null;
|
||||
return null;
|
||||
}
|
||||
if (!isRecord(input.handoff)
|
||||
|| !isRecord(input.signer_lookup)
|
||||
|| !hasExactKeys(input.signer_lookup, ['repository_id', 'signer_id'])
|
||||
|| typeof input.signer_lookup.repository_id !== 'string'
|
||||
|| !IDENTIFIER_PATTERN.test(input.signer_lookup.repository_id)
|
||||
|| typeof input.signer_lookup.signer_id !== 'string'
|
||||
|| !IDENTIFIER_PATTERN.test(input.signer_lookup.signer_id)) {
|
||||
return null;
|
||||
}
|
||||
|| !IDENTIFIER_PATTERN.test(input.signer_lookup.signer_id)) return null;
|
||||
return Object.freeze({
|
||||
handoff: input.handoff,
|
||||
signerLookup: Object.freeze({
|
||||
|
|
@ -49,13 +75,37 @@ function parseResponse(input: unknown): Readonly<DomainRuntimeHandoffCandidate>
|
|||
}
|
||||
|
||||
export class StrictDomainRuntimeHandoffSource implements DomainRuntimeHandoffSource {
|
||||
constructor(private readonly transport: DomainRuntimeHandoffTransport) {}
|
||||
constructor(
|
||||
private readonly transport: DomainRuntimeHandoffTransport,
|
||||
private readonly clock: () => number = Date.now,
|
||||
private readonly requestId: () => string = () => `HANDOFF-REQ-${randomUUID().toUpperCase()}`,
|
||||
) {}
|
||||
|
||||
async read(domainId: string, nodeId: string, nodeType: DomainNodeType): Promise<Readonly<DomainRuntimeHandoffCandidate> | null> {
|
||||
if (!IDENTIFIER_PATTERN.test(domainId) || !IDENTIFIER_PATTERN.test(nodeId)) return null;
|
||||
if (nodeType !== 'local-terminal' && nodeType !== 'cloud-resident') return null;
|
||||
async read(possession: VerifiedNodePossession): Promise<Readonly<DomainRuntimeHandoffCandidate> | null> {
|
||||
try {
|
||||
return parseResponse(await this.transport.request(Object.freeze({ domainId, nodeId, nodeType })));
|
||||
const verified = assertVerifiedNodePossession(possession);
|
||||
if (consumedPossessions.has(verified)) return null;
|
||||
const issuedAt = this.clock();
|
||||
const requestId = this.requestId();
|
||||
if (!Number.isSafeInteger(issuedAt)
|
||||
|| issuedAt < verified.verifiedAt
|
||||
|| issuedAt >= verified.challenge.expiresAt
|
||||
|| issuedAt >= verified.registration.expiresAt
|
||||
|| !IDENTIFIER_PATTERN.test(requestId)) return null;
|
||||
const request = Object.freeze({
|
||||
accountId: verified.registration.accountId,
|
||||
challenge: verified.challenge,
|
||||
domainId: verified.registration.domainId,
|
||||
issuedAt,
|
||||
nodeId: verified.registration.nodeId,
|
||||
nodeType: verified.registration.nodeType,
|
||||
registration: verified.registration,
|
||||
requestId,
|
||||
response: verified.response,
|
||||
schema: REQUEST_SCHEMA,
|
||||
});
|
||||
consumedPossessions.add(verified);
|
||||
return parseResponse(await this.transport.request(request), request);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,10 @@ test('verifies a fresh proof from the key fingerprint bound into the signed regi
|
|||
'A'.repeat(43),
|
||||
);
|
||||
const response = responseFor(challenge);
|
||||
assert.equal(verifyNodePossessionResponse(response, challenge, registration, NOW + 1), true);
|
||||
const possession = verifyNodePossessionResponse(response, challenge, registration, NOW + 1);
|
||||
assert.equal(possession.registration, registration);
|
||||
assert.equal(possession.challenge, challenge);
|
||||
assert.deepEqual(possession.response, response);
|
||||
assert.throws(() => verifyNodePossessionResponse(response, challenge, registration, NOW + 2), /proof_invalid/);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const IDENTIFIER_PATTERN = /^[A-Z0-9][A-Z0-9._:-]{1,159}$/;
|
|||
const NONCE_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
||||
const issuedChallenges = new WeakSet<object>();
|
||||
const consumedChallenges = new WeakSet<object>();
|
||||
const verifiedPossessions = new WeakSet<object>();
|
||||
|
||||
export interface NodePossessionChallenge {
|
||||
accountId: string;
|
||||
|
|
@ -43,6 +44,13 @@ export interface NodePossessionProofSource {
|
|||
prove(challenge: Readonly<NodePossessionChallenge>): Promise<unknown | null>;
|
||||
}
|
||||
|
||||
export interface VerifiedNodePossession {
|
||||
challenge: Readonly<NodePossessionChallenge>;
|
||||
registration: Readonly<VerifiedNodeRegistration>;
|
||||
response: Readonly<NodePossessionResponse>;
|
||||
verifiedAt: number;
|
||||
}
|
||||
|
||||
function invalidProof(): never {
|
||||
throw new Error('node_possession_proof_invalid');
|
||||
}
|
||||
|
|
@ -113,7 +121,7 @@ export function verifyNodePossessionResponse(
|
|||
challenge: NodePossessionChallenge,
|
||||
registration: VerifiedNodeRegistration,
|
||||
now = Date.now(),
|
||||
): true {
|
||||
): Readonly<VerifiedNodePossession> {
|
||||
try {
|
||||
assertVerifiedNodeRegistration(registration);
|
||||
} catch {
|
||||
|
|
@ -140,7 +148,11 @@ export function verifyNodePossessionResponse(
|
|||
|| input.publicKeyPem.includes('PRIVATE KEY')
|
||||
|| typeof input.signature !== 'string') invalidProof();
|
||||
|
||||
let publicKeyPem: string;
|
||||
let signatureText: string;
|
||||
try {
|
||||
publicKeyPem = input.publicKeyPem as string;
|
||||
signatureText = input.signature as string;
|
||||
const publicKey = createPublicKey({ key: input.publicKeyPem, format: 'pem' });
|
||||
if (publicKey.asymmetricKeyType !== 'ed25519') invalidProof();
|
||||
const fingerprint = createHash('sha256')
|
||||
|
|
@ -154,5 +166,24 @@ export function verifyNodePossessionResponse(
|
|||
invalidProof();
|
||||
}
|
||||
consumedChallenges.add(challenge);
|
||||
return true;
|
||||
const verified = Object.freeze({
|
||||
challenge,
|
||||
registration,
|
||||
response: Object.freeze({
|
||||
challengeId: challenge.challengeId,
|
||||
publicKeyPem: publicKeyPem!,
|
||||
schema: RESPONSE_SCHEMA,
|
||||
signature: signatureText!,
|
||||
}),
|
||||
verifiedAt: now,
|
||||
});
|
||||
verifiedPossessions.add(verified);
|
||||
return verified;
|
||||
}
|
||||
|
||||
export function assertVerifiedNodePossession(
|
||||
possession: VerifiedNodePossession,
|
||||
): VerifiedNodePossession {
|
||||
if (!verifiedPossessions.has(possession)) invalidProof();
|
||||
return possession;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,3 +79,23 @@
|
|||
运行体交接。当前桌面尚无生产节点密钥库桥,正式运行源固定返回无证明,因此即使未来出现一份
|
||||
有效登记声明,也不能越过 `verified_node_possession_missing`。代码验证器存在不等于节点密钥、
|
||||
生产密钥库桥、灯塔会话签发或域连接已经存在。
|
||||
|
||||
## 私钥证明到域交接的证据链 · 阶段 004
|
||||
|
||||
本地布尔值 `nodePossessionVerified=true` 不能作为灯塔签发域会话的请求证据。节点证明验证器
|
||||
现在生成进程内不可伪造的 `VerifiedNodePossession`:包含已验证登记声明、一次性挑战、节点
|
||||
签名响应和验证时间,并通过内部来源集合阻止调用方复制字段冒充验证结果。
|
||||
|
||||
域运行体交接源只接受这个验证对象,并构造固定
|
||||
`gh-aios.domain-runtime-handoff-request/v1` 请求包:账号、域、节点编号、节点类型、完整签名
|
||||
登记声明、挑战、节点响应、一次性请求编号和请求时间。它不包含账号密码、访问令牌、验证码
|
||||
或节点私钥。每份节点证明最多发起一次交接请求;失败重试必须重新完成新挑战,不能重放旧证明。
|
||||
|
||||
响应固定为 `gh-aios.domain-runtime-handoff-response/v1`,并必须回绑请求编号。只允许
|
||||
`ISSUED + handoff + signer_lookup` 或 `NOT_AUTHORIZED + null + null`。客户端仍须使用当前
|
||||
REPO-012 信任快照独立验证 handoff 中的域清单签名、短期会话能力和在线连接回执;服务端
|
||||
返回的 signer 名称、公钥字段或自授权布尔值都不能成为信任来源。
|
||||
|
||||
本阶段闭合的是“证明能够被带到交接边界”的客户端证据结构,不是生产 HTTP 端点。当前公共
|
||||
锚点没有域交接端点注册表,桌面运行源仍返回空结果,因此生产会话签发、域运行体连接与在线
|
||||
回执继续为 `NOT_EXISTS_0`。
|
||||
|
|
|
|||
|
|
@ -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):账号、签名节点登记与当前节点私钥持有证明分离后的端点注册表、无凭据声明读取、60 秒一次性 Ed25519 挑战、不可伪造交接证据包、失败关闭和当前生产缺口验收基线。
|
||||
- [`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 历史意图层,继续保存形成过程;不再作为当前默认入口。
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue