import { Buffer } from 'node:buffer'; import { createHash, createPublicKey, verify } from 'node:crypto'; import type { KeyObject } from 'node:crypto'; import { isMakeLoreVersionCompatible, parsePluginReleaseDescriptor, serializePluginReleaseDescriptor, type PluginReleaseDescriptor, } from './release-descriptor'; import { createCodeOwnedPluginTrustStore, type PluginSigningKeyStore, } from './trusted-keys'; export type PluginSignatureFailureCode = | 'plugin_signature_invalid' | 'plugin_artifact_invalid' | 'plugin_incompatible_client'; export type PluginSignatureVerificationResult = | { readonly ok: true } | { readonly ok: false; readonly code: PluginSignatureFailureCode; readonly message: string }; export interface PluginSignatureVerifierOptions { readonly keyStore?: PluginSigningKeyStore | ReadonlyMap; readonly clientVersion: string; } export interface PluginSignatureVerifier { verify(input: { readonly keyId: string; readonly signature: string | Uint8Array; readonly descriptor: PluginReleaseDescriptor; readonly artifact: Uint8Array; }): PluginSignatureVerificationResult; } function failure(code: PluginSignatureFailureCode, message: string): PluginSignatureVerificationResult { return { ok: false, code, message }; } function keyFromStore( keyStore: PluginSigningKeyStore | ReadonlyMap, keyId: string, ): Uint8Array | string | null { const key = keyStore.get(keyId); return key === undefined ? null : key; } function publicKey(value: Uint8Array | string): KeyObject | null { try { if (typeof value === 'string' && !value.includes('PUBLIC KEY')) return null; const key = typeof value === 'string' ? createPublicKey(value) : createPublicKey({ key: Buffer.from(value), format: 'der', type: 'spki' }); return key.asymmetricKeyType === 'ed25519' ? key : null; } catch { return null; } } function signatureBytes(value: string | Uint8Array): Buffer | null { try { if (typeof value === 'string' && (value.length !== 86 || !/^[A-Za-z0-9_-]+$/u.test(value))) return null; const bytes = typeof value === 'string' ? Buffer.from(value, 'base64url') : Buffer.from(value); return bytes.length === 64 ? bytes : null; } catch { return null; } } function artifactDigest(artifact: Uint8Array): string { return createHash('sha256').update(artifact).digest('hex'); } export function createPluginSignatureVerifier( options: PluginSignatureVerifierOptions, ): PluginSignatureVerifier { const keyStore = options.keyStore ?? createCodeOwnedPluginTrustStore(); return { verify(input) { if (!(input.artifact instanceof Uint8Array)) { return failure('plugin_artifact_invalid', 'artifact bytes are invalid'); } let descriptorBytes: Uint8Array; try { descriptorBytes = serializePluginReleaseDescriptor(input.descriptor); } catch { return failure('plugin_signature_invalid', 'descriptor is invalid'); } if (input.artifact.byteLength !== input.descriptor.artifact.sizeBytes || artifactDigest(input.artifact) !== input.descriptor.artifact.sha256) { return failure('plugin_artifact_invalid', 'artifact bytes do not match the descriptor'); } if (!isCompatibleClient(options.clientVersion, input.descriptor)) { return failure('plugin_incompatible_client', 'client version is outside the Release range'); } const encodedKey = keyFromStore(keyStore, input.keyId); if (encodedKey === null) return failure('plugin_signature_invalid', 'signing key is not trusted'); const key = publicKey(encodedKey); const signature = signatureBytes(input.signature); if (!key || !signature || !verify(null, descriptorBytes, key, signature)) { return failure('plugin_signature_invalid', 'descriptor signature is invalid'); } return { ok: true }; }, }; } function isCompatibleClient(clientVersion: string, descriptor: PluginReleaseDescriptor): boolean { // Kept local to avoid making the verifier infer a range from untrusted fields. return isMakeLoreVersionCompatible( clientVersion, descriptor.minMakeloreVersion, descriptor.maxMakeloreVersion, ); } export interface VerifyPluginReleaseSignatureInput { readonly verifier?: PluginSignatureVerifier; readonly keyId: string; readonly signature: string | Uint8Array; readonly descriptor: PluginReleaseDescriptor | Uint8Array | string; readonly artifact: Uint8Array; readonly clientVersion?: string; readonly keyStore?: PluginSigningKeyStore | ReadonlyMap; } export function verifyPluginReleaseSignature( input: VerifyPluginReleaseSignatureInput, ): PluginSignatureVerificationResult { const descriptor = input.descriptor instanceof Uint8Array || typeof input.descriptor === 'string' ? (() => { try { return parsePluginReleaseDescriptor(input.descriptor as Uint8Array | string); } catch { return null; } })() : input.descriptor; if (!descriptor) return failure('plugin_signature_invalid', 'descriptor is invalid'); const verifier = input.verifier ?? createPluginSignatureVerifier({ keyStore: input.keyStore, clientVersion: input.clientVersion ?? '', }); return verifier.verify({ keyId: input.keyId, signature: input.signature, descriptor, artifact: input.artifact, }); } export const verifyReleaseSignature = verifyPluginReleaseSignature;