igneum/proto-vdf/src/rsa.rs
igneum-labs ffa7d5de48 Brand: icon and wordmark PNG set for email and profiles
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 16:21:33 +00:00

108 lines
3.4 KiB
Rust

//! RSA-style group Z_N^* with a 2048-bit modulus.
//!
//! TRUSTED-SETUP STAND-IN. NOT FOR PRODUCTION.
//! Whoever knows the factors of N can compute x^(2^T) in two short exponentiations
//! (reduce 2^T mod phi(N)) and skip the delay entirely. In this prototype the factors are
//! derived from a PUBLIC seed string, so the trapdoor is public by construction. The group
//! exists here only to measure squaring speed and to exercise the Wesolowski code against a
//! second group. The production group is the class group in `classgroup.rs`.
//!
//! Soundness note for the record: Wesolowski over Z_N^* needs the low-order assumption, which
//! fails for -1 (order 2). Real deployments work in QR_N or in Z_N^*/{+-1}. We map inputs into
//! QR_N by squaring and do not canonicalise signs, which is enough for timing.
use crate::group::Group;
use crate::hash::{bytes_to_int, int_to_bytes, sha256};
use rug::integer::Order;
use rug::rand::RandState;
use rug::Integer;
pub const MODULUS_BITS: u32 = 2048;
pub struct RsaGroup {
pub n: Integer,
}
impl RsaGroup {
/// Deterministic modulus from a public seed. See the module note: the factors are public.
pub fn from_public_seed(seed: &str) -> RsaGroup {
let mut rs = RandState::new_mersenne_twister();
let s = sha256(&[b"igneum-vdf-rsa-standin", seed.as_bytes()]);
rs.seed(&Integer::from_digits(&s, Order::MsfBe));
let half = MODULUS_BITS / 2;
let gen_prime = |rs: &mut RandState| -> Integer {
let mut p = Integer::from(Integer::random_bits(half, rs));
p.set_bit(half - 1, true);
p.set_bit(half - 2, true);
p.set_bit(0, true);
p.next_prime_mut();
p
};
let p = gen_prime(&mut rs);
let mut q = gen_prime(&mut rs);
while q == p {
q = gen_prime(&mut rs);
}
let n = Integer::from(&p * &q);
assert_eq!(n.significant_bits(), MODULUS_BITS);
RsaGroup { n }
}
/// Hash to an element of QR_N.
pub fn hash_to_elem(&self, data: &[u8]) -> Integer {
let mut acc = Vec::new();
let mut i: u32 = 0;
while acc.len() * 8 < MODULUS_BITS as usize {
acc.extend_from_slice(&sha256(&[b"igneum-vdf-rsa-x", data, &i.to_be_bytes()]));
i += 1;
}
let mut x = bytes_to_int(&acc);
x %= &self.n;
x.square_mut();
x %= &self.n;
x
}
}
impl Group for RsaGroup {
type Elem = Integer;
fn name(&self) -> String {
format!("RSA-{} stand-in (trusted setup, public trapdoor)", MODULUS_BITS)
}
fn identity(&self) -> Integer {
Integer::from(1)
}
fn square(&self, x: &mut Integer) {
x.square_mut();
*x %= &self.n;
}
fn mul(&self, a: &Integer, b: &Integer) -> Integer {
let mut r = Integer::from(a * b);
r %= &self.n;
r
}
fn serialize(&self, x: &Integer) -> Vec<u8> {
int_to_bytes(x, (MODULUS_BITS / 8) as usize)
}
fn deserialize(&self, b: &[u8]) -> Option<Integer> {
if b.len() != (MODULUS_BITS / 8) as usize {
return None;
}
let x = bytes_to_int(b);
if self.is_valid(&x) {
Some(x)
} else {
None
}
}
fn is_valid(&self, x: &Integer) -> bool {
x.cmp0() == std::cmp::Ordering::Greater && *x < self.n
}
}