Use new algorithm

Signed-off-by: Rin Cat (鈴猫) <rincat@rincat.dev>
This commit is contained in:
2026-05-05 09:42:17 +09:00
parent 4b71ad4d75
commit c2a8d77a12
8 changed files with 1297 additions and 345 deletions

View File

@@ -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
View File

@@ -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
View 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}");
}

View File

@@ -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()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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;
/// MillerRabin primality test /// MillerRabin 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 MillerRabin to test candidate for primality. // Use MillerRabin 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
View 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));
}
}

View File

@@ -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));
}
}