diff --git a/product-source/guanghu-knowledge-base/package-lock.json b/product-source/guanghu-knowledge-base/package-lock.json index b0a5a97..eb1b214 100644 --- a/product-source/guanghu-knowledge-base/package-lock.json +++ b/product-source/guanghu-knowledge-base/package-lock.json @@ -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" diff --git a/product-source/guanghu-knowledge-base/package.json b/product-source/guanghu-knowledge-base/package.json index 60251a7..1e2cdb2 100644 --- a/product-source/guanghu-knowledge-base/package.json +++ b/product-source/guanghu-knowledge-base/package.json @@ -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" }, 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 b0e837a..6b978b2 100644 --- a/product-source/guanghu-knowledge-base/server/domain-access.test.ts +++ b/product-source/guanghu-knowledge-base/server/domain-access.test.ts @@ -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 = {}): 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/); }); diff --git a/product-source/guanghu-knowledge-base/server/domain-access.ts b/product-source/guanghu-knowledge-base/server/domain-access.ts index dc8c19f..1a5ee6d 100644 --- a/product-source/guanghu-knowledge-base/server/domain-access.ts +++ b/product-source/guanghu-knowledge-base/server/domain-access.ts @@ -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; diff --git a/product-source/guanghu-knowledge-base/server/trusted-signer-registry.test.ts b/product-source/guanghu-knowledge-base/server/trusted-signer-registry.test.ts new file mode 100644 index 0000000..8b6fe46 --- /dev/null +++ b/product-source/guanghu-knowledge-base/server/trusted-signer-registry.test.ts @@ -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 = {}) { + 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/); +}); diff --git a/product-source/guanghu-knowledge-base/server/trusted-signer-registry.ts b/product-source/guanghu-knowledge-base/server/trusted-signer-registry.ts new file mode 100644 index 0000000..4dc8599 --- /dev/null +++ b/product-source/guanghu-knowledge-base/server/trusted-signer-registry.ts @@ -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(); + +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; + 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 { + 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 isIdentifier(value: unknown): value is string { + return typeof value === 'string' && IDENTIFIER_PATTERN.test(value); +} + +function parseSource(source: TrustedSignerRegistrySource): Readonly { + 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; +} diff --git a/product-source/guanghu-knowledge-base/tsconfig.json b/product-source/guanghu-knowledge-base/tsconfig.json index 1881c76..dec1eda 100644 --- a/product-source/guanghu-knowledge-base/tsconfig.json +++ b/product-source/guanghu-knowledge-base/tsconfig.json @@ -15,8 +15,7 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"], - "@server/*": ["./server/*"], - "marked": ["../hololake-desktop/node_modules/marked/lib/marked.esm.js"] + "@server/*": ["./server/*"] } }, "include": ["src/**/*", "server/**/*"],