Skip to main content

rs_crypto/
rsa.rs

1use base64::{Engine, engine::general_purpose::STANDARD};
2use num_bigint::Sign;
3use simple_asn1::{ASN1Block, BigInt, from_der};
4use std::fs;
5use std::path::Path;
6
7#[derive(Debug)]
8pub struct RsaKey {
9    pub n: BigInt,    // modulus
10    pub e: BigInt,    // public exponent
11    pub d: BigInt,    // private exponent
12    pub p: BigInt,    // prime factor 1
13    pub q: BigInt,    // prime factor 2
14    pub dp: BigInt,   // d mod (p-1)
15    pub dq: BigInt,   // d mod (q-1)
16    pub qinv: BigInt, // q^-1 mod p
17}
18
19fn pem_to_der(pem: &str) -> Vec<u8> {
20    let body: String = pem
21        .lines()
22        .map(|l| l.trim())
23        .filter(|l| !l.starts_with("-----"))
24        .collect();
25    let clean = body.replace(['\n', '\r', ' ', '\t'], "");
26    STANDARD.decode(clean).expect("Invalid base64")
27}
28
29pub fn parse_rsa_key_from_pem(pem: &str) -> std::io::Result<RsaKey> {
30    let der = pem_to_der(pem);
31    let blocks = from_der(&der).expect("Invalid DER encoding");
32
33    let outer_seq = match &blocks[0] {
34        ASN1Block::Sequence(_, items) => items,
35        _ => panic!("Expected outer SEQUENCE"),
36    };
37
38    let inner_der = match &outer_seq[2] {
39        ASN1Block::OctetString(_, bytes) => bytes,
40        _ => panic!("Expected OCTET STRING"),
41    };
42
43    let inner_blocks = from_der(inner_der).expect("Invalid inner DER encoding");
44    let inner_seq = match &inner_blocks[0] {
45        ASN1Block::Sequence(_, items) => items,
46        _ => panic!("Expected inner SEQUENCE"),
47    };
48
49    let ints: Vec<Vec<u8>> = inner_seq
50        .iter()
51        .filter_map(|b| match b {
52            ASN1Block::Integer(_, n) => Some(n.to_bytes_be().1),
53            _ => None,
54        })
55        .collect();
56
57    if ints.len() < 9 {
58        panic!("Expected 9 integers, got {}", ints.len());
59    }
60
61    Ok(RsaKey {
62        n: BigInt::from_bytes_be(Sign::Plus, &ints[1]), // modulus
63        e: BigInt::from_bytes_be(Sign::Plus, &ints[2]), // public exponent
64        d: BigInt::from_bytes_be(Sign::Plus, &ints[3]), // private exponent
65        p: BigInt::from_bytes_be(Sign::Plus, &ints[4]), // prime factor 1
66        q: BigInt::from_bytes_be(Sign::Plus, &ints[5]), // prime factor 2
67        dp: BigInt::from_bytes_be(Sign::Plus, &ints[6]), // d mod (p-1)
68        dq: BigInt::from_bytes_be(Sign::Plus, &ints[7]), // d mod (q-1)
69        qinv: BigInt::from_bytes_be(Sign::Plus, &ints[8]), // q^-1 mod p
70    })
71}
72
73pub fn load_rsa_key<P: AsRef<Path>>(path: P) -> std::io::Result<RsaKey> {
74    parse_rsa_key_from_pem(&fs::read_to_string(path)?)
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use num_bigint::BigInt;
81
82    const PEM_512: &str = "\
83-----BEGIN PRIVATE KEY-----
84MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAzLUczq12qOSGm+Pv
85HSiJ+BPm/Z7vKITVMcUbipwycMcYZF8TjWiiShYLuVtWLA5mEE86FKvrmaf79/tG
86RTB6LQIDAQABAkEAu3ZgGyTfNWuRmdDyeFFXh8cyEbAqc4CxfBJ1VkoUJxGJDM+z
87m9DXk/vYvL5G+PbNOOHIE2VKNkWbdgX3OQ8GQQIhAOWUQkJEfVYNX8whrjFEzd9j
88SprujROHi4IKxzI1o4mrAiEA5EQY/mqL8LIEM7+1UXBN8dPJlSmcxcyRGwL39pTh
89o4cCIB0xw1NF/mJJBRuiVNJzG3MC32PgXhRTskvxLu+VnpxNAiBm59BAufXWj9pX
90HgD28uMgtzK0fSsA/QUZoU/6KQpD9wIgGDW/g+ztlu98aA9+zzaKbU77J/pDsAdN
91Az5KOlJPE6c=
92-----END PRIVATE KEY-----";
93
94    const PEM_1024: &str = "\
95-----BEGIN PRIVATE KEY-----
96MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALo81vSeR9KcXane
97KwJtS1QAAntAXSkLUQK9Aq/xZrEuI66lWwsUD+A1jutCy0ss/02PxqFIGjX3ByHl
989HOwkb+qThrYu8aThVTroR3NWXudGYvofkR7OZGRDlIPju06von9pzvsqfaTy6Rk
99ThdSdK33DsTJ5bjeUxEwblJKUFTjAgMBAAECgYA7iW2Sf/MoAjLzNgH74aK+NM6W
100RkpB78szG+d7Bao1pDFmCJilXwGARL7uuMiyvKzVR8xRDPLMI6+VB6VxQpYk6tbq
101XEQ1MwfaPGIjP69fs5g0x4QtkGcjIqEt0lQEYkcDkOtrfBt6CnkGyT+HtKFG2s4a
102W9MTs65vsXbWh4NLqQJBAOgtyqizfLgw+/2fvhE9wpamcNw4uNPWFeA/nHibZItG
103iBzHP5SeN7ifi5H+uhvg4R3bqivuvjW1fU/ujBCitcUCQQDNWGVcZH/fvWpKPe+N
1047/Y7AS734PIUGsYLyUQw9h2bWfmYQfb/bytsJlORKq7raydnAGGGU/JtDvo8ky2b
105GjKHAkAT7mppVQ8t2LapLR9p531e5Wbm4M+tD8HNAGj0SZK2ChYBMnGY1oQ+CyQ2
106IkHjxshMgeD36ITXo37gb8ACZZVpAkEAgaulbl/EZFxvh2xvHvl+SypnJ370P3/c
107ukqhdi2k6po5xE07lXf1OrlFIjGK/fzPh/q0myfducKwgJoMPZqgdwJAUKvzw/uz
10897Vl50Ot+Ir42/o9tvn/1VpSlAHhjtg8rC1XProY3UAaCDaDSnjws43d4a8AI+zt
109Eu7k9eTFIs8d5w==
110-----END PRIVATE KEY-----";
111
112    const PEM_2048: &str = "\
113-----BEGIN PRIVATE KEY-----
114MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDAFhkfShNkFq3c
115pQgsD/Ck9p30cvGY9l/6jRfCuxnaHZhNoEXzbqksFfQrf8KyjsEDlBDPueQkD5X5
116IaT0a6QQDI84k2PCuH1BP41UPTfRcCQfVeTt46Pnj99nemo526HDBEaD7iy3pe85
117TqKvE+1Y2qve40FajUndGGrylPpmvT7EIghmj9RJRgJ22Zy/2Xh7PdNZxF8fixuA
118IvfaBP9AbdepfQF1OeApv0si8tGTQxv5BM7Xuiy/SG19iLCkK9sEn0vqlylrYNXx
1192K7efpbHwh+dxdFaXI8GVpQWzvowL2rIvZWg6zRo19u2DjEWIzncQ8uLygT94cfJ
1200sND6VNjAgMBAAECggEAUxycpR2zkzB+7MPZa9s/x9jPUngzkfg0YiAPYln7XxVE
121E35gFerRNvqOyg1/LCw5VneH6KFplbLKtN96VKmIdMtCYvvfA984jvVVDNhqIOxR
122LN/I7Kd9AVIOm2LruHoQHWXprubsoU+iWRztpixMm5AOIqQY4HnWtlv81lZgm/fQ
123OwTAdTIlDNVchw7N71L6MM4TPOitkQswigrZD08oNy5ymoN34JhX55KxZvH+VK6i
124RKAFewgKwkZfK4inl/708hudL2ysdjUt48Y/s0vvYSaiUc7BB2NYgHJSttGJUIZN
125QWprn6u2TmES7Ns+jh5GBuuugj6QB3DLI9c0l5oRKQKBgQD3M00pjXE+ruWXDHDM
1261J/1y5b14LiLvf6MO81dtnhbuQA8KtKjyfi+vk9oanIv+xSRcPSkc85n2FHdEUXP
127DJ80lsVFQeJEJ1WaaNBDpjH/3cAr5xEmAywWnPTAiZh4Angcpc/D5aquW1ttMEqL
128WCbyG1sqraHr06ZPQfO8okOHywKBgQDG7Iz+t5WY3ef+NdLHAdOqnloYjBByvojE
129ZOPan7MC/wKqKTYNHxEm1lFQFWaPhgSZEGi6e/5i+BQxut+WKZR4BBpIt0UZdQz1
1305PZ+f6VClD4JnIYtUZRiHVUkBUgqeMutvp0eT1V39rt3ZKAyE+fZS5f28fH2521n
131Sm256NJ/yQKBgB3nXtY//gsPLUbwglTFA/TABCsKXEjLWxerxFQp1rWB071zkLev
132nx+z9fczqUyUmxBdEbszJyz4xi5wAHnjlP7Pnl2acry75WcgdtE4MaQ6Nx0YfsKS
133b6rsoc8I1iDua4lLpa6VAejFtHGo/duNdmijVov7JTNaDyxXVhzjpDexAoGBAITI
1349Jk3LOn8/taHUSq8gnF7AMMwA+7EVwFaI2sVfWY7majCl60MluNo3qBpmKunlzwh
135YvdQu4+O79P+XS+ck9nFS1JM3BhRNRSTASORy1v1HrBFxp9LvJP95o6D5BdNyRAy
136lCjeZjwM/DiHeBPVi8dWGZujB5R8CWCQo0wdKR5ZAoGBALO4i+kgMdq+AGOdhiXG
137xNmGdirgGB/zRHUGeF0OIJAHqJ+crecUoGCmJEA9FDA7l7/T2cGGMiYAHpXr/kFX
138o+QHndhr8IJVcSpl29XtwlgasyjUiT/B++wVj5IzUXqDM2L27Fkd+3A2oHbCW9Tg
139j1rxrCfYFDCy1LPnfG/lHfER
140-----END PRIVATE KEY-----";
141
142    const PEM_4096: &str = "\
143-----BEGIN PRIVATE KEY-----
144MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQC9Hnx2olQZ9XMc
145jtVPuAMzVqZEUSxgOvBJH+7aBZTsqzEow23J+ON6ErHXI4geizv83WWw6zvYgq5/
146pAqIZIs7z/OBaJSvkCRQyxdhWrcRusnzTAF6EdJeIO33xBM3ojGwIPiXUw+MV4Ck
147tV6UmFKpOFwybCu5BqLPT0LGGihHoDT6jRfqvxcC1goFhIVqJPsEPFtgjyDymJlQ
148AXhNP/+3lEumWYocvsqnzIJ2SfJV2XwY3M2ejdkNt94COrckI7wr1CFPvcQgcsdh
149x0WbYpTicdjQm6YWCxw0vavxhKgBt4Q+mSetC/VQDPI4GrdpcVHqB3+sxx7MQQca
150wEVCijKDJEHmi0TcDukm93qyswK7r9V1xDYYCIx66BP0VSj+2aK78YoWmb++K2QB
151rRiBWOedAHgZYVHgaJ83m7iEDY8z2vExWxhOAAlGGcD+GTrjIUJui9yfHnFR8m3f
152mRMIeMvxVJUmruwhoI16s8DxYzaLrrWkSaF0l/+UOovqPXCA1RYO/jZV0cARXHaY
153fmhykv/sweEeBpZpSb3mDJaU/Xi4B+HrYCkFroCdZ1gsnoIZmzaWc3qJmTDDcw/+
1542qu/aL9JGUDiDfDM+q/B+xWGSY1oqLWetQV7bQbF4O/SLPe2JU+G9bSsR/7YA8ln
155KDkrmRBy38U3RicOijQVWmOWNrVseQIDAQABAoICAA73sjcQxYzhEbFha+gpcqC7
156YLZdzErOGgVXkHz93cRWD4fKi85lMy+5T2JCZCays1aMeTIQNAYrNZSm5CnY90dj
157v8HEB/yAcSIEfim3gcSr9Alxla5WacAHECkWAhqwQgl5Wo4IxggVRs+tBmVxTB2A
158zJSLCMeAix3S3UCLgm5EyNye57/YTCxudJBCOu2PLAoGSDRzWigJGXKCY0XjOkxf
159tsBdgXehRq29cFfcpgrTByYlQjrPFC4omVqze0TSH+BCI47Jlej8MePUXyHF+BhB
1606rx74nWyfxLSLiETyYOKmt36G3CagS71j1gPDpQQTWuIbhDmbigRP3bdCmjn7MkJ
161lsNGf03RJPj8FezOpqzR6yLto7V/goT25q1ZA9zJNqSBLnj3p3y6keMrtnTS2a/7
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
193LOGOoj3Ev6V5r1LqrSryKdC6YDpW4nJY
194-----END PRIVATE KEY-----";
195
196    fn assert_key_valid(key: &RsaKey) {
197        assert_eq!(key.n.sign(), Sign::Plus);
198        assert_eq!(key.e.sign(), Sign::Plus);
199        assert_eq!(key.d.sign(), Sign::Plus);
200        assert_eq!(key.p.sign(), Sign::Plus);
201        assert_eq!(key.q.sign(), Sign::Plus);
202        assert_eq!(key.dp.sign(), Sign::Plus);
203        assert_eq!(key.dq.sign(), Sign::Plus);
204        assert_eq!(key.qinv.sign(), Sign::Plus);
205
206        assert_eq!(key.e, BigInt::from(65537));
207        assert_eq!(key.n, &key.p * &key.q);
208
209        let p_minus_1 = &key.p - BigInt::from(1);
210        let q_minus_1 = &key.q - BigInt::from(1);
211        assert_eq!(key.dp, &key.d % &p_minus_1);
212        assert_eq!(key.dq, &key.d % &q_minus_1);
213    }
214
215    #[test]
216    fn test_parse_512_bit_key() {
217        let key = parse_rsa_key_from_pem(PEM_512).unwrap();
218        assert_key_valid(&key);
219    }
220
221    #[test]
222    fn test_parse_1024_bit_key() {
223        let key = parse_rsa_key_from_pem(PEM_1024).unwrap();
224        assert_key_valid(&key);
225    }
226
227    #[test]
228    fn test_parse_2048_bit_key() {
229        let key = parse_rsa_key_from_pem(PEM_2048).unwrap();
230        assert_key_valid(&key);
231    }
232
233    #[test]
234    fn test_parse_4096_bit_key() {
235        let key = parse_rsa_key_from_pem(PEM_4096).unwrap();
236        assert_key_valid(&key);
237    }
238
239    #[test]
240    #[should_panic(expected = "Invalid base64")]
241    fn test_invalid_pem_body() {
242        let bad_pem = "\
243-----BEGIN PRIVATE KEY-----
244not-valid-base64!!!
245-----END PRIVATE KEY-----";
246        let _ = parse_rsa_key_from_pem(bad_pem);
247    }
248
249    #[test]
250    fn test_load_rsa_key_file_not_found() {
251        let result = load_rsa_key("nonexistent_key.pem");
252        assert!(result.is_err());
253    }
254
255    #[test]
256    fn test_pem_with_extra_whitespace() {
257        let spaced_pem = PEM_512.replace('\n', "\n  \n");
258        let key = parse_rsa_key_from_pem(&spaced_pem).unwrap();
259        assert_eq!(key.e, BigInt::from(65537));
260    }
261}