294 lines
11 KiB
TypeScript
294 lines
11 KiB
TypeScript
|
|
import { createPublicKey, verify as verifySignature } from 'node:crypto';
|
||
|
|
|
||
|
|
export const PERSONA_CONTROL_AUTHORIZATION_SCHEMA = 'hololake.persona-control-authorization/v2' as const;
|
||
|
|
export const PERSONA_CONTROL_AUTHORIZATION_SIGNER_REGISTRY_SCHEMA = 'gh-aios.persona-control-authorization-signers/v1' as const;
|
||
|
|
export const PERSONA_PRIMARY_LANGUAGE_PLANNING_SCOPE = 'PERSONA_PRIMARY_LANGUAGE_PLANNING' as const;
|
||
|
|
|
||
|
|
const SIGNING_CONTEXT = 'hololake.persona-control-authorization/signing/v1';
|
||
|
|
const REGISTRY_PATH = 'routing/persona-control-authorization-signers.json';
|
||
|
|
const COMMIT_PATTERN = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/;
|
||
|
|
const DIGEST_PATTERN = /^[a-f0-9]{64}$/;
|
||
|
|
const IDENTIFIER_PATTERN = /^[A-Z0-9][A-Z0-9._:@-]{1,159}$/;
|
||
|
|
const SIGNATURE_PATTERN = /^[A-Za-z0-9_-]{80,128}$/;
|
||
|
|
|
||
|
|
export interface PersonaControlAuthorizationReceipt {
|
||
|
|
schema: typeof PERSONA_CONTROL_AUTHORIZATION_SCHEMA;
|
||
|
|
outcome: 'VERIFIED';
|
||
|
|
authorizationId: string;
|
||
|
|
verifier: 'GUANGHU_OS';
|
||
|
|
scope: typeof PERSONA_PRIMARY_LANGUAGE_PLANNING_SCOPE;
|
||
|
|
personaId: string;
|
||
|
|
humanResponsibilitySubject: string;
|
||
|
|
repositoryHead: string;
|
||
|
|
modelInstanceId: string;
|
||
|
|
requestId: string;
|
||
|
|
sourceLanguageAnchor: string;
|
||
|
|
issuedAt: string;
|
||
|
|
validUntil: string;
|
||
|
|
evidenceDigest: string;
|
||
|
|
signerId: string;
|
||
|
|
signatureAlgorithm: 'Ed25519';
|
||
|
|
signature: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface PersonaControlAuthorizationSigner {
|
||
|
|
algorithm: 'Ed25519';
|
||
|
|
humanResponsibilitySubjects: readonly string[];
|
||
|
|
personaIds: readonly string[];
|
||
|
|
publicKeyPem: string;
|
||
|
|
scopes: readonly [typeof PERSONA_PRIMARY_LANGUAGE_PLANNING_SCOPE, ...string[]];
|
||
|
|
signerId: string;
|
||
|
|
status: 'ACTIVE' | 'REVOKED';
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface PersonaControlAuthorizationSignerRegistrySource {
|
||
|
|
repositoryId: 'REPO-012';
|
||
|
|
sourceCommit: string;
|
||
|
|
sourcePath: typeof REGISTRY_PATH;
|
||
|
|
sourceUrl: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface PersonaControlAuthorizationSignerRegistry {
|
||
|
|
registryId: 'GH-AIOS-PERSONA-CONTROL-AUTHORIZATION-SIGNERS-001';
|
||
|
|
schema: typeof PERSONA_CONTROL_AUTHORIZATION_SIGNER_REGISTRY_SCHEMA;
|
||
|
|
signers: readonly PersonaControlAuthorizationSigner[];
|
||
|
|
source: Readonly<PersonaControlAuthorizationSignerRegistrySource>;
|
||
|
|
state: 'CURRENT';
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface PersonaControlAuthorizationExpectation {
|
||
|
|
personaId: string;
|
||
|
|
humanResponsibilitySubject: string;
|
||
|
|
repositoryHead: string;
|
||
|
|
modelInstanceId: string;
|
||
|
|
requestId: string;
|
||
|
|
sourceLanguageAnchor: string;
|
||
|
|
observedAt: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface VerifiedPersonaControlAuthorization {
|
||
|
|
receipt: Readonly<PersonaControlAuthorizationReceipt>;
|
||
|
|
signerId: string;
|
||
|
|
sourceCommit: string;
|
||
|
|
verifiedAt: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
function invalidRegistry(): never {
|
||
|
|
throw new Error('persona_control_authorization_signer_registry_invalid');
|
||
|
|
}
|
||
|
|
|
||
|
|
function invalidSource(): never {
|
||
|
|
throw new Error('persona_control_authorization_signer_registry_source_invalid');
|
||
|
|
}
|
||
|
|
|
||
|
|
function invalidReceipt(): never {
|
||
|
|
throw new Error('persona_control_authorization_receipt_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 exactIdentifierList(value: unknown): readonly string[] {
|
||
|
|
if (!Array.isArray(value)
|
||
|
|
|| value.length === 0
|
||
|
|
|| value.some(item => !isIdentifier(item))
|
||
|
|
|| new Set(value).size !== value.length) invalidRegistry();
|
||
|
|
return Object.freeze([...value] as string[]);
|
||
|
|
}
|
||
|
|
|
||
|
|
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 parseSource(source: PersonaControlAuthorizationSignerRegistrySource): Readonly<PersonaControlAuthorizationSignerRegistrySource> {
|
||
|
|
if (!isRecord(source)
|
||
|
|
|| !hasExactKeys(source, ['repositoryId', 'sourceCommit', 'sourcePath', 'sourceUrl'])
|
||
|
|
|| source.repositoryId !== 'REPO-012'
|
||
|
|
|| typeof source.sourceCommit !== 'string'
|
||
|
|
|| !COMMIT_PATTERN.test(source.sourceCommit)
|
||
|
|
|| source.sourcePath !== REGISTRY_PATH
|
||
|
|
|| typeof source.sourceUrl !== 'string') invalidSource();
|
||
|
|
const expectedUrl = `https://guanghulab.com/code/bingshuo/guanghu-ice-heart/raw/commit/${source.sourceCommit}/${REGISTRY_PATH}`;
|
||
|
|
if (source.sourceUrl !== expectedUrl) invalidSource();
|
||
|
|
return Object.freeze({ ...source });
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseSigner(input: unknown): PersonaControlAuthorizationSigner {
|
||
|
|
if (!isRecord(input)
|
||
|
|
|| !hasExactKeys(input, ['algorithm', 'humanResponsibilitySubjects', 'personaIds', 'publicKeyPem', 'scopes', 'signerId', 'status'])
|
||
|
|
|| input.algorithm !== 'Ed25519'
|
||
|
|
|| !isIdentifier(input.signerId)
|
||
|
|
|| (input.status !== 'ACTIVE' && input.status !== 'REVOKED')) invalidRegistry();
|
||
|
|
const scopes = exactIdentifierList(input.scopes);
|
||
|
|
if (!scopes.includes(PERSONA_PRIMARY_LANGUAGE_PLANNING_SCOPE)) invalidRegistry();
|
||
|
|
return Object.freeze({
|
||
|
|
algorithm: 'Ed25519',
|
||
|
|
humanResponsibilitySubjects: exactIdentifierList(input.humanResponsibilitySubjects),
|
||
|
|
personaIds: exactIdentifierList(input.personaIds),
|
||
|
|
publicKeyPem: parsePublicKey(input.publicKeyPem),
|
||
|
|
scopes: scopes as PersonaControlAuthorizationSigner['scopes'],
|
||
|
|
signerId: input.signerId,
|
||
|
|
status: input.status,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export function parsePersonaControlAuthorizationSignerRegistry(
|
||
|
|
input: unknown,
|
||
|
|
source: PersonaControlAuthorizationSignerRegistrySource,
|
||
|
|
): PersonaControlAuthorizationSignerRegistry {
|
||
|
|
if (!isRecord(input)
|
||
|
|
|| !hasExactKeys(input, ['registryId', 'schema', 'signers', 'state'])
|
||
|
|
|| input.registryId !== 'GH-AIOS-PERSONA-CONTROL-AUTHORIZATION-SIGNERS-001'
|
||
|
|
|| input.schema !== PERSONA_CONTROL_AUTHORIZATION_SIGNER_REGISTRY_SCHEMA
|
||
|
|
|| input.state !== 'CURRENT'
|
||
|
|
|| !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,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function requiredString(record: Record<string, unknown>, key: string): string {
|
||
|
|
const value = Reflect.get(record, key);
|
||
|
|
if (typeof value !== 'string' || !value.trim()) invalidReceipt();
|
||
|
|
return value;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function parsePersonaControlAuthorizationReceipt(input: unknown): PersonaControlAuthorizationReceipt {
|
||
|
|
const keys = [
|
||
|
|
'authorizationId', 'evidenceDigest', 'humanResponsibilitySubject', 'issuedAt', 'modelInstanceId',
|
||
|
|
'outcome', 'personaId', 'repositoryHead', 'requestId', 'schema', 'scope', 'signature',
|
||
|
|
'signatureAlgorithm', 'signerId', 'sourceLanguageAnchor', 'validUntil', 'verifier',
|
||
|
|
];
|
||
|
|
if (!isRecord(input)
|
||
|
|
|| !hasExactKeys(input, keys)
|
||
|
|
|| input.schema !== PERSONA_CONTROL_AUTHORIZATION_SCHEMA
|
||
|
|
|| input.outcome !== 'VERIFIED'
|
||
|
|
|| input.verifier !== 'GUANGHU_OS'
|
||
|
|
|| input.scope !== PERSONA_PRIMARY_LANGUAGE_PLANNING_SCOPE
|
||
|
|
|| input.signatureAlgorithm !== 'Ed25519') invalidReceipt();
|
||
|
|
const receipt = {
|
||
|
|
schema: input.schema,
|
||
|
|
outcome: input.outcome,
|
||
|
|
authorizationId: requiredString(input, 'authorizationId'),
|
||
|
|
verifier: input.verifier,
|
||
|
|
scope: input.scope,
|
||
|
|
personaId: requiredString(input, 'personaId'),
|
||
|
|
humanResponsibilitySubject: requiredString(input, 'humanResponsibilitySubject'),
|
||
|
|
repositoryHead: requiredString(input, 'repositoryHead'),
|
||
|
|
modelInstanceId: requiredString(input, 'modelInstanceId'),
|
||
|
|
requestId: requiredString(input, 'requestId'),
|
||
|
|
sourceLanguageAnchor: requiredString(input, 'sourceLanguageAnchor'),
|
||
|
|
issuedAt: requiredString(input, 'issuedAt'),
|
||
|
|
validUntil: requiredString(input, 'validUntil'),
|
||
|
|
evidenceDigest: requiredString(input, 'evidenceDigest'),
|
||
|
|
signerId: requiredString(input, 'signerId'),
|
||
|
|
signatureAlgorithm: input.signatureAlgorithm,
|
||
|
|
signature: requiredString(input, 'signature'),
|
||
|
|
} satisfies PersonaControlAuthorizationReceipt;
|
||
|
|
if (!isIdentifier(receipt.authorizationId)
|
||
|
|
|| !isIdentifier(receipt.personaId)
|
||
|
|
|| !isIdentifier(receipt.humanResponsibilitySubject)
|
||
|
|
|| !COMMIT_PATTERN.test(receipt.repositoryHead)
|
||
|
|
|| !isIdentifier(receipt.modelInstanceId)
|
||
|
|
|| !isIdentifier(receipt.requestId)
|
||
|
|
|| !isIdentifier(receipt.signerId)
|
||
|
|
|| !DIGEST_PATTERN.test(receipt.evidenceDigest)
|
||
|
|
|| !SIGNATURE_PATTERN.test(receipt.signature)) invalidReceipt();
|
||
|
|
return Object.freeze(receipt);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function personaControlAuthorizationSigningBytes(receipt: Omit<PersonaControlAuthorizationReceipt, 'signature'>): Buffer {
|
||
|
|
return Buffer.from(`${JSON.stringify([
|
||
|
|
SIGNING_CONTEXT,
|
||
|
|
receipt.schema,
|
||
|
|
receipt.outcome,
|
||
|
|
receipt.authorizationId,
|
||
|
|
receipt.verifier,
|
||
|
|
receipt.scope,
|
||
|
|
receipt.personaId,
|
||
|
|
receipt.humanResponsibilitySubject,
|
||
|
|
receipt.repositoryHead,
|
||
|
|
receipt.modelInstanceId,
|
||
|
|
receipt.requestId,
|
||
|
|
receipt.sourceLanguageAnchor,
|
||
|
|
receipt.issuedAt,
|
||
|
|
receipt.validUntil,
|
||
|
|
receipt.evidenceDigest,
|
||
|
|
receipt.signerId,
|
||
|
|
receipt.signatureAlgorithm,
|
||
|
|
])}\n`, 'utf8');
|
||
|
|
}
|
||
|
|
|
||
|
|
export function verifyPersonaControlAuthorization(
|
||
|
|
input: unknown,
|
||
|
|
registry: PersonaControlAuthorizationSignerRegistry,
|
||
|
|
expected: PersonaControlAuthorizationExpectation,
|
||
|
|
): VerifiedPersonaControlAuthorization | null {
|
||
|
|
let receipt: PersonaControlAuthorizationReceipt;
|
||
|
|
try {
|
||
|
|
receipt = parsePersonaControlAuthorizationReceipt(input);
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const issuedAt = Date.parse(receipt.issuedAt);
|
||
|
|
const validUntil = Date.parse(receipt.validUntil);
|
||
|
|
if (receipt.personaId !== expected.personaId
|
||
|
|
|| receipt.humanResponsibilitySubject !== expected.humanResponsibilitySubject
|
||
|
|
|| receipt.repositoryHead !== expected.repositoryHead
|
||
|
|
|| receipt.modelInstanceId !== expected.modelInstanceId
|
||
|
|
|| receipt.requestId !== expected.requestId
|
||
|
|
|| receipt.sourceLanguageAnchor !== expected.sourceLanguageAnchor
|
||
|
|
|| !Number.isFinite(issuedAt)
|
||
|
|
|| !Number.isFinite(validUntil)
|
||
|
|
|| issuedAt > expected.observedAt
|
||
|
|
|| expected.observedAt > validUntil
|
||
|
|
|| issuedAt >= validUntil) return null;
|
||
|
|
const signer = registry.signers.find(candidate => candidate.status === 'ACTIVE'
|
||
|
|
&& candidate.signerId === receipt.signerId
|
||
|
|
&& candidate.personaIds.includes(receipt.personaId)
|
||
|
|
&& candidate.humanResponsibilitySubjects.includes(receipt.humanResponsibilitySubject)
|
||
|
|
&& candidate.scopes.includes(receipt.scope));
|
||
|
|
if (!signer) return null;
|
||
|
|
const { signature, ...signedReceipt } = receipt;
|
||
|
|
let signatureBytes: Buffer;
|
||
|
|
try {
|
||
|
|
signatureBytes = Buffer.from(signature, 'base64url');
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
if (!verifySignature(null, personaControlAuthorizationSigningBytes(signedReceipt), signer.publicKeyPem, signatureBytes)) return null;
|
||
|
|
return Object.freeze({
|
||
|
|
receipt,
|
||
|
|
signerId: signer.signerId,
|
||
|
|
sourceCommit: registry.source.sourceCommit,
|
||
|
|
verifiedAt: new Date(expected.observedAt).toISOString(),
|
||
|
|
});
|
||
|
|
}
|