21
Cargo.toml
21
Cargo.toml
@@ -1,24 +1,33 @@
|
||||
[package]
|
||||
name = "chidori-pow"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib", "cdylib"]
|
||||
|
||||
[[bin]]
|
||||
name = "bench"
|
||||
path = "src/bin/bench.rs"
|
||||
required-features = ["native-bin"]
|
||||
|
||||
[features]
|
||||
native-bin = []
|
||||
|
||||
[dependencies]
|
||||
num-traits = "0.2"
|
||||
num-integer = "0.1.46"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
cfg-if = "1.0.0"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
bincode = "1.3"
|
||||
cfg-if = "1.0.4"
|
||||
base64 = "0.22.1"
|
||||
wasm-bindgen = "0.2"
|
||||
sha2 = "0.10"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
num-bigint = { version = "0.4", features = ["serde"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
num-bigint = { version="0.4", features = ["rand", "serde"] }
|
||||
rand = "0.8"
|
||||
ed25519-dalek = { version = "2.1.1", features = ["std", "rand_core"] }
|
||||
rand = "0.10"
|
||||
ed25519-dalek = { version = "2.2.0", features = ["std", "rand_core"] }
|
||||
|
||||
110
README.md
110
README.md
@@ -1,2 +1,112 @@
|
||||
# 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 {
|
||||
pub fn generate_sign_key() -> [u8; 32] {
|
||||
rand::random()
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
let signing_key = SigningKey::generate(&mut rand::thread_rng());
|
||||
ChallengeSigner { signing_key }
|
||||
ChallengeSigner::new_from_bytes(ChallengeSigner::generate_sign_key())
|
||||
}
|
||||
|
||||
pub fn new_from_bytes(bytes: [u8; 32]) -> Self {
|
||||
@@ -26,3 +29,9 @@ impl ChallengeSigner {
|
||||
self.signing_key.verify(message, &signature_result).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ChallengeSigner {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
892
src/lib.rs
892
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_traits::One;
|
||||
use rand::thread_rng;
|
||||
use rand::RngExt;
|
||||
|
||||
/// 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.
|
||||
pub fn is_probably_prime(n: &BigUint, k: u32) -> bool {
|
||||
// Handle small numbers.
|
||||
if n == &2u32.to_biguint().unwrap() {
|
||||
if n == &BigUint::from(2_u32) || n == &BigUint::from(3_u32) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if n < &2u32.to_biguint().unwrap() || n.is_even() {
|
||||
if n < &BigUint::from(2_u32) || n.is_even() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write n − 1 as d * 2^r.
|
||||
let one: BigUint = One::one();
|
||||
let two: BigUint = 2u32.to_biguint().unwrap();
|
||||
let two = BigUint::from(2_u32);
|
||||
|
||||
let n_minus_one = n - &one;
|
||||
|
||||
@@ -33,11 +33,11 @@ pub fn is_probably_prime(n: &BigUint, k: u32) -> bool {
|
||||
}
|
||||
|
||||
// Try k random witnesses.
|
||||
let mut rng = thread_rng();
|
||||
let mut rng = rand::rng();
|
||||
|
||||
'witness_loop: for _ in 0..k {
|
||||
// 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.
|
||||
let mut x = a.modpow(&d, n);
|
||||
@@ -60,14 +60,53 @@ pub fn is_probably_prime(n: &BigUint, k: u32) -> bool {
|
||||
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 {
|
||||
let mut rng = thread_rng();
|
||||
let four: BigUint = 4u32.to_biguint().unwrap();
|
||||
let three: BigUint = 3u32.to_biguint().unwrap();
|
||||
let bits = bits.max(2);
|
||||
|
||||
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 {
|
||||
// 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.
|
||||
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.
|
||||
let rem = &candidate % &four;
|
||||
if rem != three {
|
||||
// Calculate the adjustment needed.
|
||||
// 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
|
||||
};
|
||||
let adj = &three - &rem;
|
||||
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.
|
||||
if is_probably_prime(&candidate, rounds) {
|
||||
return candidate;
|
||||
@@ -108,6 +136,9 @@ mod tests {
|
||||
/// This test has chance of false positives since it's probabilistic.
|
||||
#[test]
|
||||
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![
|
||||
BigUint::parse_bytes(b"723646214863847842402314246121044767400617866176733021174245260448070467161519753555151305391831172396032179266088879736498934532967238875067731186605319314486487094813782345277515046149035823394700558031365128080643117834402421935144013956523482034192169360458395261772557972018417296402072764848759", 10).unwrap(),
|
||||
BigUint::parse_bytes(b"268263962333296278340388301081833650583348564229009436402694247537863120457689419730666587700766950987474095807568632415133217325374788947918148879191904084190506645642271822238922848768059332086841470078498514866531550241226838886983034780850983546266212727522552823301120544245546076442247019621281", 10).unwrap(),
|
||||
@@ -122,7 +153,7 @@ mod tests {
|
||||
];
|
||||
|
||||
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() {
|
||||
assert!(!is_probably_prime(&c, 64));
|
||||
assert!(!is_probably_prime(c, 64));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_prime_mod_3_4() {
|
||||
let bits = 2048;
|
||||
let bits = 9;
|
||||
let rounds = 64;
|
||||
let prime = generate_prime_mod_3_4(bits, rounds);
|
||||
assert!(is_probably_prime(&prime, rounds));
|
||||
assert_eq!(prime.bits(), bits);
|
||||
assert_eq!(
|
||||
&prime % 4u32.to_biguint().unwrap(),
|
||||
3u32.to_biguint().unwrap()
|
||||
);
|
||||
assert_eq!(&prime % BigUint::from(4_u32), BigUint::from(3_u32));
|
||||
}
|
||||
|
||||
#[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