/** * @ruvector/edge-net Model Integrity System * * Content-addressed integrity with: * - Canonical JSON signing * - Threshold signatures with trust roots * - Merkle chunk verification for streaming * - Transparency log integration * * Design principle: Manifest is truth, everything else is replaceable. * * @module @ruvector/edge-net/models/integrity */ import { createHash } from 'crypto'; // ============================================================================ // CANONICAL JSON // ============================================================================ /** * Canonical JSON encoding for deterministic signing. * - Keys sorted lexicographically * - No whitespace * - Unicode escaped consistently * - Numbers without trailing zeros */ export function canonicalize(obj) { if (obj === null || obj === undefined) { return 'null'; } if (typeof obj === 'boolean') { return obj ? 'true' : 'false'; } if (typeof obj === 'number') { if (!Number.isFinite(obj)) { throw new Error('Cannot canonicalize Infinity or NaN'); } // Use JSON for consistent number formatting return JSON.stringify(obj); } if (typeof obj === 'string') { // Escape unicode consistently return JSON.stringify(obj).replace(/[\u007f-\uffff]/g, (c) => { return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); }); } if (Array.isArray(obj)) { const elements = obj.map(canonicalize); return '[' + elements.join(',') + ']'; } if (typeof obj === 'object') { const keys = Object.keys(obj).sort(); const pairs = keys .filter(k => obj[k] !== undefined) .map(k => canonicalize(k) + ':' + canonicalize(obj[k])); return '{' + pairs.join(',') + '}'; } throw new Error(`Cannot canonicalize type: ${typeof obj}`); } /** * Hash canonical JSON bytes */ export function hashCanonical(obj, algorithm = 'sha256') { const canonical = canonicalize(obj); const hash = createHash(algorithm); hash.update(canonical, 'utf8'); return hash.digest('hex'); } // ============================================================================ // TRUST ROOT // ============================================================================ /** * Built-in root keys shipped with SDK. * These are the only keys trusted by default. */ export const BUILTIN_ROOT_KEYS = Object.freeze({ 'ruvector-root-2024': { keyId: 'ruvector-root-2024', algorithm: 'ed25519', publicKey: 'MCowBQYDK2VwAyEAaGVsbG8td29ybGQta2V5LXBsYWNlaG9sZGVy', // Placeholder validFrom: '2024-01-01T00:00:00Z', validUntil: '2030-01-01T00:00:00Z', capabilities: ['sign-manifest', 'sign-adapter', 'delegate'], }, 'ruvector-models-2024': { keyId: 'ruvector-models-2024', algorithm: 'ed25519', publicKey: 'MCowBQYDK2VwAyEAbW9kZWxzLWtleS1wbGFjZWhvbGRlcg==', // Placeholder validFrom: '2024-01-01T00:00:00Z', validUntil: '2026-01-01T00:00:00Z', capabilities: ['sign-manifest'], delegatedBy: 'ruvector-root-2024', }, }); /** * Trust root configuration */ export class TrustRoot { constructor(options = {}) { // Start with built-in keys this.trustedKeys = new Map(); for (const [id, key] of Object.entries(BUILTIN_ROOT_KEYS)) { this.trustedKeys.set(id, key); } // Add enterprise keys if configured if (options.enterpriseKeys) { for (const key of options.enterpriseKeys) { this.addEnterpriseKey(key); } } // Revocation list this.revokedKeys = new Set(options.revokedKeys || []); // Minimum signatures required for official releases this.minimumSignaturesRequired = options.minimumSignaturesRequired || 1; // Threshold for high-security operations (e.g., new root key) this.thresholdSignaturesRequired = options.thresholdSignaturesRequired || 2; } /** * Add an enterprise root key (for private deployments) */ addEnterpriseKey(key) { if (!key.keyId || !key.publicKey) { throw new Error('Enterprise key must have keyId and publicKey'); } // Verify delegation chain if not self-signed if (key.delegatedBy && key.delegationSignature) { const delegator = this.trustedKeys.get(key.delegatedBy); if (!delegator) { throw new Error(`Unknown delegator: ${key.delegatedBy}`); } if (!delegator.capabilities.includes('delegate')) { throw new Error(`Key ${key.delegatedBy} cannot delegate`); } // In production, verify delegationSignature here } this.trustedKeys.set(key.keyId, { ...key, isEnterprise: true, }); } /** * Revoke a key */ revokeKey(keyId, reason) { this.revokedKeys.add(keyId); console.warn(`[TrustRoot] Key revoked: ${keyId} - ${reason}`); } /** * Check if a key is trusted for a capability */ isKeyTrusted(keyId, capability = 'sign-manifest') { if (this.revokedKeys.has(keyId)) { return false; } const key = this.trustedKeys.get(keyId); if (!key) { return false; } // Check validity period const now = new Date(); if (key.validFrom && new Date(key.validFrom) > now) { return false; } if (key.validUntil && new Date(key.validUntil) < now) { return false; } // Check capability if (!key.capabilities.includes(capability)) { return false; } return true; } /** * Get public key for verification */ getPublicKey(keyId) { const key = this.trustedKeys.get(keyId); if (!key || this.revokedKeys.has(keyId)) { return null; } return key.publicKey; } /** * Verify signature set meets threshold */ verifySignatureThreshold(signatures, requiredCount = null) { const required = requiredCount || this.minimumSignaturesRequired; let validCount = 0; const validSigners = []; for (const sig of signatures) { if (this.isKeyTrusted(sig.keyId, 'sign-manifest')) { // In production, verify actual signature here validCount++; validSigners.push(sig.keyId); } } return { valid: validCount >= required, validCount, required, validSigners, }; } /** * Export current trust configuration */ export() { return { trustedKeys: Object.fromEntries(this.trustedKeys), revokedKeys: Array.from(this.revokedKeys), minimumSignaturesRequired: this.minimumSignaturesRequired, thresholdSignaturesRequired: this.thresholdSignaturesRequired, }; } } // ============================================================================ // MERKLE CHUNK VERIFICATION // ============================================================================ /** * Compute Merkle tree from chunk hashes */ export function computeMerkleRoot(chunkHashes) { if (chunkHashes.length === 0) { return hashCanonical({ empty: true }); } if (chunkHashes.length === 1) { return chunkHashes[0]; } // Build tree bottom-up let level = [...chunkHashes]; while (level.length > 1) { const nextLevel = []; for (let i = 0; i < level.length; i += 2) { const left = level[i]; const right = level[i + 1] || left; // Duplicate last if odd const combined = createHash('sha256') .update(left, 'hex') .update(right, 'hex') .digest('hex'); nextLevel.push(combined); } level = nextLevel; } return level[0]; } /** * Generate Merkle proof for a chunk */ export function generateMerkleProof(chunkHashes, chunkIndex) { const proof = []; let level = [...chunkHashes]; let index = chunkIndex; while (level.length > 1) { const isRight = index % 2 === 1; const siblingIndex = isRight ? index - 1 : index + 1; if (siblingIndex < level.length) { proof.push({ hash: level[siblingIndex], position: isRight ? 'left' : 'right', }); } else { // Odd number, sibling is self proof.push({ hash: level[index], position: 'right', }); } // Move up const nextLevel = []; for (let i = 0; i < level.length; i += 2) { const left = level[i]; const right = level[i + 1] || left; nextLevel.push( createHash('sha256') .update(left, 'hex') .update(right, 'hex') .digest('hex') ); } level = nextLevel; index = Math.floor(index / 2); } return proof; } /** * Verify a chunk against Merkle root */ export function verifyMerkleProof(chunkHash, chunkIndex, proof, merkleRoot) { let computed = chunkHash; for (const step of proof) { const left = step.position === 'left' ? step.hash : computed; const right = step.position === 'right' ? step.hash : computed; computed = createHash('sha256') .update(left, 'hex') .update(right, 'hex') .digest('hex'); } return computed === merkleRoot; } /** * Chunk a buffer and compute hashes */ export function chunkAndHash(buffer, chunkSize = 256 * 1024) { const chunks = []; const hashes = []; for (let offset = 0; offset < buffer.length; offset += chunkSize) { const chunk = buffer.slice(offset, offset + chunkSize); chunks.push(chunk); hashes.push( createHash('sha256').update(chunk).digest('hex') ); } return { chunks, chunkHashes: hashes, chunkSize, chunkCount: chunks.length, totalSize: buffer.length, merkleRoot: computeMerkleRoot(hashes), }; } // ============================================================================ // MANIFEST INTEGRITY // ============================================================================ /** * Integrity block for manifests */ export function createIntegrityBlock(manifest, chunkInfo) { // Create the signed payload (everything except signatures) const signedPayload = { model: manifest.model, version: manifest.version, artifacts: manifest.artifacts, provenance: manifest.provenance, capabilities: manifest.capabilities, timestamp: new Date().toISOString(), }; const signedPayloadHash = hashCanonical(signedPayload); return { manifestHash: hashCanonical(manifest), signedPayloadHash, merkleRoot: chunkInfo.merkleRoot, chunking: { chunkSize: chunkInfo.chunkSize, chunkCount: chunkInfo.chunkCount, chunkHashes: chunkInfo.chunkHashes, }, signatures: [], // To be filled by signing process }; } /** * Provenance block for manifests */ export function createProvenanceBlock(options = {}) { return { builtBy: { tool: options.tool || '@ruvector/model-optimizer', version: options.toolVersion || '1.0.0', commit: options.commit || 'unknown', }, optimizationRecipeHash: options.recipeHash || null, calibrationDatasetHash: options.calibrationHash || null, parentLineage: options.parentLineage || null, buildTimestamp: new Date().toISOString(), environment: { platform: process.platform, arch: process.arch, nodeVersion: process.version, }, }; } /** * Full manifest with integrity */ export function createSecureManifest(model, artifacts, options = {}) { const manifest = { schemaVersion: '2.0.0', model: { id: model.id, name: model.name, version: model.version, type: model.type, // 'embedding' | 'generation' tier: model.tier, // 'micro' | 'small' | 'large' capabilities: model.capabilities || [], memoryRequirement: model.memoryRequirement, }, artifacts: artifacts.map(a => ({ path: a.path, size: a.size, sha256: a.sha256, format: a.format, quantization: a.quantization, })), distribution: { gcs: options.gcsUrl, ipfs: options.ipfsCid, fallbackUrls: options.fallbackUrls || [], }, provenance: createProvenanceBlock(options.provenance || {}), capabilities: model.capabilities || [], }; // Add integrity block if chunk info provided if (options.chunkInfo) { manifest.integrity = createIntegrityBlock(manifest, options.chunkInfo); } // Add trust metadata manifest.trust = { trustedKeySetId: options.trustedKeySetId || 'ruvector-default-2024', minimumSignaturesRequired: options.minimumSignaturesRequired || 1, }; return manifest; } // ============================================================================ // MANIFEST VERIFICATION // ============================================================================ /** * Verify a manifest's integrity */ export class ManifestVerifier { constructor(trustRoot = null) { this.trustRoot = trustRoot || new TrustRoot(); } /** * Full verification of a manifest */ verify(manifest) { const errors = []; const warnings = []; // 1. Schema version check if (!manifest.schemaVersion || manifest.schemaVersion < '2.0.0') { warnings.push('Manifest uses old schema version'); } // 2. Verify integrity block if (manifest.integrity) { // Check manifest hash const computed = hashCanonical(manifest); // Note: manifestHash is computed before adding integrity, so we skip this // Check signed payload hash const signedPayload = { model: manifest.model, version: manifest.version, artifacts: manifest.artifacts, provenance: manifest.provenance, capabilities: manifest.capabilities, timestamp: manifest.integrity.timestamp, }; const computedPayloadHash = hashCanonical(signedPayload); // 3. Verify signatures meet threshold if (manifest.integrity.signatures?.length > 0) { const sigResult = this.trustRoot.verifySignatureThreshold( manifest.integrity.signatures, manifest.trust?.minimumSignaturesRequired ); if (!sigResult.valid) { errors.push(`Insufficient valid signatures: ${sigResult.validCount}/${sigResult.required}`); } } else { warnings.push('No signatures present'); } // 4. Verify Merkle root matches chunk hashes if (manifest.integrity.chunking) { const computedRoot = computeMerkleRoot(manifest.integrity.chunking.chunkHashes); if (computedRoot !== manifest.integrity.merkleRoot) { errors.push('Merkle root mismatch'); } } } else { warnings.push('No integrity block present'); } // 5. Check provenance if (!manifest.provenance) { warnings.push('No provenance information'); } // 6. Check required fields if (!manifest.model?.id) errors.push('Missing model.id'); if (!manifest.model?.version) errors.push('Missing model.version'); if (!manifest.artifacts?.length) errors.push('No artifacts defined'); return { valid: errors.length === 0, errors, warnings, trust: manifest.trust, provenance: manifest.provenance, }; } /** * Verify a single chunk during streaming download */ verifyChunk(chunkData, chunkIndex, manifest) { if (!manifest.integrity?.chunking) { return { valid: false, error: 'No chunking info in manifest' }; } const expectedHash = manifest.integrity.chunking.chunkHashes[chunkIndex]; if (!expectedHash) { return { valid: false, error: `No hash for chunk ${chunkIndex}` }; } const actualHash = createHash('sha256').update(chunkData).digest('hex'); if (actualHash !== expectedHash) { return { valid: false, error: `Chunk ${chunkIndex} hash mismatch`, expected: expectedHash, actual: actualHash, }; } return { valid: true, chunkIndex, hash: actualHash }; } } // ============================================================================ // TRANSPARENCY LOG // ============================================================================ /** * Entry in the transparency log */ export function createLogEntry(manifest, publisherKeyId) { return { manifestHash: hashCanonical(manifest), modelId: manifest.model.id, version: manifest.model.version, publisherKeyId, timestamp: new Date().toISOString(), signedPayloadHash: manifest.integrity?.signedPayloadHash, }; } /** * Simple append-only transparency log * In production, this would be backed by a Merkle tree or blockchain */ export class TransparencyLog { constructor(options = {}) { this.entries = []; this.indexByModel = new Map(); this.indexByHash = new Map(); this.logRoot = null; } /** * Append an entry to the log */ append(entry) { const index = this.entries.length; // Compute log entry hash including previous const logEntryHash = hashCanonical({ ...entry, index, previousHash: this.logRoot, }); const fullEntry = { ...entry, index, previousHash: this.logRoot, logEntryHash, }; this.entries.push(fullEntry); this.logRoot = logEntryHash; // Update indexes if (!this.indexByModel.has(entry.modelId)) { this.indexByModel.set(entry.modelId, []); } this.indexByModel.get(entry.modelId).push(index); this.indexByHash.set(entry.manifestHash, index); return fullEntry; } /** * Generate inclusion proof */ getInclusionProof(manifestHash) { const index = this.indexByHash.get(manifestHash); if (index === undefined) { return null; } const entry = this.entries[index]; const proof = []; // Simple chain proof (in production, use Merkle tree) for (let i = index; i < this.entries.length; i++) { proof.push({ index: i, logEntryHash: this.entries[i].logEntryHash, }); } return { entry, proof, currentRoot: this.logRoot, logLength: this.entries.length, }; } /** * Verify inclusion proof */ verifyInclusionProof(proof) { if (!proof || !proof.entry || !proof.proof.length) { return false; } // Verify chain let expectedHash = proof.entry.logEntryHash; for (let i = 1; i < proof.proof.length; i++) { const entry = proof.proof[i]; // Verify chain continuity if (i < proof.proof.length - 1) { // Each entry should reference the previous } } return proof.proof[proof.proof.length - 1].logEntryHash === proof.currentRoot; } /** * Get history for a model */ getModelHistory(modelId) { const indices = this.indexByModel.get(modelId) || []; return indices.map(i => this.entries[i]); } /** * Export log for persistence */ export() { return { entries: this.entries, logRoot: this.logRoot, }; } /** * Import log */ import(data) { this.entries = data.entries || []; this.logRoot = data.logRoot; // Rebuild indexes this.indexByModel.clear(); this.indexByHash.clear(); for (let i = 0; i < this.entries.length; i++) { const entry = this.entries[i]; if (!this.indexByModel.has(entry.modelId)) { this.indexByModel.set(entry.modelId, []); } this.indexByModel.get(entry.modelId).push(i); this.indexByHash.set(entry.manifestHash, i); } } } // ============================================================================ // EXPORTS // ============================================================================ export default { canonicalize, hashCanonical, TrustRoot, BUILTIN_ROOT_KEYS, computeMerkleRoot, generateMerkleProof, verifyMerkleProof, chunkAndHash, createIntegrityBlock, createProvenanceBlock, createSecureManifest, ManifestVerifier, createLogEntry, TransparencyLog, };