Skip to main content

keyroost_rsakey/
lib.rs

1//! Host-side RSA-2048 key material for OpenPGP card import.
2//!
3//! A small front-end helper shared by `keyroostctl` and `keyroost`. It owns the one
4//! external-dependency exception in the workspace — the `rsa` crate — so the
5//! protocol crates stay dependency-free and the security-critical keygen / parse
6//! logic lives in exactly one place. It produces the full CRT component set
7//! (`e, p, q, u, dp, dq, n`, minimal big-endian) that `keyroost_openpgp`'s import
8//! path frames into the card's Extended Header List; the card selects which of
9//! those parts it actually wants per its declared algorithm attributes.
10
11use std::path::Path;
12
13/// RSA-2048 private-key components for OpenPGP import, minimal big-endian.
14///
15/// Holds the full CRT set so the transport layer can satisfy whichever import
16/// format the card declares (standard `e,p,q` or CRT `e,p,q,u,dp,dq`, with or
17/// without the modulus). Borrow these as `keyroost_openpgp::RsaPrivateKeyParts`.
18pub struct RsaKeyParts {
19    pub e: Vec<u8>,
20    pub p: Vec<u8>,
21    pub q: Vec<u8>,
22    /// `u = q⁻¹ mod p`.
23    pub u: Vec<u8>,
24    /// `dp = d mod (p−1)`.
25    pub dp: Vec<u8>,
26    /// `dq = d mod (q−1)`.
27    pub dq: Vec<u8>,
28    pub n: Vec<u8>,
29}
30
31/// The `rsa` crate zeroizes its own `RsaPrivateKey` on drop; these extracted
32/// copies deserve the same so a full private key doesn't linger in freed heap
33/// pages (or swap / core dumps) after the card import finishes. `e` and `n`
34/// are public but wiping them too costs nothing.
35impl Drop for RsaKeyParts {
36    fn drop(&mut self) {
37        use zeroize::Zeroize;
38        for buf in [
39            &mut self.e,
40            &mut self.p,
41            &mut self.q,
42            &mut self.u,
43            &mut self.dp,
44            &mut self.dq,
45            &mut self.n,
46        ] {
47            buf.zeroize();
48        }
49    }
50}
51
52/// Why obtaining RSA key parts failed. The `Display` strings are user-facing.
53#[derive(Debug)]
54pub enum RsaKeyError {
55    /// Reading the key file failed.
56    Io(std::io::Error),
57    /// The key could not be parsed as PKCS#1 or PKCS#8 (PEM or DER).
58    Parse(String),
59    /// The key is not RSA-2048 (carries the actual modulus bit length).
60    WrongSize(usize),
61    /// Key generation or CRT precompute failed inside the `rsa` crate.
62    Crypto(String),
63    /// A required component was missing after CRT precompute.
64    MissingComponent(&'static str),
65}
66
67impl std::fmt::Display for RsaKeyError {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        match self {
70            RsaKeyError::Io(e) => write!(f, "cannot read key file: {e}"),
71            RsaKeyError::Parse(e) => write!(f, "could not parse RSA private key: {e}"),
72            RsaKeyError::WrongSize(bits) => write!(
73                f,
74                "key is RSA-{bits}, but the card slot is RSA-2048; \
75                 import only supports 2048-bit keys"
76            ),
77            RsaKeyError::Crypto(e) => write!(f, "RSA operation failed: {e}"),
78            RsaKeyError::MissingComponent(c) => write!(f, "RSA key missing precomputed {c}"),
79        }
80    }
81}
82
83impl std::error::Error for RsaKeyError {}
84
85impl From<std::io::Error> for RsaKeyError {
86    fn from(e: std::io::Error) -> Self {
87        RsaKeyError::Io(e)
88    }
89}
90
91/// Generate a fresh RSA-2048 key on the host and extract its import parts.
92///
93/// `RsaPrivateKey::new` validates the key and precomputes the CRT values
94/// (dp, dq, qinv), so the full component set is available immediately.
95pub fn generate_2048() -> Result<RsaKeyParts, RsaKeyError> {
96    let mut rng = rand::thread_rng();
97    let key =
98        rsa::RsaPrivateKey::new(&mut rng, 2048).map_err(|e| RsaKeyError::Crypto(e.to_string()))?;
99    parts_from_key(key)
100}
101
102/// Load an RSA private key from `path` and extract its import parts.
103///
104/// Accepts PKCS#8 or PKCS#1, PEM or DER, auto-detected: PEM by its
105/// `-----BEGIN ... PRIVATE KEY-----` header, otherwise DER. The key must be
106/// RSA-2048 (the only size the card slot is provisioned for here). The file
107/// bytes are read locally; this crate never logs them.
108pub fn load_from_file(path: &Path) -> Result<RsaKeyParts, RsaKeyError> {
109    // The raw file *is* the private key — wipe the read buffer on drop, the
110    // same hygiene `RsaKeyParts` applies to the parsed components.
111    let bytes = zeroize::Zeroizing::new(std::fs::read(path)?);
112    parts_from_encoded(&bytes)
113}
114
115/// Decode PKCS#1/PKCS#8 (PEM or DER) key bytes and extract the import parts.
116/// Split out from [`load_from_file`] so the parsing path can be unit-tested
117/// without touching the filesystem.
118fn parts_from_encoded(bytes: &[u8]) -> Result<RsaKeyParts, RsaKeyError> {
119    use rsa::pkcs1::DecodeRsaPrivateKey;
120    use rsa::pkcs8::DecodePrivateKey;
121
122    let key = if bytes.starts_with(b"-----BEGIN") {
123        let text = std::str::from_utf8(bytes)
124            .map_err(|_| RsaKeyError::Parse("key file is not valid PEM/UTF-8".into()))?;
125        // PKCS#8 ("BEGIN PRIVATE KEY") vs PKCS#1 ("BEGIN RSA PRIVATE KEY").
126        rsa::RsaPrivateKey::from_pkcs8_pem(text)
127            .or_else(|_| rsa::RsaPrivateKey::from_pkcs1_pem(text))
128            .map_err(|e| RsaKeyError::Parse(e.to_string()))?
129    } else {
130        // Raw DER: try PKCS#8 then PKCS#1.
131        rsa::RsaPrivateKey::from_pkcs8_der(bytes)
132            .or_else(|_| rsa::RsaPrivateKey::from_pkcs1_der(bytes))
133            .map_err(|e| RsaKeyError::Parse(e.to_string()))?
134    };
135    parts_from_key(key)
136}
137
138/// Validate the key is RSA-2048, ensure the CRT values are precomputed, and
139/// extract the components (e, p, q, u, dp, dq, n) as minimal big-endian bytes.
140fn parts_from_key(mut key: rsa::RsaPrivateKey) -> Result<RsaKeyParts, RsaKeyError> {
141    use rsa::traits::{PrivateKeyParts, PublicKeyParts};
142
143    let bits = key.n().bits();
144    if bits != 2048 {
145        return Err(RsaKeyError::WrongSize(bits));
146    }
147    // `from_*` decoders do not precompute the CRT values; `new` does. Calling
148    // precompute unconditionally is cheap and makes dp/dq/qinv always present.
149    key.precompute()
150        .map_err(|e| RsaKeyError::Crypto(e.to_string()))?;
151
152    let primes = key.primes();
153    if primes.len() != 2 {
154        return Err(RsaKeyError::Crypto("expected a 2-prime RSA key".into()));
155    }
156    let dp = key
157        .dp()
158        .ok_or(RsaKeyError::MissingComponent("dp"))?
159        .to_bytes_be();
160    let dq = key
161        .dq()
162        .ok_or(RsaKeyError::MissingComponent("dq"))?
163        .to_bytes_be();
164    // qinv = q⁻¹ mod p is positive; take its big-endian magnitude (drop sign).
165    let u = key
166        .qinv()
167        .ok_or(RsaKeyError::MissingComponent("qinv"))?
168        .to_bytes_be()
169        .1;
170    Ok(RsaKeyParts {
171        e: key.e().to_bytes_be(),
172        n: key.n().to_bytes_be(),
173        p: primes[0].to_bytes_be(),
174        q: primes[1].to_bytes_be(),
175        u,
176        dp,
177        dq,
178    })
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use rsa::pkcs8::EncodePrivateKey;
185
186    #[test]
187    fn generate_2048_has_expected_shapes() {
188        let k = generate_2048().expect("keygen");
189        // A 2048-bit modulus is exactly 256 bytes minimal big-endian (top bit
190        // set); each prime is 1024 bits = 128 bytes.
191        assert_eq!(k.n.len(), 256, "modulus should be 256 bytes");
192        assert_eq!(k.p.len(), 128, "p should be 128 bytes");
193        assert_eq!(k.q.len(), 128, "q should be 128 bytes");
194        // Default public exponent is 65537 = 01 00 01.
195        assert_eq!(k.e, vec![0x01, 0x00, 0x01]);
196        // CRT components are present and sized like the primes.
197        assert!(!k.u.is_empty() && !k.dp.is_empty() && !k.dq.is_empty());
198        assert!(k.dp.len() <= 128 && k.dq.len() <= 128);
199    }
200
201    #[test]
202    fn load_round_trips_through_der() {
203        // Generate, serialize to PKCS#8 DER, parse back, and confirm the public
204        // modulus survives the round trip (exercises the file-parse path
205        // without keygen in the loader).
206        let mut rng = rand::thread_rng();
207        let key = rsa::RsaPrivateKey::new(&mut rng, 2048).expect("keygen");
208        let der = key.to_pkcs8_der().expect("encode der");
209        let parsed = parts_from_encoded(der.as_bytes()).expect("parse der");
210        use rsa::traits::PublicKeyParts;
211        assert_eq!(parsed.n, key.n().to_bytes_be());
212        assert_eq!(parsed.e, key.e().to_bytes_be());
213    }
214
215    #[test]
216    fn rejects_non_2048() {
217        // A 1024-bit key (faster to generate) must be size-rejected.
218        let mut rng = rand::thread_rng();
219        let key = rsa::RsaPrivateKey::new(&mut rng, 1024).expect("keygen");
220        let der = key.to_pkcs8_der().expect("encode der");
221        // Avoid `{:?}` on the Ok value — RsaKeyParts deliberately isn't Debug
222        // (it holds private-key bytes). Match on the error via Display.
223        match parts_from_encoded(der.as_bytes()) {
224            Err(RsaKeyError::WrongSize(1024)) => {}
225            Err(e) => panic!("expected WrongSize(1024), got error: {e}"),
226            Ok(_) => panic!("expected WrongSize(1024), but parsing succeeded"),
227        }
228    }
229
230    #[test]
231    fn rejects_garbage() {
232        assert!(matches!(
233            parts_from_encoded(&[0xDE, 0xAD, 0xBE, 0xEF]),
234            Err(RsaKeyError::Parse(_))
235        ));
236    }
237}