rspace-online/scripts/test-eoa-derivation.ts

112 lines
4.7 KiB
TypeScript

/**
* 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();