UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

347 lines 13.6 kB
/** * RVFA Ed25519 Code Signing -- Digital signatures for RVFA appliance files. * * Provides tamper detection and publisher identity verification using * Ed25519 (RFC 8032) via Node.js native crypto. Zero external dependencies. * * @module @claude-flow/cli/appliance/rvfa-signing */ import { generateKeyPairSync, createHash, sign, verify, createPublicKey, createPrivateKey, } from 'node:crypto'; import { readFile, writeFile, stat, chmod, mkdir } from 'node:fs/promises'; // ── Constants ──────────────────────────────────────────────── const PREAMBLE_SIZE = 12; // 4B magic + 4B version + 4B header_len const SHA256_SIZE = 32; const KEY_FILE_MODE = 0o600; // ── Key Management ─────────────────────────────────────────── /** Compute the fingerprint of a public key: first 16 hex chars of its SHA256. */ function computeFingerprint(publicKeyPem) { return createHash('sha256') .update(publicKeyPem, 'utf-8') .digest('hex') .slice(0, 16); } /** * Generate a new Ed25519 key pair for RVFA signing. */ export async function generateKeyPair() { const { publicKey, privateKey } = generateKeyPairSync('ed25519', { publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, }); const pubBuf = Buffer.from(publicKey, 'utf-8'); const privBuf = Buffer.from(privateKey, 'utf-8'); const fingerprint = computeFingerprint(publicKey); return { publicKey: pubBuf, privateKey: privBuf, fingerprint }; } /** * Save a key pair to disk as PEM files. * * @param keyPair The key pair to persist. * @param dir Directory to write files into. * @param name Base name for the key files (default: 'rvfa-signing'). * @returns Paths to the written public and private key files. */ export async function saveKeyPair(keyPair, dir, name = 'rvfa-signing') { await mkdir(dir, { recursive: true }); const pubPath = `${dir}/${name}.pub`; const privPath = `${dir}/${name}.key`; await writeFile(pubPath, keyPair.publicKey); await writeFile(privPath, keyPair.privateKey, { mode: KEY_FILE_MODE }); // Ensure private key has restrictive permissions even on existing files await chmod(privPath, KEY_FILE_MODE); return { publicKeyPath: pubPath, privateKeyPath: privPath }; } /** * Load a key pair from PEM files on disk. * * @param dir Directory containing the key files. * @param name Base name for the key files (default: 'rvfa-signing'). */ export async function loadKeyPair(dir, name = 'rvfa-signing') { const pubPath = `${dir}/${name}.pub`; const privPath = `${dir}/${name}.key`; const publicKey = await readFile(pubPath); const privateKey = await readFile(privPath); // Warn if private key permissions are too open const privStat = await stat(privPath); const mode = privStat.mode & 0o777; if (mode & 0o077) { console.warn(`[rvfa-signing] WARNING: Private key ${privPath} has open permissions ` + `(${mode.toString(8)}). Consider running: chmod 600 ${privPath}`); } const fingerprint = computeFingerprint(publicKey.toString('utf-8')); return { publicKey, privateKey, fingerprint }; } /** * Load a public key from a single PEM file. */ export async function loadPublicKey(path) { return readFile(path); } // ── Internal Helpers ───────────────────────────────────────── /** * Recursively sort object keys for canonical JSON serialization. * Produces deterministic output regardless of insertion order. */ function canonicalJson(value) { return JSON.stringify(value, (_key, val) => { if (val !== null && typeof val === 'object' && !Array.isArray(val) && !Buffer.isBuffer(val)) { const sorted = {}; for (const k of Object.keys(val).sort()) { sorted[k] = val[k]; } return sorted; } return val; }); } /** * Parse an RVFA binary into its components without full validation. * Returns the header object, header JSON bytes, section data region, and footer. */ function parseRvfaBinary(buf) { if (buf.length < PREAMBLE_SIZE + SHA256_SIZE) { throw new Error('Buffer too small to be a valid RVFA file'); } const magic = buf.subarray(0, 4).toString('ascii'); if (magic !== 'RVFA') { throw new Error(`Invalid RVFA magic: expected "RVFA", got "${magic}"`); } const headerLen = buf.readUInt32LE(8); const headerStart = PREAMBLE_SIZE; const headerEnd = headerStart + headerLen; if (headerEnd > buf.length - SHA256_SIZE) { throw new Error('Header length extends beyond buffer'); } const headerJson = buf.subarray(headerStart, headerEnd).toString('utf-8'); let header; try { header = JSON.parse(headerJson); } catch { throw new Error('Failed to parse RVFA header JSON'); } const footer = buf.subarray(buf.length - SHA256_SIZE); const sectionData = buf.subarray(headerEnd, buf.length - SHA256_SIZE); return { header, headerStart, headerEnd, sectionData, footer }; } /** * Compute the signing digest for an RVFA file. * * The digest is SHA256 of: canonical_header_json (without signature field) * + section_data_bytes * + footer_32_bytes */ function computeSigningDigest(header, sectionData, footer) { // Strip signature field from header for digest computation const stripped = { ...header }; delete stripped.signature; const canonical = Buffer.from(canonicalJson(stripped), 'utf-8'); return createHash('sha256') .update(canonical) .update(sectionData) .update(footer) .digest(); } /** Convert a Buffer or PEM string into a KeyObject. */ function toPrivateKeyObject(key) { const pem = Buffer.isBuffer(key) ? key.toString('utf-8') : key; return createPrivateKey(pem); } /** Convert a Buffer or PEM string into a KeyObject. */ function toPublicKeyObject(key) { const pem = Buffer.isBuffer(key) ? key.toString('utf-8') : key; return createPublicKey(pem); } /** * Rebuild the RVFA binary with an updated header. * * Preserves the original preamble version, recalculates header length, * and keeps section data and footer intact. */ function rebuildRvfa(originalBuf, newHeader, sectionData, footer) { const headerJson = Buffer.from(JSON.stringify(newHeader), 'utf-8'); // Preamble: magic + version + new header length const preamble = Buffer.alloc(PREAMBLE_SIZE); originalBuf.copy(preamble, 0, 0, 8); // magic + version unchanged preamble.writeUInt32LE(headerJson.length, 8); return Buffer.concat([preamble, headerJson, sectionData, footer]); } // ── RvfaSigner ─────────────────────────────────────────────── /** * Signs RVFA appliance files and data with Ed25519. */ export class RvfaSigner { keyObj; fingerprint; constructor(privateKey) { this.keyObj = toPrivateKeyObject(privateKey); // Derive public key to compute fingerprint const pubPem = createPublicKey(this.keyObj) .export({ type: 'spki', format: 'pem' }); this.fingerprint = computeFingerprint(pubPem); } /** * Sign an RVFA appliance file in-place. * * Algorithm: * 1. Read and parse the RVFA binary * 2. Strip any existing signature from the header * 3. Compute SHA256 of [canonical_header + section_data + footer] * 4. Sign the digest with Ed25519 * 5. Embed signature metadata into the header * 6. Write the updated binary back to the file * * @param rvfaPath Path to the .rvf appliance file. * @param signedBy Optional publisher name. * @returns The signature metadata that was embedded. */ async signAppliance(rvfaPath, signedBy) { const buf = await readFile(rvfaPath); const { header, sectionData, footer } = parseRvfaBinary(buf); // Compute digest over header (without signature) + sections + footer const digest = computeSigningDigest(header, sectionData, footer); // Ed25519 sign const sig = sign(null, digest, this.keyObj); const metadata = { algorithm: 'ed25519', publicKeyFingerprint: this.fingerprint, signature: sig.toString('hex'), signedAt: new Date().toISOString(), signedBy, scope: 'full', }; // Embed signature in header and rebuild header.signature = metadata; const rebuilt = rebuildRvfa(buf, header, sectionData, footer); await writeFile(rvfaPath, rebuilt); return metadata; } /** * Sign a section footer hash (detached signature). * * @param footerHash The 32-byte SHA256 footer hash from an RVFA file. * @returns Hex-encoded Ed25519 signature. */ async signSections(footerHash) { if (footerHash.length !== SHA256_SIZE) { throw new Error(`Footer hash must be ${SHA256_SIZE} bytes, got ${footerHash.length}`); } const sig = sign(null, footerHash, this.keyObj); return sig.toString('hex'); } /** * Sign an RVFP patch file (detached signature). * * @param patchData The raw patch binary data. * @returns Hex-encoded Ed25519 signature. */ async signPatch(patchData) { const digest = createHash('sha256').update(patchData).digest(); const sig = sign(null, digest, this.keyObj); return sig.toString('hex'); } } // ── RvfaVerifier ───────────────────────────────────────────── /** * Verifies Ed25519 signatures on RVFA appliance files and data. */ export class RvfaVerifier { keyObj; fingerprint; constructor(publicKey) { this.keyObj = toPublicKeyObject(publicKey); const pem = Buffer.isBuffer(publicKey) ? publicKey.toString('utf-8') : publicKey; this.fingerprint = computeFingerprint(pem); } /** * Verify the Ed25519 signature embedded in an RVFA appliance file. * * @param rvfaPath Path to the .rvf appliance file. * @returns Verification result with details and any errors. */ async verifyAppliance(rvfaPath) { const errors = []; let buf; try { buf = await readFile(rvfaPath); } catch (err) { return { valid: false, errors: [`Failed to read file: ${err.message}`] }; } let parsed; try { parsed = parseRvfaBinary(buf); } catch (err) { return { valid: false, errors: [`Invalid RVFA file: ${err.message}`] }; } const { header, sectionData, footer } = parsed; // Extract signature metadata from header const sigRaw = header.signature; if (!sigRaw || typeof sigRaw !== 'object') { return { valid: false, errors: ['No signature found in RVFA header'] }; } const sigMeta = sigRaw; if (sigMeta.algorithm !== 'ed25519') { errors.push(`Unsupported algorithm: ${String(sigMeta.algorithm)}`); return { valid: false, errors }; } if (typeof sigMeta.signature !== 'string' || !sigMeta.signature) { errors.push('Signature field is missing or empty'); return { valid: false, errors }; } // Recompute the digest the same way the signer did const digest = computeSigningDigest(header, sectionData, footer); // Verify let sigBuf; try { sigBuf = Buffer.from(sigMeta.signature, 'hex'); } catch { errors.push('Signature is not valid hex'); return { valid: false, errors }; } let valid; try { valid = verify(null, digest, this.keyObj, sigBuf); } catch (err) { errors.push(`Verification error: ${err.message}`); return { valid: false, errors }; } if (!valid) { errors.push('Ed25519 signature verification failed: data may be tampered'); } return { valid, signerFingerprint: sigMeta.publicKeyFingerprint, signedAt: sigMeta.signedAt, signedBy: sigMeta.signedBy, errors, }; } /** * Verify a detached Ed25519 signature over arbitrary data. * * @param data The data that was signed. * @param signature Hex-encoded Ed25519 signature. */ async verifyDetached(data, signature) { const digest = createHash('sha256').update(data).digest(); const sigBuf = Buffer.from(signature, 'hex'); return verify(null, digest, this.keyObj, sigBuf); } /** * Verify an RVFP patch file signature. * * @param patchData The raw patch binary data. * @param signature Hex-encoded Ed25519 signature. */ async verifyPatch(patchData, signature) { const digest = createHash('sha256').update(patchData).digest(); const sigBuf = Buffer.from(signature, 'hex'); return verify(null, digest, this.keyObj, sigBuf); } } //# sourceMappingURL=rvfa-signing.js.map