21
Cargo.toml
21
Cargo.toml
@@ -1,24 +1,33 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "chidori-pow"
|
name = "chidori-pow"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["rlib", "cdylib"]
|
crate-type = ["rlib", "cdylib"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "bench"
|
||||||
|
path = "src/bin/bench.rs"
|
||||||
|
required-features = ["native-bin"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
native-bin = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
num-integer = "0.1.46"
|
num-integer = "0.1.46"
|
||||||
serde_json = "1.0"
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde = { version = "1.0.217", features = ["derive"] }
|
bincode = "1.3"
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0.4"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
|
sha2 = "0.10"
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
num-bigint = { version = "0.4", features = ["serde"] }
|
num-bigint = { version = "0.4", features = ["serde"] }
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
num-bigint = { version="0.4", features = ["rand", "serde"] }
|
num-bigint = { version="0.4", features = ["rand", "serde"] }
|
||||||
rand = "0.8"
|
rand = "0.10"
|
||||||
ed25519-dalek = { version = "2.1.1", features = ["std", "rand_core"] }
|
ed25519-dalek = { version = "2.2.0", features = ["std", "rand_core"] }
|
||||||
|
|||||||
110
README.md
110
README.md
@@ -1,2 +1,112 @@
|
|||||||
# chidori-pow
|
# 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.
|
||||||
|
|||||||
155
src/bin/bench.rs
Normal file
155
src/bin/bench.rs
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
use chidori_pow::{ChallengerBuilder, solve_challenge};
|
||||||
|
use std::env;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
modulus_bits: u64,
|
||||||
|
difficulty: u32,
|
||||||
|
rounds: u32,
|
||||||
|
binding_data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
fn from_args() -> Self {
|
||||||
|
let mut config = Self {
|
||||||
|
modulus_bits: 2048,
|
||||||
|
difficulty: 450_000,
|
||||||
|
rounds: 5,
|
||||||
|
binding_data: b"bench:binding".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut args = env::args().skip(1);
|
||||||
|
while let Some(arg) = args.next() {
|
||||||
|
match arg.as_str() {
|
||||||
|
"--modulus-bits" => {
|
||||||
|
config.modulus_bits = parse_next(&mut args, "--modulus-bits");
|
||||||
|
}
|
||||||
|
"--difficulty" => {
|
||||||
|
config.difficulty = parse_next(&mut args, "--difficulty");
|
||||||
|
}
|
||||||
|
"--rounds" => {
|
||||||
|
config.rounds = parse_next::<u32>(&mut args, "--rounds").max(1);
|
||||||
|
}
|
||||||
|
"--binding" => {
|
||||||
|
config.binding_data = args
|
||||||
|
.next()
|
||||||
|
.unwrap_or_else(|| usage("--binding requires a value"))
|
||||||
|
.into_bytes();
|
||||||
|
}
|
||||||
|
"--help" | "-h" => {
|
||||||
|
print_usage_and_exit();
|
||||||
|
}
|
||||||
|
_ => usage(&format!("unknown argument: {arg}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_next<T>(args: &mut impl Iterator<Item = String>, name: &str) -> T
|
||||||
|
where
|
||||||
|
T: std::str::FromStr,
|
||||||
|
{
|
||||||
|
args.next()
|
||||||
|
.unwrap_or_else(|| usage(&format!("{name} requires a value")))
|
||||||
|
.parse()
|
||||||
|
.unwrap_or_else(|_| usage(&format!("{name} has an invalid value")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_usage_and_exit() -> ! {
|
||||||
|
println!(
|
||||||
|
"Usage: cargo run --release --features native-bin --bin bench -- [--modulus-bits 2048] [--difficulty 450000] [--rounds 5] [--binding bench:binding]"
|
||||||
|
);
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage(message: &str) -> ! {
|
||||||
|
eprintln!("{message}");
|
||||||
|
eprintln!(
|
||||||
|
"Usage: cargo run --release --features native-bin --bin bench -- [--modulus-bits 2048] [--difficulty 450000] [--rounds 5] [--binding bench:binding]"
|
||||||
|
);
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timed<T>(f: impl FnOnce() -> T) -> (T, Duration) {
|
||||||
|
let start = Instant::now();
|
||||||
|
let value = f();
|
||||||
|
(value, start.elapsed())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let config = Config::from_args();
|
||||||
|
|
||||||
|
println!("modulus_bits: {}", config.modulus_bits);
|
||||||
|
println!("difficulty_steps: {}", config.difficulty);
|
||||||
|
println!("rounds: {}", config.rounds);
|
||||||
|
println!("binding_data_len: {}", config.binding_data.len());
|
||||||
|
|
||||||
|
let (challenger, build_time) = timed(|| {
|
||||||
|
match ChallengerBuilder::new()
|
||||||
|
.with_modulus_bits(config.modulus_bits)
|
||||||
|
.with_difficulty(config.difficulty)
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(challenger) => challenger,
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("build failed: {error:?}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut issue_times = Vec::with_capacity(config.rounds as usize);
|
||||||
|
let mut solve_times = Vec::with_capacity(config.rounds as usize);
|
||||||
|
let mut verify_times = Vec::with_capacity(config.rounds as usize);
|
||||||
|
let mut challenge_len = 0;
|
||||||
|
let mut solution_len = 0;
|
||||||
|
|
||||||
|
for _ in 0..config.rounds {
|
||||||
|
let (challenge, issue_time) = timed(|| match challenger.issue_challenge() {
|
||||||
|
Ok(challenge) => challenge,
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("challenge issue failed: {error:?}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let (solution, solve_time) = timed(|| solve_challenge(&challenge, &config.binding_data));
|
||||||
|
let (verified, verify_time) =
|
||||||
|
timed(|| challenger.verify_challenge(&challenge, &solution, &config.binding_data));
|
||||||
|
|
||||||
|
if !verified {
|
||||||
|
println!("verified: false");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
challenge_len = challenge.len();
|
||||||
|
solution_len = solution.len();
|
||||||
|
issue_times.push(issue_time);
|
||||||
|
solve_times.push(solve_time);
|
||||||
|
verify_times.push(verify_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("build_ms: {:.3}", build_time.as_secs_f64() * 1000.0);
|
||||||
|
print_stats("issue", &mut issue_times);
|
||||||
|
print_stats("solve", &mut solve_times);
|
||||||
|
print_stats("verify", &mut verify_times);
|
||||||
|
println!("challenge_len: {challenge_len}");
|
||||||
|
println!("solution_len: {solution_len}");
|
||||||
|
println!("verified: true");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_stats(label: &str, times: &mut [Duration]) {
|
||||||
|
times.sort();
|
||||||
|
|
||||||
|
let sum = times.iter().fold(Duration::ZERO, |acc, value| acc + *value);
|
||||||
|
let avg = sum.as_secs_f64() * 1000.0 / times.len() as f64;
|
||||||
|
let min = times[0].as_secs_f64() * 1000.0;
|
||||||
|
let p50 = times[times.len() / 2].as_secs_f64() * 1000.0;
|
||||||
|
let max = times[times.len() - 1].as_secs_f64() * 1000.0;
|
||||||
|
|
||||||
|
println!("{label}_min_ms: {min:.3}");
|
||||||
|
println!("{label}_avg_ms: {avg:.3}");
|
||||||
|
println!("{label}_p50_ms: {p50:.3}");
|
||||||
|
println!("{label}_max_ms: {max:.3}");
|
||||||
|
}
|
||||||
@@ -7,9 +7,12 @@ pub struct ChallengeSigner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ChallengeSigner {
|
impl ChallengeSigner {
|
||||||
|
pub fn generate_sign_key() -> [u8; 32] {
|
||||||
|
rand::random()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let signing_key = SigningKey::generate(&mut rand::thread_rng());
|
ChallengeSigner::new_from_bytes(ChallengeSigner::generate_sign_key())
|
||||||
ChallengeSigner { signing_key }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_from_bytes(bytes: [u8; 32]) -> Self {
|
pub fn new_from_bytes(bytes: [u8; 32]) -> Self {
|
||||||
@@ -26,3 +29,9 @@ impl ChallengeSigner {
|
|||||||
self.signing_key.verify(message, &signature_result).is_ok()
|
self.signing_key.verify(message, &signature_result).is_ok()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for ChallengeSigner {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
886
src/lib.rs
886
src/lib.rs
File diff suppressed because it is too large
Load Diff
96
src/prime.rs
96
src/prime.rs
@@ -1,7 +1,7 @@
|
|||||||
use num_bigint::{BigUint, RandBigInt, ToBigUint};
|
use num_bigint::BigUint;
|
||||||
use num_integer::Integer;
|
use num_integer::Integer;
|
||||||
use num_traits::One;
|
use num_traits::One;
|
||||||
use rand::thread_rng;
|
use rand::RngExt;
|
||||||
|
|
||||||
/// Miller–Rabin primality test
|
/// Miller–Rabin primality test
|
||||||
/// https://en.wikipedia.org/wiki/Miller-Rabin_primality_test
|
/// https://en.wikipedia.org/wiki/Miller-Rabin_primality_test
|
||||||
@@ -10,17 +10,17 @@ use rand::thread_rng;
|
|||||||
/// This is a basic implementation for odd candidate `n` greater than 2.
|
/// This is a basic implementation for odd candidate `n` greater than 2.
|
||||||
pub fn is_probably_prime(n: &BigUint, k: u32) -> bool {
|
pub fn is_probably_prime(n: &BigUint, k: u32) -> bool {
|
||||||
// Handle small numbers.
|
// Handle small numbers.
|
||||||
if n == &2u32.to_biguint().unwrap() {
|
if n == &BigUint::from(2_u32) || n == &BigUint::from(3_u32) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if n < &2u32.to_biguint().unwrap() || n.is_even() {
|
if n < &BigUint::from(2_u32) || n.is_even() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write n − 1 as d * 2^r.
|
// Write n − 1 as d * 2^r.
|
||||||
let one: BigUint = One::one();
|
let one: BigUint = One::one();
|
||||||
let two: BigUint = 2u32.to_biguint().unwrap();
|
let two = BigUint::from(2_u32);
|
||||||
|
|
||||||
let n_minus_one = n - &one;
|
let n_minus_one = n - &one;
|
||||||
|
|
||||||
@@ -33,11 +33,11 @@ pub fn is_probably_prime(n: &BigUint, k: u32) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try k random witnesses.
|
// Try k random witnesses.
|
||||||
let mut rng = thread_rng();
|
let mut rng = rand::rng();
|
||||||
|
|
||||||
'witness_loop: for _ in 0..k {
|
'witness_loop: for _ in 0..k {
|
||||||
// Choose a random a in [2, n-2]
|
// Choose a random a in [2, n-2]
|
||||||
let a = rng.gen_biguint_range(&two, &(n - &two));
|
let a = random_biguint_range(&mut rng, &two, &n_minus_one);
|
||||||
|
|
||||||
// Compute x = a^d mod n.
|
// Compute x = a^d mod n.
|
||||||
let mut x = a.modpow(&d, n);
|
let mut x = a.modpow(&d, n);
|
||||||
@@ -60,14 +60,53 @@ pub fn is_probably_prime(n: &BigUint, k: u32) -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn random_biguint_range<R: rand::Rng + ?Sized>(
|
||||||
|
rng: &mut R,
|
||||||
|
low: &BigUint,
|
||||||
|
high: &BigUint,
|
||||||
|
) -> BigUint {
|
||||||
|
if low >= high {
|
||||||
|
return low.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let bits = high.bits();
|
||||||
|
let byte_len = bits.div_ceil(8) as usize;
|
||||||
|
let excess_bits = byte_len * 8 - bits as usize;
|
||||||
|
let mut bytes = vec![0_u8; byte_len];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
rng.fill(&mut bytes);
|
||||||
|
|
||||||
|
if excess_bits > 0 {
|
||||||
|
bytes[0] &= 0xff >> excess_bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
let candidate = BigUint::from_bytes_be(&bytes);
|
||||||
|
if &candidate >= low && &candidate < high {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn generate_prime_mod_3_4(bits: u64, rounds: u32) -> BigUint {
|
pub fn generate_prime_mod_3_4(bits: u64, rounds: u32) -> BigUint {
|
||||||
let mut rng = thread_rng();
|
let bits = bits.max(2);
|
||||||
let four: BigUint = 4u32.to_biguint().unwrap();
|
|
||||||
let three: BigUint = 3u32.to_biguint().unwrap();
|
let mut rng = rand::rng();
|
||||||
|
let byte_len = bits.div_ceil(8) as usize;
|
||||||
|
let excess_bits = byte_len * 8 - bits as usize;
|
||||||
|
let mut bytes = vec![0_u8; byte_len];
|
||||||
|
let four = BigUint::from(4_u32);
|
||||||
|
let three = BigUint::from(3_u32);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Generate a random number of the required bit-length.
|
// Generate a random number of the required bit-length.
|
||||||
let mut candidate = rng.gen_biguint(bits);
|
rng.fill(&mut bytes);
|
||||||
|
|
||||||
|
if excess_bits > 0 {
|
||||||
|
bytes[0] &= 0xff >> excess_bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut candidate = BigUint::from_bytes_be(&bytes);
|
||||||
|
|
||||||
// Force the most significant bit to ensure it is exactly `bits` long.
|
// Force the most significant bit to ensure it is exactly `bits` long.
|
||||||
candidate.set_bit(bits - 1, true);
|
candidate.set_bit(bits - 1, true);
|
||||||
@@ -78,21 +117,10 @@ pub fn generate_prime_mod_3_4(bits: u64, rounds: u32) -> BigUint {
|
|||||||
// Compute candidate mod 4.
|
// Compute candidate mod 4.
|
||||||
let rem = &candidate % &four;
|
let rem = &candidate % &four;
|
||||||
if rem != three {
|
if rem != three {
|
||||||
// Calculate the adjustment needed.
|
let adj = &three - &rem;
|
||||||
// We want candidate + adj ≡ 3 (mod 4); that is, adj ≡ (3 - rem) mod 4.
|
|
||||||
let adj = if three >= rem {
|
|
||||||
&three - &rem
|
|
||||||
} else {
|
|
||||||
&three + &four - &rem
|
|
||||||
};
|
|
||||||
candidate += &adj;
|
candidate += &adj;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now candidate mod 4 should equal 3.
|
|
||||||
if &candidate % &four != three {
|
|
||||||
continue; // Should not happen, but be safe.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use Miller–Rabin to test candidate for primality.
|
// Use Miller–Rabin to test candidate for primality.
|
||||||
if is_probably_prime(&candidate, rounds) {
|
if is_probably_prime(&candidate, rounds) {
|
||||||
return candidate;
|
return candidate;
|
||||||
@@ -108,6 +136,9 @@ mod tests {
|
|||||||
/// This test has chance of false positives since it's probabilistic.
|
/// This test has chance of false positives since it's probabilistic.
|
||||||
#[test]
|
#[test]
|
||||||
fn test_is_probably_prime() {
|
fn test_is_probably_prime() {
|
||||||
|
assert!(is_probably_prime(&BigUint::from(2_u32), 64));
|
||||||
|
assert!(is_probably_prime(&BigUint::from(3_u32), 64));
|
||||||
|
|
||||||
let primes: Vec<BigUint> = vec![
|
let primes: Vec<BigUint> = vec![
|
||||||
BigUint::parse_bytes(b"723646214863847842402314246121044767400617866176733021174245260448070467161519753555151305391831172396032179266088879736498934532967238875067731186605319314486487094813782345277515046149035823394700558031365128080643117834402421935144013956523482034192169360458395261772557972018417296402072764848759", 10).unwrap(),
|
BigUint::parse_bytes(b"723646214863847842402314246121044767400617866176733021174245260448070467161519753555151305391831172396032179266088879736498934532967238875067731186605319314486487094813782345277515046149035823394700558031365128080643117834402421935144013956523482034192169360458395261772557972018417296402072764848759", 10).unwrap(),
|
||||||
BigUint::parse_bytes(b"268263962333296278340388301081833650583348564229009436402694247537863120457689419730666587700766950987474095807568632415133217325374788947918148879191904084190506645642271822238922848768059332086841470078498514866531550241226838886983034780850983546266212727522552823301120544245546076442247019621281", 10).unwrap(),
|
BigUint::parse_bytes(b"268263962333296278340388301081833650583348564229009436402694247537863120457689419730666587700766950987474095807568632415133217325374788947918148879191904084190506645642271822238922848768059332086841470078498514866531550241226838886983034780850983546266212727522552823301120544245546076442247019621281", 10).unwrap(),
|
||||||
@@ -122,7 +153,7 @@ mod tests {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for p in primes.iter() {
|
for p in primes.iter() {
|
||||||
assert!(is_probably_prime(&p, 64));
|
assert!(is_probably_prime(p, 64));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,20 +173,25 @@ mod tests {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for c in composites.iter() {
|
for c in composites.iter() {
|
||||||
assert!(!is_probably_prime(&c, 64));
|
assert!(!is_probably_prime(c, 64));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_generate_prime_mod_3_4() {
|
fn test_generate_prime_mod_3_4() {
|
||||||
let bits = 2048;
|
let bits = 9;
|
||||||
let rounds = 64;
|
let rounds = 64;
|
||||||
let prime = generate_prime_mod_3_4(bits, rounds);
|
let prime = generate_prime_mod_3_4(bits, rounds);
|
||||||
assert!(is_probably_prime(&prime, rounds));
|
assert!(is_probably_prime(&prime, rounds));
|
||||||
assert_eq!(prime.bits(), bits);
|
assert_eq!(prime.bits(), bits);
|
||||||
assert_eq!(
|
assert_eq!(&prime % BigUint::from(4_u32), BigUint::from(3_u32));
|
||||||
&prime % 4u32.to_biguint().unwrap(),
|
}
|
||||||
3u32.to_biguint().unwrap()
|
|
||||||
);
|
#[test]
|
||||||
|
fn test_random_biguint_range_handles_empty_range() {
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
let value = BigUint::from(7_u32);
|
||||||
|
|
||||||
|
assert_eq!(value, random_biguint_range(&mut rng, &value, &value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
250
src/rsa.rs
Normal file
250
src/rsa.rs
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
use num_bigint::BigUint;
|
||||||
|
use num_traits::One;
|
||||||
|
use sha2::{Digest, Sha256, Sha512};
|
||||||
|
|
||||||
|
const DOMAIN: &[u8] = b"chidori-pow-rsa-v1";
|
||||||
|
const SCHEDULE_DOMAIN: &[u8] = b"chidori-pow-rsa-schedule-v1";
|
||||||
|
const SCHEDULE_BLOCK_STEPS: u32 = 64;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub struct RsaTrapdoor {
|
||||||
|
pub modulus: BigUint,
|
||||||
|
lambda: BigUint,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
impl RsaTrapdoor {
|
||||||
|
pub fn generate(modulus_bits: u64, rounds: u32) -> Self {
|
||||||
|
generate_with(modulus_bits, |bits| {
|
||||||
|
crate::prime::generate_prime_mod_3_4(bits, rounds)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_factors(p: BigUint, q: BigUint) -> Self {
|
||||||
|
use num_integer::Integer;
|
||||||
|
use num_traits::One;
|
||||||
|
|
||||||
|
let modulus = &p * &q;
|
||||||
|
let lambda = (&p - BigUint::one()).lcm(&(&q - BigUint::one()));
|
||||||
|
|
||||||
|
Self { modulus, lambda }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify(
|
||||||
|
&self,
|
||||||
|
base: &BigUint,
|
||||||
|
solution: &BigUint,
|
||||||
|
payload: &[u8],
|
||||||
|
binding_data: &[u8],
|
||||||
|
difficulty: u32,
|
||||||
|
) -> bool {
|
||||||
|
let exponent = derive_exponent(payload, binding_data, difficulty, &self.lambda);
|
||||||
|
let expected = base.modpow(&exponent, &self.modulus);
|
||||||
|
&expected == solution
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn generate_with(modulus_bits: u64, mut generate_prime: impl FnMut(u64) -> BigUint) -> RsaTrapdoor {
|
||||||
|
let modulus_bits = modulus_bits.max(crate::MIN_RSA_MODULUS_BITS);
|
||||||
|
let factor_bits = modulus_bits.div_ceil(2);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let p = generate_prime(factor_bits);
|
||||||
|
let q = generate_prime(factor_bits);
|
||||||
|
if q == p {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let trapdoor = RsaTrapdoor::from_factors(p, q);
|
||||||
|
if trapdoor.modulus.bits() < modulus_bits {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trapdoor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn solve(
|
||||||
|
base: &BigUint,
|
||||||
|
modulus: &BigUint,
|
||||||
|
payload: &[u8],
|
||||||
|
binding_data: &[u8],
|
||||||
|
difficulty: u32,
|
||||||
|
) -> BigUint {
|
||||||
|
let mut solution = base.clone();
|
||||||
|
let schedule = Schedule::new(payload, binding_data);
|
||||||
|
|
||||||
|
for step in 0..difficulty {
|
||||||
|
solution = (&solution * &solution) % modulus;
|
||||||
|
if schedule.cube_after_square(step) {
|
||||||
|
solution = (&solution * &solution * &solution) % modulus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
solution
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn derive_exponent(
|
||||||
|
payload: &[u8],
|
||||||
|
binding_data: &[u8],
|
||||||
|
difficulty: u32,
|
||||||
|
modulus: &BigUint,
|
||||||
|
) -> BigUint {
|
||||||
|
let mut exponent = BigUint::one();
|
||||||
|
let schedule = Schedule::new(payload, binding_data);
|
||||||
|
let two = BigUint::from(2_u32);
|
||||||
|
let mut step = 0_u32;
|
||||||
|
|
||||||
|
while step < difficulty {
|
||||||
|
let square_steps = SCHEDULE_BLOCK_STEPS.min(difficulty - step);
|
||||||
|
exponent *= two.modpow(&BigUint::from(square_steps), modulus);
|
||||||
|
exponent %= modulus;
|
||||||
|
|
||||||
|
if schedule.cube_after_square(step) {
|
||||||
|
exponent *= 3_u32;
|
||||||
|
exponent %= modulus;
|
||||||
|
}
|
||||||
|
|
||||||
|
step += square_steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
exponent
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Schedule {
|
||||||
|
seed: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Schedule {
|
||||||
|
fn new(payload: &[u8], binding_data: &[u8]) -> Self {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(SCHEDULE_DOMAIN);
|
||||||
|
hasher.update((payload.len() as u64).to_be_bytes());
|
||||||
|
hasher.update(payload);
|
||||||
|
hasher.update((binding_data.len() as u64).to_be_bytes());
|
||||||
|
hasher.update(binding_data);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
seed: hasher.finalize().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cube_after_square(&self, step: u32) -> bool {
|
||||||
|
if !step.is_multiple_of(SCHEDULE_BLOCK_STEPS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let block = step / SCHEDULE_BLOCK_STEPS;
|
||||||
|
let byte = self.seed[(block as usize) % self.seed.len()];
|
||||||
|
let bit = (byte >> (block % 8)) & 1;
|
||||||
|
bit == 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn derive_base(payload: &[u8], binding_data: &[u8], modulus: &BigUint) -> BigUint {
|
||||||
|
if modulus <= &BigUint::one() {
|
||||||
|
return BigUint::one();
|
||||||
|
}
|
||||||
|
|
||||||
|
let byte_len = modulus.bits().div_ceil(8) as usize + 16;
|
||||||
|
let mut bytes = Vec::with_capacity(byte_len);
|
||||||
|
let mut counter = 0_u64;
|
||||||
|
|
||||||
|
while bytes.len() < byte_len {
|
||||||
|
let mut hasher = Sha512::new();
|
||||||
|
hasher.update(DOMAIN);
|
||||||
|
hasher.update((payload.len() as u64).to_be_bytes());
|
||||||
|
hasher.update(payload);
|
||||||
|
hasher.update((binding_data.len() as u64).to_be_bytes());
|
||||||
|
hasher.update(binding_data);
|
||||||
|
hasher.update(counter.to_be_bytes());
|
||||||
|
bytes.extend_from_slice(&hasher.finalize());
|
||||||
|
counter += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes.truncate(byte_len);
|
||||||
|
|
||||||
|
let one = BigUint::one();
|
||||||
|
let range = modulus - &one;
|
||||||
|
(BigUint::from_bytes_be(&bytes) % range) + one
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rsa_trapdoor() {
|
||||||
|
let rsa = RsaTrapdoor::generate(512, 16);
|
||||||
|
let payload = b"payload";
|
||||||
|
let binding_data = b"app-owned binding data";
|
||||||
|
let difficulty = 10;
|
||||||
|
|
||||||
|
let base = derive_base(payload, binding_data, &rsa.modulus);
|
||||||
|
let solution = solve(&base, &rsa.modulus, payload, binding_data, difficulty);
|
||||||
|
|
||||||
|
assert!(rsa.verify(&base, &solution, payload, binding_data, difficulty));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generate_retries_equal_factors() {
|
||||||
|
let p: BigUint = (BigUint::one() << 256) + BigUint::one();
|
||||||
|
let q: BigUint = (BigUint::one() << 256) + BigUint::from(3_u32);
|
||||||
|
|
||||||
|
let values = [
|
||||||
|
p.clone(),
|
||||||
|
p.clone(),
|
||||||
|
BigUint::from(3_u32),
|
||||||
|
BigUint::from(5_u32),
|
||||||
|
p,
|
||||||
|
q,
|
||||||
|
];
|
||||||
|
let mut index = 0;
|
||||||
|
let rsa = generate_with(crate::MIN_RSA_MODULUS_BITS, |_| {
|
||||||
|
let value = values[index].clone();
|
||||||
|
index += 1;
|
||||||
|
value
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(rsa.modulus.bits() >= crate::MIN_RSA_MODULUS_BITS);
|
||||||
|
assert_eq!(values.len(), index);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_derive_base_handles_invalid_modulus() {
|
||||||
|
assert_eq!(
|
||||||
|
BigUint::one(),
|
||||||
|
derive_base(b"payload", b"binding", &BigUint::one())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_binding_data_is_bound() {
|
||||||
|
let rsa = RsaTrapdoor::generate(512, 16);
|
||||||
|
let payload = b"payload";
|
||||||
|
let difficulty = 10;
|
||||||
|
|
||||||
|
let base = derive_base(payload, b"expected", &rsa.modulus);
|
||||||
|
let solution = solve(&base, &rsa.modulus, payload, b"expected", difficulty);
|
||||||
|
|
||||||
|
let wrong_base = derive_base(payload, b"wrong", &rsa.modulus);
|
||||||
|
assert!(!rsa.verify(&wrong_base, &solution, payload, b"wrong", difficulty));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_schedule_changes_solution() {
|
||||||
|
let rsa = RsaTrapdoor::generate(512, 16);
|
||||||
|
let payload = b"payload";
|
||||||
|
let difficulty = 128;
|
||||||
|
|
||||||
|
let base_a = derive_base(payload, b"a", &rsa.modulus);
|
||||||
|
let base_b = derive_base(payload, b"b", &rsa.modulus);
|
||||||
|
let solution_a = solve(&base_a, &rsa.modulus, payload, b"a", difficulty);
|
||||||
|
let solution_b = solve(&base_b, &rsa.modulus, payload, b"b", difficulty);
|
||||||
|
|
||||||
|
assert_ne!(solution_a, solution_b);
|
||||||
|
assert!(rsa.verify(&base_a, &solution_a, payload, b"a", difficulty));
|
||||||
|
assert!(rsa.verify(&base_b, &solution_b, payload, b"b", difficulty));
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/sloth.rs
111
src/sloth.rs
@@ -1,111 +0,0 @@
|
|||||||
use num_bigint::{BigUint, ToBigUint};
|
|
||||||
use num_traits::One;
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
use rand::thread_rng;
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
use num_bigint::RandBigInt;
|
|
||||||
|
|
||||||
/// Sloth is a verifiable delay function (VDF) that is designed to enforce
|
|
||||||
/// a predetermined amount of sequential work.
|
|
||||||
|
|
||||||
pub struct Sloth {
|
|
||||||
/// The modulus `p` is a large prime number.
|
|
||||||
pub p: BigUint,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sloth {
|
|
||||||
/// Create a new Sloth VDF with the given modulus `p` and iteration count `t`.
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
pub fn new(p: BigUint) -> Self {
|
|
||||||
Sloth { p }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encode a message `x` using the Sloth VDF.
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
pub fn encode(&self, x: &BigUint, t: u32) -> BigUint {
|
|
||||||
let mut y = x.clone();
|
|
||||||
|
|
||||||
// Repeatedly square `x` T times mod `p`.
|
|
||||||
for _ in 0..t {
|
|
||||||
// y = y^2 mod p
|
|
||||||
y = (&y * &y) % &self.p;
|
|
||||||
}
|
|
||||||
y
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decode a message `y` using the Sloth VDF.
|
|
||||||
pub fn decode(y: &BigUint, p: &BigUint, t: u32) -> BigUint {
|
|
||||||
let mut x = y.clone();
|
|
||||||
for _ in 0..t {
|
|
||||||
x = Sloth::mod_sqrt(&x, p);
|
|
||||||
}
|
|
||||||
x
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new tuple `(x, y)` where `x` is a random secret number
|
|
||||||
/// and `y` is the encoded challenge value.
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
pub fn create(&self, t: u32) -> (BigUint, BigUint, BigUint) {
|
|
||||||
let x = thread_rng().gen_biguint_range(&BigUint::one(), &self.p);
|
|
||||||
let y = self.encode(&x, t);
|
|
||||||
(x, y, self.p.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify that the encoded value `y` was computed from the secret `x`.
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
pub fn verify(&self, x: &BigUint, y: &BigUint, t: u32) -> bool {
|
|
||||||
let check = self.encode(x, t);
|
|
||||||
&check == y
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute a modular square root when p ≡ 3 mod 4:
|
|
||||||
/// sqrt(a) mod p = a^((p+1)/4) mod p
|
|
||||||
/// We'll pick the "lower root" consistently to ensure uniqueness.
|
|
||||||
fn mod_sqrt(a: &BigUint, p: &BigUint) -> BigUint {
|
|
||||||
// exponent = (p + 1) / 4
|
|
||||||
let exp = (p + BigUint::one()) / 4_u32.to_biguint().unwrap();
|
|
||||||
let root = a.modpow(&exp, p);
|
|
||||||
|
|
||||||
// A prime p ≡ 3 mod 4 has exactly two roots: `r` and `p-r`.
|
|
||||||
// We'll choose the smaller one to ensure uniqueness.
|
|
||||||
let p_minus_root = (p - &root) % p;
|
|
||||||
if root <= p_minus_root {
|
|
||||||
root
|
|
||||||
} else {
|
|
||||||
p_minus_root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sloth() {
|
|
||||||
let p = crate::prime::generate_prime_mod_3_4(2048, 64);
|
|
||||||
let t = 100;
|
|
||||||
let sloth = Sloth::new(p);
|
|
||||||
|
|
||||||
let (x, y, p) = sloth.create(t);
|
|
||||||
assert!(sloth.verify(&x, &y, t));
|
|
||||||
|
|
||||||
let decoded = Sloth::decode(&y, &p, t);
|
|
||||||
assert!(sloth.verify(&decoded, &y, t));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sloth_incorrect() {
|
|
||||||
let p = crate::prime::generate_prime_mod_3_4(2048, 64);
|
|
||||||
let t = 100;
|
|
||||||
let sloth = Sloth::new(p);
|
|
||||||
|
|
||||||
let (x, y, _p) = sloth.create(t);
|
|
||||||
assert!(sloth.verify(&x, &y, t));
|
|
||||||
|
|
||||||
let decoded = 1024_u32.to_biguint().unwrap();
|
|
||||||
assert!(!sloth.verify(&decoded, &y, t));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user