/** * test-eoa-derivation.ts — Verify EOA key derivation from PRF output. * * Tests: * 1. Determinism: same PRF input → same EOA address every time * 2. Domain separation: different from what encryption/signing keys would produce * 3. Valid Ethereum address format (EIP-55 checksummed) * 4. Different PRF inputs → different addresses * 5. Private key zeroing works * * Usage: * bun run scripts/test-eoa-derivation.ts */ import { deriveEOAFromPRF, zeroPrivateKey } from '../src/encryptid/eoa-derivation'; let passed = 0; let failed = 0; function assert(condition: boolean, msg: string) { if (condition) { console.log(` ✓ ${msg}`); passed++; } else { console.error(` ✗ ${msg}`); failed++; } } function toHex(bytes: Uint8Array): string { return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''); } async function main() { console.log('=== EOA Key Derivation Tests ===\n'); // Fixed PRF output (simulates 32 bytes from WebAuthn PRF extension) const prfOutput = new Uint8Array(32); for (let i = 0; i < 32; i++) prfOutput[i] = i + 1; // Test 1: Basic derivation works console.log('[1] Basic derivation'); const result1 = deriveEOAFromPRF(prfOutput); assert(result1.privateKey.length === 32, 'Private key is 32 bytes'); assert(result1.publicKey.length === 65, 'Public key is 65 bytes (uncompressed)'); assert(result1.publicKey[0] === 0x04, 'Public key starts with 0x04'); assert(result1.address.startsWith('0x'), 'Address starts with 0x'); assert(result1.address.length === 42, 'Address is 42 chars (0x + 40 hex)'); console.log(` Address: ${result1.address}`); console.log(` PubKey: 0x${toHex(result1.publicKey).slice(0, 20)}...`); // Test 2: Determinism — same input always gives same output console.log('\n[2] Determinism'); const result2 = deriveEOAFromPRF(prfOutput); assert(result2.address === result1.address, 'Same PRF → same address'); assert(toHex(result2.privateKey) === toHex(result1.privateKey), 'Same PRF → same private key'); // Run it 5 more times to be sure for (let i = 0; i < 5; i++) { const r = deriveEOAFromPRF(prfOutput); assert(r.address === result1.address, `Deterministic on iteration ${i + 3}`); } // Test 3: Different input → different address console.log('\n[3] Different inputs produce different addresses'); const prfOutput2 = new Uint8Array(32); for (let i = 0; i < 32; i++) prfOutput2[i] = 32 - i; const result3 = deriveEOAFromPRF(prfOutput2); assert(result3.address !== result1.address, 'Different PRF → different address'); console.log(` Address 1: ${result1.address}`); console.log(` Address 2: ${result3.address}`); // Test 4: EIP-55 checksum validity console.log('\n[4] EIP-55 checksum format'); const hasUppercase = /[A-F]/.test(result1.address.slice(2)); const hasLowercase = /[a-f]/.test(result1.address.slice(2)); const isAllHex = /^0x[0-9a-fA-F]{40}$/.test(result1.address); assert(isAllHex, 'Address is valid hex'); assert(hasUppercase || hasLowercase, 'Address has mixed case (EIP-55 checksum)'); // Test 5: Private key zeroing console.log('\n[5] Private key zeroing'); const result4 = deriveEOAFromPRF(prfOutput); const keyBefore = toHex(result4.privateKey); assert(keyBefore !== '00'.repeat(32), 'Key is non-zero before wipe'); zeroPrivateKey(result4.privateKey); const keyAfter = toHex(result4.privateKey); assert(keyAfter === '00'.repeat(32), 'Key is all zeros after wipe'); // Test 6: Domain separation — verify the address is different from what // you'd get with different HKDF params (we can't test WebCrypto HKDF here // but we can verify our specific salt/info combo produces a unique result) console.log('\n[6] Domain separation'); // Manually derive with same input but different params using @noble/hashes const { hkdf } = await import('@noble/hashes/hkdf'); const { sha256 } = await import('@noble/hashes/sha256'); const encoder = new TextEncoder(); const encryptionKeyBytes = hkdf(sha256, prfOutput, encoder.encode('encryptid-encryption-key-v1'), encoder.encode('AES-256-GCM'), 32); const signingKeyBytes = hkdf(sha256, prfOutput, encoder.encode('encryptid-signing-key-v1'), encoder.encode('ECDSA-P256-seed'), 32); const eoaKeyBytes = hkdf(sha256, prfOutput, encoder.encode('encryptid-eoa-key-v1'), encoder.encode('secp256k1-signing-key'), 32); assert(toHex(encryptionKeyBytes) !== toHex(eoaKeyBytes), 'EOA key ≠ encryption key material'); assert(toHex(signingKeyBytes) !== toHex(eoaKeyBytes), 'EOA key ≠ signing key material'); assert(toHex(result1.privateKey) === toHex(eoaKeyBytes), 'EOA key matches expected HKDF output'); // Summary console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`); process.exit(failed > 0 ? 1 : 0); } main();