feat(domain-access): load trusted signer registry
This commit is contained in:
parent
fa2c868e0a
commit
6bd95c76e9
7 changed files with 318 additions and 17 deletions
|
|
@ -12,6 +12,7 @@
|
|||
"diff": "^7.0.0",
|
||||
"express": "^5.1.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"lucide-react": "^0.468.0",
|
||||
"marked": "^15.0.0",
|
||||
"simple-git": "^3.27.0"
|
||||
},
|
||||
|
|
@ -2352,6 +2353,15 @@
|
|||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.468.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz",
|
||||
"integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "15.0.12",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||
|
|
@ -2638,7 +2648,6 @@
|
|||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"cors": "^2.8.5",
|
||||
"simple-git": "^3.27.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"lucide-react": "^0.468.0",
|
||||
"marked": "^15.0.0",
|
||||
"diff": "^7.0.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,16 +6,44 @@ import {
|
|||
evaluateDomainAccess,
|
||||
parseDomainAccessHandoff,
|
||||
} from './domain-access.js';
|
||||
import {
|
||||
parseTrustedManifestSignerRegistry,
|
||||
resolveTrustedManifestSigner,
|
||||
type TrustedManifestSigner,
|
||||
} from './trusted-signer-registry.js';
|
||||
|
||||
const NOW = 1_786_291_200_000;
|
||||
const COMMIT = 'b'.repeat(40);
|
||||
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
||||
const trustedSigner = {
|
||||
algorithm: 'Ed25519',
|
||||
publicKeyPem: publicKey.export({ format: 'pem', type: 'spki' }).toString(),
|
||||
repositoryId: 'REPO-014',
|
||||
signerId: 'GH-LIGHTHOUSE-001',
|
||||
} as const;
|
||||
function trustedSignerFromRegistry(overrides: Record<string, unknown> = {}): TrustedManifestSigner {
|
||||
const parsed = parseTrustedManifestSignerRegistry({
|
||||
registryId: 'GH-AIOS-TRUSTED-DOMAIN-MANIFEST-SIGNERS-001',
|
||||
schema: 'gh-aios.trusted-domain-manifest-signers/v1',
|
||||
signers: [{
|
||||
algorithm: 'Ed25519',
|
||||
domainIds: ['DOM-FIFTH-0001'],
|
||||
publicKeyPem: publicKey.export({ format: 'pem', type: 'spki' }).toString(),
|
||||
repositoryId: 'REPO-014',
|
||||
signerId: 'GH-LIGHTHOUSE-001',
|
||||
status: 'ACTIVE',
|
||||
...overrides,
|
||||
}],
|
||||
state: 'CURRENT',
|
||||
version: '1.0.0',
|
||||
}, {
|
||||
repositoryId: 'REPO-012',
|
||||
sourceCommit: 'a'.repeat(40),
|
||||
sourceUrl: 'https://guanghulab.com/code/bingshuo/guanghu-ice-heart',
|
||||
});
|
||||
const signer = resolveTrustedManifestSigner(parsed, {
|
||||
domainId: 'DOM-FIFTH-0001',
|
||||
repositoryId: String(overrides.repositoryId ?? 'REPO-014'),
|
||||
signerId: 'GH-LIGHTHOUSE-001',
|
||||
});
|
||||
if (!signer) throw new Error('test_trusted_signer_missing');
|
||||
return signer;
|
||||
}
|
||||
const trustedSigner = trustedSignerFromRegistry();
|
||||
const manifestPayload = {
|
||||
domainId: 'DOM-FIFTH-0001',
|
||||
repositoryId: 'REPO-014',
|
||||
|
|
@ -147,7 +175,7 @@ test('trusted signer identity and repository are external inputs, not payload au
|
|||
validHandoff,
|
||||
'DOM-FIFTH-0001',
|
||||
'LOCAL-001',
|
||||
{ ...trustedSigner, repositoryId: 'REPO-OTHER' },
|
||||
trustedSignerFromRegistry({ repositoryId: 'REPO-OTHER' }),
|
||||
NOW,
|
||||
), /domain_access_handoff_invalid/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import { createHash, verify as verifySignature } from 'node:crypto';
|
||||
import {
|
||||
assertRegisteredTrustedManifestSigner,
|
||||
type TrustedManifestSigner,
|
||||
} from './trusted-signer-registry.js';
|
||||
|
||||
const MANIFEST_SCHEMA = 'gh-aios.domain-manifest/v1' as const;
|
||||
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
||||
|
|
@ -30,13 +34,6 @@ export interface DomainManifestSignedPayload {
|
|||
sourceCommit: string;
|
||||
}
|
||||
|
||||
export interface TrustedManifestSigner {
|
||||
algorithm: 'Ed25519';
|
||||
publicKeyPem: string;
|
||||
repositoryId: string;
|
||||
signerId: string;
|
||||
}
|
||||
|
||||
export interface DomainSessionCapability {
|
||||
capabilityId: string;
|
||||
domainId: string;
|
||||
|
|
@ -117,6 +114,7 @@ export function parseDomainAccessHandoff(
|
|||
trustedSigner: TrustedManifestSigner,
|
||||
now = Date.now(),
|
||||
): DomainAccessHandoff {
|
||||
assertRegisteredTrustedManifestSigner(trustedSigner);
|
||||
if (!isRecord(input) || !hasExactKeys(input, ['connectionReceipt', 'manifest', 'sessionCapability'])) invalidHandoff();
|
||||
const manifest = input.manifest;
|
||||
const capability = input.sessionCapability;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { generateKeyPairSync } from 'node:crypto';
|
||||
import {
|
||||
assertRegisteredTrustedManifestSigner,
|
||||
parseTrustedManifestSignerRegistry,
|
||||
resolveTrustedManifestSigner,
|
||||
} from './trusted-signer-registry.js';
|
||||
|
||||
const COMMIT = 'd'.repeat(40);
|
||||
const SOURCE = {
|
||||
repositoryId: 'REPO-012',
|
||||
sourceCommit: COMMIT,
|
||||
sourceUrl: 'https://guanghulab.com/code/bingshuo/guanghu-ice-heart',
|
||||
} as const;
|
||||
const publicKeyPem = generateKeyPairSync('ed25519').publicKey
|
||||
.export({ format: 'pem', type: 'spki' }).toString();
|
||||
|
||||
function registry(signers: unknown[] = []) {
|
||||
return {
|
||||
registryId: 'GH-AIOS-TRUSTED-DOMAIN-MANIFEST-SIGNERS-001',
|
||||
schema: 'gh-aios.trusted-domain-manifest-signers/v1',
|
||||
signers,
|
||||
state: 'CURRENT',
|
||||
version: '1.0.0',
|
||||
};
|
||||
}
|
||||
|
||||
function signer(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
algorithm: 'Ed25519',
|
||||
domainIds: ['DOM-FIFTH-0001'],
|
||||
publicKeyPem,
|
||||
repositoryId: 'REPO-014',
|
||||
signerId: 'GH-LIGHTHOUSE-001',
|
||||
status: 'ACTIVE',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('an empty current registry is safe and trusts no signer', () => {
|
||||
const parsed = parseTrustedManifestSignerRegistry(registry(), SOURCE);
|
||||
assert.equal(parsed.signers.length, 0);
|
||||
assert.equal(resolveTrustedManifestSigner(parsed, {
|
||||
domainId: 'DOM-FIFTH-0001',
|
||||
repositoryId: 'REPO-014',
|
||||
signerId: 'GH-LIGHTHOUSE-001',
|
||||
}), null);
|
||||
});
|
||||
|
||||
test('an active Ed25519 SPKI signer resolves only for its exact domain and repository', () => {
|
||||
const parsed = parseTrustedManifestSignerRegistry(registry([signer()]), SOURCE);
|
||||
const trusted = resolveTrustedManifestSigner(parsed, {
|
||||
domainId: 'DOM-FIFTH-0001',
|
||||
repositoryId: 'REPO-014',
|
||||
signerId: 'GH-LIGHTHOUSE-001',
|
||||
});
|
||||
assert.ok(trusted);
|
||||
assert.equal(assertRegisteredTrustedManifestSigner(trusted), trusted);
|
||||
assert.equal(resolveTrustedManifestSigner(parsed, {
|
||||
domainId: 'DOMAIN-OTHER',
|
||||
repositoryId: 'REPO-014',
|
||||
signerId: 'GH-LIGHTHOUSE-001',
|
||||
}), null);
|
||||
});
|
||||
|
||||
test('copied signer fields do not cross the parsed-registry trust boundary', () => {
|
||||
const parsed = parseTrustedManifestSignerRegistry(registry([signer()]), SOURCE);
|
||||
const trusted = resolveTrustedManifestSigner(parsed, {
|
||||
domainId: 'DOM-FIFTH-0001',
|
||||
repositoryId: 'REPO-014',
|
||||
signerId: 'GH-LIGHTHOUSE-001',
|
||||
});
|
||||
assert.ok(trusted);
|
||||
assert.throws(() => assertRegisteredTrustedManifestSigner({ ...trusted }), /trusted_manifest_signer_unregistered/);
|
||||
});
|
||||
|
||||
test('revoked signers stay parseable for history but never resolve', () => {
|
||||
const parsed = parseTrustedManifestSignerRegistry(registry([signer({ status: 'REVOKED' })]), SOURCE);
|
||||
assert.equal(resolveTrustedManifestSigner(parsed, {
|
||||
domainId: 'DOM-FIFTH-0001',
|
||||
repositoryId: 'REPO-014',
|
||||
signerId: 'GH-LIGHTHOUSE-001',
|
||||
}), null);
|
||||
});
|
||||
|
||||
test('duplicates, unknown fields and unsupported algorithms fail closed', () => {
|
||||
assert.throws(() => parseTrustedManifestSignerRegistry(registry([signer(), signer()]), SOURCE), /trusted_signer_registry_invalid/);
|
||||
assert.throws(() => parseTrustedManifestSignerRegistry({ ...registry(), extra: true }, SOURCE), /trusted_signer_registry_invalid/);
|
||||
assert.throws(() => parseTrustedManifestSignerRegistry(registry([signer({ algorithm: 'RSA' })]), SOURCE), /trusted_signer_registry_invalid/);
|
||||
});
|
||||
|
||||
test('private keys and malformed public keys fail closed', () => {
|
||||
const privateKeyPem = generateKeyPairSync('ed25519').privateKey
|
||||
.export({ format: 'pem', type: 'pkcs8' }).toString();
|
||||
assert.throws(() => parseTrustedManifestSignerRegistry(registry([signer({ publicKeyPem: privateKeyPem })]), SOURCE), /trusted_signer_registry_invalid/);
|
||||
assert.throws(() => parseTrustedManifestSignerRegistry(registry([signer({ publicKeyPem: 'not-a-key' })]), SOURCE), /trusted_signer_registry_invalid/);
|
||||
});
|
||||
|
||||
test('registry source provenance requires exact Guanghu HTTPS and a full commit', () => {
|
||||
assert.throws(() => parseTrustedManifestSignerRegistry(registry(), { ...SOURCE, sourceCommit: 'short' }), /trusted_signer_registry_source_invalid/);
|
||||
assert.throws(() => parseTrustedManifestSignerRegistry(registry(), { ...SOURCE, sourceUrl: 'https://example.com/registry.json' }), /trusted_signer_registry_source_invalid/);
|
||||
assert.throws(() => parseTrustedManifestSignerRegistry(registry(), { ...SOURCE, sourceUrl: 'https://user:pass@guanghulab.com/registry.json' }), /trusted_signer_registry_source_invalid/);
|
||||
});
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
import { createPublicKey } from 'node:crypto';
|
||||
|
||||
const REGISTRY_SCHEMA = 'gh-aios.trusted-domain-manifest-signers/v1' as const;
|
||||
const COMMIT_PATTERN = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/;
|
||||
const IDENTIFIER_PATTERN = /^[A-Z0-9][A-Z0-9._:-]{1,159}$/;
|
||||
const VERSION_PATTERN = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
|
||||
const registeredTrustedSigners = new WeakSet<object>();
|
||||
|
||||
export interface TrustedManifestSigner {
|
||||
algorithm: 'Ed25519';
|
||||
domainIds: readonly string[];
|
||||
publicKeyPem: string;
|
||||
repositoryId: string;
|
||||
signerId: string;
|
||||
status: 'ACTIVE' | 'REVOKED';
|
||||
}
|
||||
|
||||
export interface TrustedSignerRegistrySource {
|
||||
repositoryId: 'REPO-012';
|
||||
sourceCommit: string;
|
||||
sourceUrl: string;
|
||||
}
|
||||
|
||||
export interface TrustedManifestSignerRegistry {
|
||||
registryId: 'GH-AIOS-TRUSTED-DOMAIN-MANIFEST-SIGNERS-001';
|
||||
schema: typeof REGISTRY_SCHEMA;
|
||||
signers: readonly TrustedManifestSigner[];
|
||||
source: Readonly<TrustedSignerRegistrySource>;
|
||||
state: 'CURRENT';
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface TrustedSignerLookup {
|
||||
domainId: string;
|
||||
repositoryId: string;
|
||||
signerId: string;
|
||||
}
|
||||
|
||||
function invalidRegistry(): never {
|
||||
throw new Error('trusted_signer_registry_invalid');
|
||||
}
|
||||
|
||||
function invalidSource(): never {
|
||||
throw new Error('trusted_signer_registry_source_invalid');
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function hasExactKeys(record: Record<string, unknown>, 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 isIdentifier(value: unknown): value is string {
|
||||
return typeof value === 'string' && IDENTIFIER_PATTERN.test(value);
|
||||
}
|
||||
|
||||
function parseSource(source: TrustedSignerRegistrySource): Readonly<TrustedSignerRegistrySource> {
|
||||
if (!isRecord(source)
|
||||
|| !hasExactKeys(source, ['repositoryId', 'sourceCommit', 'sourceUrl'])
|
||||
|| source.repositoryId !== 'REPO-012'
|
||||
|| typeof source.sourceCommit !== 'string'
|
||||
|| !COMMIT_PATTERN.test(source.sourceCommit)
|
||||
|| typeof source.sourceUrl !== 'string') invalidSource();
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(source.sourceUrl);
|
||||
} catch {
|
||||
invalidSource();
|
||||
}
|
||||
if (url.protocol !== 'https:'
|
||||
|| url.hostname !== 'guanghulab.com'
|
||||
|| url.port
|
||||
|| url.username
|
||||
|| url.password
|
||||
|| url.search
|
||||
|| url.hash) invalidSource();
|
||||
return Object.freeze({ ...source });
|
||||
}
|
||||
|
||||
function parsePublicKey(value: unknown): string {
|
||||
if (typeof value !== 'string'
|
||||
|| value.length > 4096
|
||||
|| value.includes('PRIVATE KEY')
|
||||
|| !value.startsWith('-----BEGIN PUBLIC KEY-----\n')
|
||||
|| !value.endsWith('-----END PUBLIC KEY-----\n')) invalidRegistry();
|
||||
try {
|
||||
const key = createPublicKey({ format: 'pem', key: value });
|
||||
if (key.asymmetricKeyType !== 'ed25519') invalidRegistry();
|
||||
} catch {
|
||||
invalidRegistry();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseSigner(input: unknown): TrustedManifestSigner {
|
||||
if (!isRecord(input)
|
||||
|| !hasExactKeys(input, ['algorithm', 'domainIds', 'publicKeyPem', 'repositoryId', 'signerId', 'status'])
|
||||
|| input.algorithm !== 'Ed25519'
|
||||
|| !isIdentifier(input.repositoryId)
|
||||
|| !isIdentifier(input.signerId)
|
||||
|| (input.status !== 'ACTIVE' && input.status !== 'REVOKED')
|
||||
|| !Array.isArray(input.domainIds)
|
||||
|| input.domainIds.length === 0
|
||||
|| input.domainIds.some(domainId => !isIdentifier(domainId))
|
||||
|| new Set(input.domainIds).size !== input.domainIds.length) invalidRegistry();
|
||||
const signer: TrustedManifestSigner = Object.freeze({
|
||||
algorithm: 'Ed25519',
|
||||
domainIds: Object.freeze([...input.domainIds] as string[]),
|
||||
publicKeyPem: parsePublicKey(input.publicKeyPem),
|
||||
repositoryId: input.repositoryId,
|
||||
signerId: input.signerId,
|
||||
status: input.status,
|
||||
});
|
||||
registeredTrustedSigners.add(signer);
|
||||
return signer;
|
||||
}
|
||||
|
||||
export function parseTrustedManifestSignerRegistry(
|
||||
input: unknown,
|
||||
source: TrustedSignerRegistrySource,
|
||||
): TrustedManifestSignerRegistry {
|
||||
if (!isRecord(input)
|
||||
|| !hasExactKeys(input, ['registryId', 'schema', 'signers', 'state', 'version'])
|
||||
|| input.registryId !== 'GH-AIOS-TRUSTED-DOMAIN-MANIFEST-SIGNERS-001'
|
||||
|| input.schema !== REGISTRY_SCHEMA
|
||||
|| input.state !== 'CURRENT'
|
||||
|| typeof input.version !== 'string'
|
||||
|| !VERSION_PATTERN.test(input.version)
|
||||
|| !Array.isArray(input.signers)) invalidRegistry();
|
||||
const signers = input.signers.map(parseSigner);
|
||||
if (new Set(signers.map(signer => signer.signerId)).size !== signers.length) invalidRegistry();
|
||||
return Object.freeze({
|
||||
registryId: input.registryId,
|
||||
schema: input.schema,
|
||||
signers: Object.freeze(signers),
|
||||
source: parseSource(source),
|
||||
state: input.state,
|
||||
version: input.version,
|
||||
});
|
||||
}
|
||||
|
||||
export function assertRegisteredTrustedManifestSigner(
|
||||
signer: TrustedManifestSigner,
|
||||
): TrustedManifestSigner {
|
||||
if (!registeredTrustedSigners.has(signer)) throw new Error('trusted_manifest_signer_unregistered');
|
||||
return signer;
|
||||
}
|
||||
|
||||
export function resolveTrustedManifestSigner(
|
||||
registry: TrustedManifestSignerRegistry,
|
||||
lookup: TrustedSignerLookup,
|
||||
): TrustedManifestSigner | null {
|
||||
const signer = registry.signers.find(candidate => candidate.status === 'ACTIVE'
|
||||
&& candidate.signerId === lookup.signerId
|
||||
&& candidate.repositoryId === lookup.repositoryId
|
||||
&& candidate.domainIds.includes(lookup.domainId));
|
||||
return signer ? assertRegisteredTrustedManifestSigner(signer) : null;
|
||||
}
|
||||
|
|
@ -15,8 +15,7 @@
|
|||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@server/*": ["./server/*"],
|
||||
"marked": ["../hololake-desktop/node_modules/marked/lib/marked.esm.js"]
|
||||
"@server/*": ["./server/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*", "server/**/*"],
|
||||
|
|
|
|||
Loading…
Reference in a new issue