# chidori-pow Anti-bruteforce proof-of-work. ## Design The challenge flow uses an RSA trapdoor repeated-squaring puzzle: - the server generates or loads RSA factors at process start; - the signed binary challenge payload contains the public modulus and exact difficulty step count; - the client performs scheduled sequential modular squaring; - the server verifies cheaply with the private trapdoor. The default RSA modulus is 2048 bits, and applications may set a different modulus at initialization. Generated moduli use at least 512 bits. The default difficulty is `450000` scheduled RSA work steps. Applications should choose modulus and step counts from their own benchmarks and risk policy. Challenges are signed with Ed25519. The default builder generates a fresh signing key and RSA factors at process start. Persist or inject keys and factors if restart continuity matters. `ChallengerBuilder::build()` and `Challenger::issue_challenge()` return `Result`s. Generated factors are clamped to the minimum modulus size. `with_factors(p, q)` injects persisted RSA prime factors and validates that they are distinct primes with a large enough product. The challenge string is `base64url(payload || signature)`. `payload` is the bincode-serialized `Puzzle` bytes, `signature` is the fixed 64-byte Ed25519 signature suffix, and the signature covers the raw payload bytes. ## Binding Data Applications may bind their own opaque bytes into the puzzle without sending those bytes in the challenge: ```rust let solution = solve_challenge(&challenge, app_binding_data); challenger.verify_challenge(&challenge, &solution, expected_binding_data); ``` The library does not interpret `binding_data`; callers are responsible for canonicalizing it. Recommended `binding_data` contents are app-specific request context such as flow name, route, normalized username hash, CSRF token, form nonce, and a versioned site-specific prefix. Do not put passwords or raw secrets in `binding_data`. Mismatched binding data fails exactly like an invalid proof-of-work solution. Verification returns `false` for malformed, expired, replayed, mismatched, or internally unavailable challenges. ## Native Example ```rust use chidori_pow::{ChallengerBuilder, solve_challenge}; let binding_data = b"site-login-v1\0/login\0user-hash"; let challenger = ChallengerBuilder::new() .with_modulus_bits(2048) .with_difficulty(450_000) .build()?; let challenge = challenger.issue_challenge()?; let solution = solve_challenge(&challenge, binding_data); assert!(challenger.verify_challenge( &challenge, &solution, binding_data, )); ``` ## Browser/WASM The wasm package exports: ```ts solve_challenge(challenge: string, binding_data: Uint8Array): string ``` The browser/app code is responsible for constructing the same canonical `binding_data` bytes that the server will later use for verification. Pass an empty `Uint8Array` when no app-specific binding data is needed. The browser solver checks the decoded puzzle before solving. It accepts modulus sizes from 512 to 8192 bits and difficulty up to `10000000` scheduled steps. ## Benchmark Run the native benchmark with: ```sh cargo run --release --features native-bin --bin bench -- \ --modulus-bits 2048 \ --difficulty 450000 \ --rounds 5 \ --binding login:user=a ``` The benchmark builds one challenger, then measures repeated issue/solve/verify rounds and prints min/average/p50/max timings. ## Replay Cache Solved challenge tickets are remembered for the current and previous validity windows. The default cache capacity is `250000` tickets per window. When the current window is full, verification fails closed instead of evicting entries, so replay protection is preserved at the cost of rejecting additional solves until the next rotation.