import { verifySignature } from './crypto.js'; const DEFAULT_TIMESTAMP_WINDOW_MS = 300000; function verifyTimestamp(timestamp, windowMs) { const now = Date.now(); const diff = Math.abs(now - timestamp); return diff <= windowMs; } function verifySignaturesStrict(hashValue, signatures, allowedPubkeys) { let valid = 0; let unauthorized = 0; for (const s of signatures) { const messageToVerify = `${hashValue}-${s.nonce}`; const cryptoOk = verifySignature(messageToVerify, s.signature, s.cle_publique); if (!cryptoOk) { continue; } if (allowedPubkeys.has(s.cle_publique)) { valid++; } else { unauthorized++; } } return { valid, unauthorized }; } /** * Verify login proof: crypto, allowed pubkeys, timestamp window, nonce anti-replay. * Service must provide allowedPubkeys (from validators) and a NonceCache. */ export function verifyLoginProof(proof, ctx) { if (ctx.allowedPubkeys.size === 0) { return { accept: false, reason: 'validators_not_verifiable', }; } const windowMs = ctx.timestampWindowMs ?? DEFAULT_TIMESTAMP_WINDOW_MS; if (!verifyTimestamp(proof.challenge.timestamp, windowMs)) { return { accept: false, reason: 'timestamp_out_of_window', }; } if (!ctx.nonceCache.isValid(proof.challenge.nonce, proof.challenge.timestamp)) { return { accept: false, reason: 'nonce_reused', }; } const hashValue = proof.challenge.hash; const { valid, unauthorized } = verifySignaturesStrict(hashValue, proof.signatures, ctx.allowedPubkeys); if (valid === 0) { return { accept: false, reason: 'no_validator_signature', }; } if (unauthorized > 0) { return { accept: false, reason: 'signature_cle_publique_not_authorized', }; } return { accept: true }; }