#!/usr/bin/env node
/**
* Test signature compatibility between hypercore-crypto (server) and @noble/ed25519 (browser)
*
* This validates that the browser's signing approach produces signatures
* the server can verify, and vice versa.
*
* Browser flow:
* 1. Server generates keypair via crypto.keyPair(seed)
* 2. Keypair stored as base64: publicKey(44 chars) + secretKey(88 chars)
* 3. Browser signs with noble-ed25519 using first 32 bytes of secretKey (= seed)
* 4. Server verifies with hypercore-crypto
*
* Challenge flow:
* 1. Server creates challenge: crypto.randomBytes(32).toString('base64')
* 2. Browser encodes challenge with TextEncoder (UTF-8 bytes of the base64 string)
* 3. Server encodes challenge with b4a.from(challenge, 'utf8')
* 4. Both should produce identical byte arrays
*/
import crypto from 'hypercore-crypto'
import b4a from 'b4a'
import * as ed25519 from '@noble/ed25519'
import { sha512 } from '@noble/hashes/sha2.js'
import { ANProto } from '../lib/anproto.js'
// Configure noble-ed25519 with sha512 (same as browser does)
ed25519.etc.sha512Sync = (...m) => sha512(ed25519.etc.concatBytes(...m))
ed25519.etc.sha512Async = async (...m) => sha512(ed25519.etc.concatBytes(...m))
let passed = 0
let failed = 0
function assert (condition, message) {
if (condition) {
console.log(' ✓', message)
passed++
} else {
console.error(' ✗', message)
failed++
}
}
// Helper: simulate browser's base64 encoding
function bytesToBase64 (bytes) {
return b4a.toString(Buffer.from(bytes), 'base64')
}
function base64ToBytes (base64) {
return new Uint8Array(b4a.from(base64, 'base64'))
}
async function run () {
console.log('\n=== Ed25519 Signature Compatibility Test ===\n')
// 1. Generate keypair (server-side, same as WebIdentityProvider.generateIdentity)
console.log('1. Keypair generation')
const seed = crypto.randomBytes(32)
const keypair = crypto.keyPair(seed)
const publicKeyB64 = b4a.toString(keypair.publicKey, 'base64')
const secretKeyB64 = b4a.toString(keypair.secretKey, 'base64')
assert(publicKeyB64.length === 44, `Public key base64 length = ${publicKeyB64.length} (expected 44)`)
assert(secretKeyB64.length === 88, `Secret key base64 length = ${secretKeyB64.length} (expected 88)`)
assert(keypair.publicKey.length === 32, `Public key buffer length = ${keypair.publicKey.length} (expected 32)`)
assert(keypair.secretKey.length === 64, `Secret key buffer length = ${keypair.secretKey.length} (expected 64)`)
// 2. Challenge generation (same as ChallengeManager.generate)
console.log('\n2. Challenge encoding')
const challenge = crypto.randomBytes(32).toString('base64')
console.log(' Challenge string:', challenge.substring(0, 20) + '...')
// Server encoding
const serverChallengeBytes = b4a.from(challenge, 'utf8')
// Browser encoding (TextEncoder)
const browserChallengeBytes = new TextEncoder().encode(challenge)
assert(
serverChallengeBytes.length === browserChallengeBytes.length,
`Challenge byte length match: server=${serverChallengeBytes.length} browser=${browserChallengeBytes.length}`
)
let bytesMatch = true
for (let i = 0; i < serverChallengeBytes.length; i++) {
if (serverChallengeBytes[i] !== browserChallengeBytes[i]) {
bytesMatch = false
break
}
}
assert(bytesMatch, 'Challenge bytes are identical (UTF-8 encoding match)')
// 3. Browser-side signing (noble-ed25519 with seed)
console.log('\n3. Signing compatibility')
const secretKeyBytes = base64ToBytes(secretKeyB64)
assert(secretKeyBytes.length === 64, `Decoded secret key length = ${secretKeyBytes.length} (expected 64)`)
const privateKey = secretKeyBytes.slice(0, 32) // Browser uses first 32 bytes = seed
const nobleSignature = await ed25519.signAsync(browserChallengeBytes, privateKey)
const nobleSignatureB64 = bytesToBase64(nobleSignature)
assert(nobleSignature.length === 64, `Noble signature length = ${nobleSignature.length} (expected 64)`)
assert(nobleSignatureB64.length === 88, `Noble signature base64 length = ${nobleSignatureB64.length} (expected 88)`)
// 4. Server-side verification (hypercore-crypto)
console.log('\n4. Cross-library verification')
// Server verifies browser signature (the critical path)
const serverVerifiesNoble = crypto.verify(
serverChallengeBytes,
Buffer.from(nobleSignature),
keypair.publicKey
)
assert(serverVerifiesNoble, 'hypercore-crypto verifies noble-ed25519 signature')
// Also test via ANProto.verify (the actual server code path)
const anprotoVerifies = ANProto.verify(challenge, nobleSignatureB64, publicKeyB64)
assert(anprotoVerifies, 'ANProto.verify() verifies noble-ed25519 signature')
// 5. Server-side signing (hypercore-crypto)
console.log('\n5. Reverse verification')
const hcSignature = crypto.sign(serverChallengeBytes, keypair.secretKey)
const hcSignatureB64 = b4a.toString(hcSignature, 'base64')
// Noble verifies hypercore-crypto signature
const nobleVerifiesHC = await ed25519.verifyAsync(
hcSignature,
browserChallengeBytes,
new Uint8Array(keypair.publicKey)
)
assert(nobleVerifiesHC, 'noble-ed25519 verifies hypercore-crypto signature')
// 6. Signature equality (both libs should produce identical signatures)
console.log('\n6. Signature determinism')
let sigsIdentical = true
for (let i = 0; i < 64; i++) {
if (nobleSignature[i] !== hcSignature[i]) {
sigsIdentical = false
break
}
}
assert(sigsIdentical, 'Signatures are byte-identical (deterministic ed25519)')
// 7. Full round-trip (simulate complete browser → server flow)
console.log('\n7. Full round-trip simulation')
const testChallenge = crypto.randomBytes(32).toString('base64')
const testChallengeId = crypto.randomBytes(16).toString('hex')
// Browser signs
const testChallengeBytes = new TextEncoder().encode(testChallenge)
const testSig = await ed25519.signAsync(testChallengeBytes, privateKey)
const testSigB64 = bytesToBase64(testSig)
// Server verifies (exactly as WebIdentityProvider.authenticate does)
const serverCBuf = b4a.from(testChallenge, 'utf8')
const serverSigBuf = b4a.from(testSigB64, 'base64')
const serverPubBuf = b4a.from(publicKeyB64, 'base64')
assert(serverSigBuf.length === 64, `Server decoded signature length = ${serverSigBuf.length}`)
assert(serverPubBuf.length === 32, `Server decoded public key length = ${serverPubBuf.length}`)
const roundTripResult = crypto.verify(serverCBuf, serverSigBuf, serverPubBuf)
assert(roundTripResult, 'Full round-trip: browser sign → server verify')
// Summary
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`)
if (failed > 0) {
console.error('\n❌ SIGNATURE INCOMPATIBILITY DETECTED')
console.error(' Browser signatures will NOT verify on server')
process.exit(1)
} else {
console.log('\n✅ All compatibility checks passed')
console.log(' noble-ed25519 (browser) ↔ hypercore-crypto (server) are fully compatible')
}
}
run().catch(err => {
console.error('Test error:', err)
process.exit(1)
})