import { createHash, randomUUID } from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import { assertApiCredentialId } from '@qinglong/runtime-core/api-credential'; import { assertApiCredentialSecret, formatApiCredentialToken, } from '@qinglong/runtime-core/api-credential-token'; import { assertProjectPolicyProjectId, normalizeProjectPolicySubject, } from '@qinglong/runtime-core/project-policy'; import type { SecuritySubject } from '@qinglong/runtime-core/security'; const MAX_DIRECTORY_ENTRIES = 64; const MAX_RECORD_BYTES = 4 * 1024; const MAX_PRESENTATION_BYTES = 1024; const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; const FILE_NAME_PATTERN = /^managed-credential-([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.(pending|ready)\.json$/; const TEMP_NAME_PATTERN = /^\.managed-credential-([0-9a-f-]{36})\.([0-9a-f-]{36})\.tmp$/; export interface LocalCredentialAdministrationDeliveryRecord { readonly schemaVersion: 1; readonly kind: 'qinglong3-local-managed-credential-delivery'; readonly mutationId: string; readonly requestId: string; readonly projectId: string; readonly subject: SecuritySubject; readonly credentialId: string; readonly secret: string; readonly notBeforeAtMs: number; readonly expiresAtMs: number; } export interface LocalCredentialAdministrationDeliverySummary { readonly mutationId: string; readonly requestId: string; readonly projectId: string; readonly subject: Readonly; readonly credentialId: string; readonly deliveryDigest: string; readonly path: string; } interface PrivateFile { readonly value: unknown; readonly device: bigint; readonly inode: bigint; readonly digest: string; } export class LocalCredentialAdministrationDeliveryError extends Error { readonly code = 'LOCAL_CREDENTIAL_ADMINISTRATION_DELIVERY_FAILED'; constructor(message: string, readonly cause?: unknown) { super(`Local credential administration delivery failed: ${message}`); this.name = 'LocalCredentialAdministrationDeliveryError'; } } function exactKeys(value: object, expected: readonly string[]): boolean { const actual = Object.keys(value).sort(); const canonical = [...expected].sort(); return ( actual.length === canonical.length && actual.every((key, index) => key === canonical[index]) ); } function missing(error: unknown): boolean { return ( !!error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT' ); } function uid(): number { if ( typeof process.getuid !== 'function' || typeof process.geteuid !== 'function' || process.getuid() !== process.geteuid() ) { throw new LocalCredentialAdministrationDeliveryError( 'real and effective POSIX users must match', ); } return process.getuid(); } function normalizeRecord( value: LocalCredentialAdministrationDeliveryRecord, ): Readonly { if ( !value || typeof value !== 'object' || Array.isArray(value) || !exactKeys(value, [ 'schemaVersion', 'kind', 'mutationId', 'requestId', 'projectId', 'subject', 'credentialId', 'secret', 'notBeforeAtMs', 'expiresAtMs', ]) || value.schemaVersion !== 1 || value.kind !== 'qinglong3-local-managed-credential-delivery' || !UUID_V4_PATTERN.test(value.mutationId) || !REQUEST_ID_PATTERN.test(value.requestId) ) { throw new LocalCredentialAdministrationDeliveryError( 'delivery record shape is invalid', ); } let subject: Readonly; try { assertProjectPolicyProjectId(value.projectId); subject = normalizeProjectPolicySubject(value.subject); assertApiCredentialId(value.credentialId); assertApiCredentialSecret(value.secret); } catch (error) { throw new LocalCredentialAdministrationDeliveryError( 'delivery record identity is invalid', error, ); } if ( subject.type === 'system' || subject.type === 'worker' || !Number.isSafeInteger(value.notBeforeAtMs) || value.notBeforeAtMs < 0 || !Number.isSafeInteger(value.expiresAtMs) || value.expiresAtMs <= value.notBeforeAtMs ) { throw new LocalCredentialAdministrationDeliveryError( 'delivery record lifetime or subject is invalid', ); } return Object.freeze({ ...value, subject }); } function sameSemantic( left: Readonly, right: Readonly, ): boolean { return ( left.mutationId === right.mutationId && left.requestId === right.requestId && left.projectId === right.projectId && left.subject.type === right.subject.type && left.subject.id === right.subject.id && left.credentialId === right.credentialId && left.expiresAtMs - left.notBeforeAtMs === right.expiresAtMs - right.notBeforeAtMs ); } function sameRecord( left: Readonly, right: Readonly, ): boolean { return sameSemantic(left, right) && left.secret === right.secret; } function recordDigest( record: Readonly, ): string { return createHash('sha256') .update('qinglong3.local-managed-credential-delivery.v1\0', 'utf8') .update(JSON.stringify(record), 'utf8') .digest('hex'); } function pendingName(mutationId: string): string { return `managed-credential-${mutationId}.pending.json`; } function readyName(mutationId: string): string { return `managed-credential-${mutationId}.ready.json`; } function presentation( record: Readonly, ): Readonly<{ schemaVersion: 1; kind: 'qinglong3-local-identity-credential-presentation'; token: string; }> { return Object.freeze({ schemaVersion: 1, kind: 'qinglong3-local-identity-credential-presentation', token: formatApiCredentialToken(record.credentialId, record.secret), }); } function normalizePresentation(value: unknown): Readonly<{ schemaVersion: 1; kind: 'qinglong3-local-identity-credential-presentation'; token: string; }> { if ( !value || typeof value !== 'object' || Array.isArray(value) || !exactKeys(value, ['schemaVersion', 'kind', 'token']) ) { throw new LocalCredentialAdministrationDeliveryError( 'credential presentation is invalid', ); } const candidate = value as Record; if ( candidate.schemaVersion !== 1 || candidate.kind !== 'qinglong3-local-identity-credential-presentation' || typeof candidate.token !== 'string' || candidate.token.length > 256 ) { throw new LocalCredentialAdministrationDeliveryError( 'credential presentation is invalid', ); } return Object.freeze( candidate as { schemaVersion: 1; kind: 'qinglong3-local-identity-credential-presentation'; token: string; }, ); } export class FileLocalCredentialAdministrationDelivery { private readonly ownerUid: number; private readonly device: bigint; private readonly inode: bigint; constructor(readonly directory: string) { if ( typeof directory !== 'string' || !path.isAbsolute(directory) || path.parse(directory).root === directory || path.normalize(directory) !== directory || directory.includes('\0') || Buffer.byteLength(directory, 'utf8') > 4096 ) { throw new LocalCredentialAdministrationDeliveryError( 'delivery directory path is invalid', ); } this.ownerUid = uid(); let stat: fs.BigIntStats; try { stat = fs.lstatSync(directory, { bigint: true }); } catch (error) { throw new LocalCredentialAdministrationDeliveryError( 'delivery directory is unavailable', error, ); } if ( !stat.isDirectory() || stat.isSymbolicLink() || Number(stat.uid) !== this.ownerUid || (Number(stat.mode) & 0o777) !== 0o700 || fs.realpathSync(directory) !== directory ) { throw new LocalCredentialAdministrationDeliveryError( 'delivery directory must be a canonical current-UID 0700 directory', ); } this.device = stat.dev; this.inode = stat.ino; } private verifyDirectory(): void { const stat = fs.lstatSync(this.directory, { bigint: true }); if ( !stat.isDirectory() || stat.isSymbolicLink() || Number(stat.uid) !== this.ownerUid || (Number(stat.mode) & 0o777) !== 0o700 || stat.dev !== this.device || stat.ino !== this.inode ) { throw new LocalCredentialAdministrationDeliveryError( 'delivery directory identity changed', ); } } private entries(): readonly string[] { this.verifyDirectory(); const entries = fs.readdirSync(this.directory); if ( entries.length > MAX_DIRECTORY_ENTRIES || entries.some( (name) => !FILE_NAME_PATTERN.test(name) && !TEMP_NAME_PATTERN.test(name), ) ) { throw new LocalCredentialAdministrationDeliveryError( 'delivery directory contents are invalid or unbounded', ); } return entries; } private syncDirectory(): void { const descriptor = fs.openSync(this.directory, fs.constants.O_RDONLY); try { fs.fsyncSync(descriptor); } finally { fs.closeSync(descriptor); } } private read(name: string, maxBytes: number): PrivateFile { this.verifyDirectory(); const filePath = path.join(this.directory, name); let descriptor: number | undefined; let material: Buffer | undefined; try { const before = fs.lstatSync(filePath, { bigint: true }); if ( !before.isFile() || before.isSymbolicLink() || Number(before.uid) !== this.ownerUid || (Number(before.mode) & 0o777) !== 0o600 || before.size < 1n || before.size > BigInt(maxBytes) ) { throw new LocalCredentialAdministrationDeliveryError( 'delivery file is not a bounded private regular file', ); } descriptor = fs.openSync( filePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0), ); const opened = fs.fstatSync(descriptor, { bigint: true }); if ( !opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino || opened.size !== before.size || Number(opened.uid) !== this.ownerUid || (Number(opened.mode) & 0o777) !== 0o600 ) { throw new LocalCredentialAdministrationDeliveryError( 'delivery file identity changed while opening', ); } material = fs.readFileSync(descriptor); const after = fs.fstatSync(descriptor, { bigint: true }); if ( after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size ) { throw new LocalCredentialAdministrationDeliveryError( 'delivery file identity changed while reading', ); } const digest = createHash('sha256').update(material).digest('hex'); return Object.freeze({ value: JSON.parse( new TextDecoder('utf-8', { fatal: true }).decode(material), ) as unknown, device: opened.dev, inode: opened.ino, digest, }); } catch (error) { if (error instanceof LocalCredentialAdministrationDeliveryError) { throw error; } throw new LocalCredentialAdministrationDeliveryError( 'delivery file cannot be read', error, ); } finally { material?.fill(0); if (descriptor !== undefined) fs.closeSync(descriptor); } } private optional(name: string, maxBytes: number): PrivateFile | null { try { return this.read(name, maxBytes); } catch (error) { if ( error instanceof LocalCredentialAdministrationDeliveryError && error.cause && missing(error.cause) ) { return null; } throw error; } } private writeExclusive(name: string, value: unknown): PrivateFile { if (this.entries().length >= MAX_DIRECTORY_ENTRIES - 1) { throw new LocalCredentialAdministrationDeliveryError( 'delivery directory lacks capacity', ); } const temporaryName = `.managed-credential-${name .split('.')[0]! .slice('managed-credential-'.length)}.${randomUUID()}.tmp`; const temporaryPath = path.join(this.directory, temporaryName); const targetPath = path.join(this.directory, name); let descriptor: number | undefined; try { descriptor = fs.openSync( temporaryPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600, ); const serialized = `${JSON.stringify(value)}\n`; if (Buffer.byteLength(serialized) > MAX_RECORD_BYTES) { throw new LocalCredentialAdministrationDeliveryError( 'serialized delivery exceeds its byte budget', ); } fs.writeFileSync(descriptor, serialized, 'utf8'); fs.fsyncSync(descriptor); fs.closeSync(descriptor); descriptor = undefined; fs.linkSync(temporaryPath, targetPath); this.syncDirectory(); return this.read( name, name.endsWith('.ready.json') ? MAX_PRESENTATION_BYTES : MAX_RECORD_BYTES, ); } catch (error) { if ( error && typeof error === 'object' && 'code' in error && error.code === 'EEXIST' ) { return this.read( name, name.endsWith('.ready.json') ? MAX_PRESENTATION_BYTES : MAX_RECORD_BYTES, ); } if (error instanceof LocalCredentialAdministrationDeliveryError) { throw error; } throw new LocalCredentialAdministrationDeliveryError( 'delivery file cannot be published', error, ); } finally { if (descriptor !== undefined) fs.closeSync(descriptor); try { fs.unlinkSync(temporaryPath); this.syncDirectory(); } catch (error) { if (!missing(error)) { // A durable target, if created, remains authoritative. } } } } prepare( candidate: LocalCredentialAdministrationDeliveryRecord, ): Readonly { const normalized = normalizeRecord(candidate); this.entries(); const pending = this.optional( pendingName(normalized.mutationId), MAX_RECORD_BYTES, ); if (pending) { const existing = normalizeRecord( pending.value as LocalCredentialAdministrationDeliveryRecord, ); if (!sameSemantic(existing, normalized)) { throw new LocalCredentialAdministrationDeliveryError( 'pending delivery conflicts with request', ); } return existing; } if ( this.optional(readyName(normalized.mutationId), MAX_PRESENTATION_BYTES) ) { throw new LocalCredentialAdministrationDeliveryError( 'published presentation is missing its durable pending record', ); } const created = this.writeExclusive( pendingName(normalized.mutationId), normalized, ); const stored = normalizeRecord( created.value as LocalCredentialAdministrationDeliveryRecord, ); if (!sameSemantic(stored, normalized)) { throw new LocalCredentialAdministrationDeliveryError( 'concurrent pending delivery conflicts with request', ); } return stored; } digest(prepared: LocalCredentialAdministrationDeliveryRecord): string { return recordDigest(normalizeRecord(prepared)); } publish( prepared: LocalCredentialAdministrationDeliveryRecord, expectedDeliveryDigest: string, ): Readonly { const normalized = normalizeRecord(prepared); if ( !/^[0-9a-f]{64}$/.test(expectedDeliveryDigest) || recordDigest(normalized) !== expectedDeliveryDigest ) { throw new LocalCredentialAdministrationDeliveryError( 'delivery digest is invalid', ); } const pending = this.read( pendingName(normalized.mutationId), MAX_RECORD_BYTES, ); const stored = normalizeRecord( pending.value as LocalCredentialAdministrationDeliveryRecord, ); if (!sameRecord(stored, normalized)) { throw new LocalCredentialAdministrationDeliveryError( 'pending delivery changed before publication', ); } const expectedPresentation = presentation(stored); const ready = this.writeExclusive( readyName(stored.mutationId), expectedPresentation, ); const published = normalizePresentation(ready.value); if (published.token !== expectedPresentation.token) { throw new LocalCredentialAdministrationDeliveryError( 'published credential conflicts with pending delivery', ); } return Object.freeze({ mutationId: stored.mutationId, requestId: stored.requestId, projectId: stored.projectId, subject: stored.subject, credentialId: stored.credentialId, deliveryDigest: expectedDeliveryDigest, path: path.join(this.directory, readyName(stored.mutationId)), }); } inspect( mutationId: string, ): Readonly { if (!UUID_V4_PATTERN.test(mutationId)) { throw new LocalCredentialAdministrationDeliveryError( 'mutationId is invalid', ); } const pending = normalizeRecord( this.read(pendingName(mutationId), MAX_RECORD_BYTES) .value as LocalCredentialAdministrationDeliveryRecord, ); const ready = normalizePresentation( this.read(readyName(mutationId), MAX_PRESENTATION_BYTES).value, ); if (ready.token !== presentation(pending).token) { throw new LocalCredentialAdministrationDeliveryError( 'published credential conflicts with pending delivery', ); } return Object.freeze({ mutationId, requestId: pending.requestId, projectId: pending.projectId, subject: pending.subject, credentialId: pending.credentialId, deliveryDigest: recordDigest(pending), path: path.join(this.directory, readyName(mutationId)), }); } removeAcknowledged( mutationId: string, expectedDeliveryDigest: string, ): 'removed' | 'absent' { if ( !UUID_V4_PATTERN.test(mutationId) || !/^[0-9a-f]{64}$/.test(expectedDeliveryDigest) ) { throw new LocalCredentialAdministrationDeliveryError( 'acknowledgement input is invalid', ); } this.entries(); const pendingFile = this.optional( pendingName(mutationId), MAX_RECORD_BYTES, ); const readyFile = this.optional( readyName(mutationId), MAX_PRESENTATION_BYTES, ); if (!pendingFile && !readyFile) return 'absent'; if (!pendingFile && readyFile) { throw new LocalCredentialAdministrationDeliveryError( 'acknowledged delivery is incomplete', ); } const pending = normalizeRecord( pendingFile!.value as LocalCredentialAdministrationDeliveryRecord, ); if (recordDigest(pending) !== expectedDeliveryDigest) { throw new LocalCredentialAdministrationDeliveryError( 'acknowledged delivery changed before cleanup', ); } if (!readyFile) { fs.unlinkSync(path.join(this.directory, pendingName(mutationId))); this.syncDirectory(); return 'removed'; } const ready = normalizePresentation(readyFile.value); if (ready.token !== presentation(pending).token) { throw new LocalCredentialAdministrationDeliveryError( 'acknowledged delivery changed before cleanup', ); } fs.unlinkSync(path.join(this.directory, readyName(mutationId))); this.syncDirectory(); fs.unlinkSync(path.join(this.directory, pendingName(mutationId))); this.syncDirectory(); return 'removed'; } }