arcana/rsa/rsa.rs
1//! RSA core operations (RFC 8017 / PKCS#1 v2.2): key generation,
2//! raw encrypt / decrypt with the Chinese Remainder Theorem (CRT).
3//!
4//! Supports key sizes from 1024 to 4096 bits (tested values
5//! 1024 / 2048 / 3072 / 4096). Arbitrary widths are supported by
6//! the [`super::bigint::BigInt`] arithmetic but keygen above ~4096
7//! bits gets impractical with the current schoolbook multiplier.
8//!
9//! # Side-channel posture
10//!
11//! This module is the **highest-priority evaluation gap** on the
12//! classical side as of 2026-04-21:
13//!
14//! | Threat | Status | Roadmap item |
15//! |------------------------------------|----------------|------------------------------------------------------------|
16//! | Bellcore single-fault on RSA-CRT | **vulnerable** | `T1-C` — Aumüller 2002, formally verified Rauzy-Guilley |
17//! | SPA on modular exponentiation | partial | `T1-E` — bigint CT audit + `black_box` shielding |
18//! | DPA on Montgomery multiplication | **vulnerable** | `T2-I` — message blinding (Coron 1999) |
19//! | Timing on bigint operations | partial | `T1-E` |
20//! | Padding-oracle (PKCS#1 v1.5) | partial | `T2-J` — RFC 8017 §7.2.2 CT padding-oracle handling |
21//!
22//! The Bellcore attack (Boneh-DeMillo-Lipton 1997 → JoC 2001)
23//! computes `gcd(N, S - S')` where `S'` is a CRT-faulted
24//! signature: a single fault on either half-exponentiation
25//! reveals `p` or `q`, which factors `N` and recovers the entire
26//! secret key. Equipment cost: ~1 k€ for a Chipwhisperer voltage
27//! glitcher, days of bench time for a skilled operator.
28//! **Aumüller's countermeasure resists all single-fault attacks**
29//! under the formal model of `rauzy2013_formal_crt_rsa`.
30//!
31//! See `arcana/doc/sca/countermeasures/rsa.rst` for the full
32//! threat model, the implementation route for each item, and the
33//! published references.
34//!
35//! # Zeroize-on-Drop
36//!
37//! [`RsaSecretKey`] currently does **not** implement `Drop` with
38//! `silentops::ct_zeroize`. Callers handling a `RsaSecretKey`
39//! must zeroize the underlying `BigInt` storage explicitly when
40//! it leaves scope. Roadmap item `T2-E`.
41
42use super::bigint::BigInt;
43
44/// RSA public key.
45#[derive(Clone, Debug)]
46pub struct RsaPublicKey {
47 /// Modulus n = p * q.
48 pub n: BigInt,
49 /// Public exponent (typically 65537).
50 pub e: BigInt,
51}
52
53/// RSA secret key with CRT components.
54#[derive(Clone, Debug)]
55pub struct RsaSecretKey {
56 /// Modulus n = p * q.
57 pub n: BigInt,
58 /// Private exponent d = e^{-1} mod lcm(p-1, q-1).
59 pub d: BigInt,
60 /// First prime factor.
61 pub p: BigInt,
62 /// Second prime factor.
63 pub q: BigInt,
64 /// d mod (p-1) for CRT.
65 pub dp: BigInt,
66 /// d mod (q-1) for CRT.
67 pub dq: BigInt,
68 /// q^{-1} mod p for CRT.
69 pub qinv: BigInt,
70}
71
72impl RsaPublicKey {
73 /// Byte length of the modulus.
74 pub fn modulus_byte_len(&self) -> usize {
75 self.n.byte_len()
76 }
77}
78
79impl RsaSecretKey {
80 /// Byte length of the modulus.
81 pub fn modulus_byte_len(&self) -> usize {
82 self.n.byte_len()
83 }
84}
85
86/// Generate an RSA key pair of the given bit size.
87///
88/// `rng` is a callback that fills a buffer with random bytes.
89/// Typical bit sizes: 1024 (testing only), 2048, 3072, 4096.
90pub fn rsa_keygen(bits: usize, rng: &mut dyn FnMut(&mut [u8])) -> (RsaPublicKey, RsaSecretKey) {
91 let half = bits / 2;
92 let e = BigInt::from_u64(65537);
93
94 loop {
95 let p = BigInt::random_prime(half, rng);
96 let q = BigInt::random_prime(half, rng);
97
98 if p == q {
99 continue;
100 }
101
102 let n = p.mul(&q);
103 if n.bit_len() != bits {
104 continue;
105 }
106
107 let one = BigInt::from_u64(1);
108 let p1 = p.sub(&one);
109 let q1 = q.sub(&one);
110
111 // Compute lcm(p-1, q-1) = (p-1)*(q-1) / gcd(p-1, q-1)
112 let phi = p1.mul(&q1);
113 let g = gcd(&p1, &q1);
114 let lambda = phi.div_rem(&g).0;
115
116 // d = e^{-1} mod lambda
117 let d = match e.mod_inv(&lambda) {
118 Some(d) => d,
119 None => continue,
120 };
121
122 // CRT components
123 let dp = d.rem(&p1);
124 let dq = d.rem(&q1);
125 let qinv = match q.mod_inv(&p) {
126 Some(qi) => qi,
127 None => continue,
128 };
129
130 let pk = RsaPublicKey { n: n.clone(), e };
131 let sk = RsaSecretKey {
132 n,
133 d,
134 p,
135 q,
136 dp,
137 dq,
138 qinv,
139 };
140 return (pk, sk);
141 }
142}
143
144/// Raw RSA encryption: `m^e mod n`.
145///
146/// Operates on the public modulus and the public message; **no
147/// secret material flows through this function**, so SCA is not a
148/// concern here. The public exponent `e` (typically 65537) leaks
149/// trivially through control flow, which is fine.
150pub fn rsa_encrypt_raw(pk: &RsaPublicKey, m: &BigInt) -> BigInt {
151 m.mod_exp(&pk.e, &pk.n)
152}
153
154/// Raw RSA decryption with the Chinese Remainder Theorem:
155/// computes `c^d mod n` via the CRT half-exponentiations
156///
157/// ```text
158/// m_p = c^{dp} mod p, m_q = c^{dq} mod q,
159/// m = m_q + q * (qinv * (m_p - m_q) mod p)
160/// ```
161///
162/// # ⚠ Side-channel surface (evaluation-critical)
163///
164/// This is the function targeted by the **Bellcore attack**
165/// (Boneh-DeMillo-Lipton 1997): a single fault on either
166/// `m_p` or `m_q` produces a faulted signature `S'` such that
167/// `gcd(N, S - S')` reveals `p` or `q`, which factors `N` and
168/// breaks the key. **Aumüller's countermeasure
169/// (`aumuller2002rsa_crt`) is the standard fix and is NOT yet
170/// implemented in arcana** — roadmap item `T1-C` (see
171/// `arcana/doc/sca/countermeasures/rsa.rst`).
172///
173/// Additional gaps tracked in the same document:
174///
175/// - `T1-E`: bigint CT audit (`pow_mod`, Montgomery multiply,
176/// `cmp` early-exit).
177/// - `T2-I`: message blinding (`r^e * c`) against DPA on the
178/// per-iteration Montgomery multiplications inside `pow_mod`.
179///
180/// Until these countermeasures land, **this function should not
181/// be used in deployments where a level-2 (observational) or
182/// level-3 (active fault) attacker is in scope**.
183pub fn rsa_decrypt_raw(sk: &RsaSecretKey, c: &BigInt) -> BigInt {
184 // CRT decryption:
185 // m1 = c^dp mod p
186 // m2 = c^dq mod q
187 // h = qinv * (m1 - m2) mod p
188 // m = m2 + h * q
189 let cp = c.rem(&sk.p);
190 let cq = c.rem(&sk.q);
191 let m1 = cp.mod_exp(&sk.dp, &sk.p);
192 let m2 = cq.mod_exp(&sk.dq, &sk.q);
193
194 // Compute h = qinv * (m1 - m2) mod p
195 // m1 is in [0, p) and m2 is in [0, q). The difference can be negative,
196 // so we reduce m2 mod p first, then handle the sign.
197 let m2_mod_p = m2.rem(&sk.p);
198 let diff = if m1 >= m2_mod_p {
199 m1.sub(&m2_mod_p)
200 } else {
201 m1.add(&sk.p).sub(&m2_mod_p)
202 };
203 let h = sk.qinv.mul(&diff).rem(&sk.p);
204
205 // m = m2 + h * q
206 m2.add(&h.mul(&sk.q))
207}
208
209/// GCD of two BigInts using Euclidean algorithm.
210fn gcd(a: &BigInt, b: &BigInt) -> BigInt {
211 let mut a = a.clone();
212 let mut b = b.clone();
213 while !b.is_zero() {
214 let r = a.rem(&b);
215 a = b;
216 b = r;
217 }
218 a
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 fn test_rng() -> impl FnMut(&mut [u8]) {
226 let mut state: u64 = 0xdeadbeefcafebabe;
227 move |buf: &mut [u8]| {
228 for b in buf.iter_mut() {
229 state = state
230 .wrapping_mul(6364136223846793005)
231 .wrapping_add(1442695040888963407);
232 *b = (state >> 33) as u8;
233 }
234 }
235 }
236
237 #[test]
238 fn test_raw_encrypt_decrypt_small() {
239 // Use known small primes for a quick test.
240 let p = BigInt::from_u64(61);
241 let q = BigInt::from_u64(53);
242 let n = p.mul(&q); // 3233
243 let e = BigInt::from_u64(17);
244 let one = BigInt::from_u64(1);
245 let p1 = p.sub(&one);
246 let q1 = q.sub(&one);
247 let lambda = p1.mul(&q1); // 60 * 52 = 3120
248 let d = e.mod_inv(&lambda).unwrap();
249 let dp = d.rem(&p1);
250 let dq = d.rem(&q1);
251 let qinv = q.mod_inv(&p).unwrap();
252
253 let pk = RsaPublicKey { n: n.clone(), e };
254 let sk = RsaSecretKey {
255 n,
256 d,
257 p,
258 q,
259 dp,
260 dq,
261 qinv,
262 };
263
264 let m = BigInt::from_u64(42);
265 let c = rsa_encrypt_raw(&pk, &m);
266 let m2 = rsa_decrypt_raw(&sk, &c);
267 assert_eq!(m, m2);
268 }
269
270 #[test]
271 fn test_keygen_roundtrip() {
272 let mut rng = test_rng();
273 let (pk, sk) = rsa_keygen(512, &mut rng);
274 let m = BigInt::from_u64(12345678);
275 let c = rsa_encrypt_raw(&pk, &m);
276 let m2 = rsa_decrypt_raw(&sk, &c);
277 assert_eq!(m, m2);
278 }
279}