Skip to main content

uselesskey_rsa/
keypair.rs

1use std::fmt;
2use std::sync::Arc;
3
4use rsa::pkcs8::LineEnding;
5use rsa::{RsaPrivateKey, RsaPublicKey, pkcs8::EncodePrivateKey, pkcs8::EncodePublicKey};
6use uselesskey_core::negative::CorruptPem;
7use uselesskey_core::sink::TempArtifact;
8use uselesskey_core::{Error, Factory};
9use uselesskey_core_keypair_material::Pkcs8SpkiKeyMaterial;
10
11use crate::RsaSpec;
12
13/// Cache domain for RSA keypair fixtures.
14///
15/// Keep this stable: changing it changes deterministic outputs.
16pub const DOMAIN_RSA_KEYPAIR: &str = "uselesskey:rsa:keypair";
17
18/// An RSA keypair fixture with various output formats.
19///
20/// Created via [`RsaFactoryExt::rsa()`]. Provides access to:
21/// - Private key in PKCS#8 PEM and DER formats
22/// - Public key in SPKI PEM and DER formats
23/// - Negative fixtures (corrupted PEM, truncated DER, mismatched keys)
24/// - JWK output (with the `jwk` feature)
25///
26/// # Examples
27///
28/// ```
29/// use uselesskey_core::Factory;
30/// use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
31///
32/// let fx = Factory::random();
33/// let keypair = fx.rsa("my-service", RsaSpec::rs256());
34///
35/// // Access key material
36/// let private_pem = keypair.private_key_pkcs8_pem();
37/// let public_der = keypair.public_key_spki_der();
38///
39/// assert!(private_pem.contains("BEGIN PRIVATE KEY"));
40/// assert!(!public_der.is_empty());
41/// ```
42#[derive(Clone)]
43pub struct RsaKeyPair {
44    factory: Factory,
45    label: String,
46    spec: RsaSpec,
47    inner: Arc<Inner>,
48}
49
50struct Inner {
51    /// Kept for potential signing methods; not currently used.
52    _private: RsaPrivateKey,
53    #[cfg(feature = "jwk")]
54    public: RsaPublicKey,
55    material: Pkcs8SpkiKeyMaterial,
56}
57
58impl fmt::Debug for RsaKeyPair {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        f.debug_struct("RsaKeyPair")
61            .field("label", &self.label)
62            .field("spec", &self.spec)
63            .finish_non_exhaustive()
64    }
65}
66
67/// Extension trait to hang RSA helpers off the core [`Factory`].
68pub trait RsaFactoryExt {
69    /// Generate (or retrieve from cache) an RSA keypair fixture.
70    ///
71    /// The `label` identifies this keypair within your test suite.
72    /// In deterministic mode, `seed + label + spec` always produces the same key.
73    ///
74    /// # Examples
75    ///
76    /// ```
77    /// use uselesskey_core::{Factory, Seed};
78    /// use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
79    ///
80    /// let seed = Seed::from_env_value("test-seed").unwrap();
81    /// let fx = Factory::deterministic(seed);
82    /// let keypair = fx.rsa("my-service", RsaSpec::rs256());
83    ///
84    /// let pem = keypair.private_key_pkcs8_pem();
85    /// assert!(pem.contains("BEGIN PRIVATE KEY"));
86    /// ```
87    fn rsa(&self, label: impl AsRef<str>, spec: RsaSpec) -> RsaKeyPair;
88}
89
90impl RsaFactoryExt for Factory {
91    fn rsa(&self, label: impl AsRef<str>, spec: RsaSpec) -> RsaKeyPair {
92        RsaKeyPair::new(self.clone(), label.as_ref(), spec)
93    }
94}
95
96impl RsaKeyPair {
97    fn new(factory: Factory, label: &str, spec: RsaSpec) -> Self {
98        let inner = load_inner(&factory, label, spec, "good");
99        Self {
100            factory,
101            label: label.to_string(),
102            spec,
103            inner,
104        }
105    }
106
107    fn load_variant(&self, variant: &str) -> Arc<Inner> {
108        load_inner(&self.factory, &self.label, self.spec, variant)
109    }
110
111    #[cfg(feature = "jwk")]
112    fn jwk_alg(&self) -> &'static str {
113        match self.spec.bits {
114            3072 => "RS384",
115            4096 => "RS512",
116            _ => "RS256",
117        }
118    }
119
120    /// PKCS#8 DER-encoded private key bytes.
121    ///
122    /// # Examples
123    ///
124    /// ```no_run
125    /// # use uselesskey_core::{Factory, Seed};
126    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
127    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
128    /// let kp = fx.rsa("svc", RsaSpec::rs256());
129    /// let der = kp.private_key_pkcs8_der();
130    /// assert!(!der.is_empty());
131    /// ```
132    pub fn private_key_pkcs8_der(&self) -> &[u8] {
133        self.inner.material.private_key_pkcs8_der()
134    }
135
136    /// PKCS#8 PEM-encoded private key.
137    ///
138    /// # Examples
139    ///
140    /// ```no_run
141    /// # use uselesskey_core::{Factory, Seed};
142    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
143    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
144    /// let kp = fx.rsa("svc", RsaSpec::rs256());
145    /// let pem = kp.private_key_pkcs8_pem();
146    /// assert!(pem.starts_with("-----BEGIN PRIVATE KEY-----"));
147    /// ```
148    pub fn private_key_pkcs8_pem(&self) -> &str {
149        self.inner.material.private_key_pkcs8_pem()
150    }
151
152    /// SPKI DER-encoded public key bytes.
153    ///
154    /// # Examples
155    ///
156    /// ```no_run
157    /// # use uselesskey_core::{Factory, Seed};
158    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
159    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
160    /// let kp = fx.rsa("svc", RsaSpec::rs256());
161    /// let der = kp.public_key_spki_der();
162    /// assert!(!der.is_empty());
163    /// ```
164    pub fn public_key_spki_der(&self) -> &[u8] {
165        self.inner.material.public_key_spki_der()
166    }
167
168    /// SPKI PEM-encoded public key.
169    ///
170    /// # Examples
171    ///
172    /// ```no_run
173    /// # use uselesskey_core::{Factory, Seed};
174    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
175    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
176    /// let kp = fx.rsa("svc", RsaSpec::rs256());
177    /// let pem = kp.public_key_spki_pem();
178    /// assert!(pem.starts_with("-----BEGIN PUBLIC KEY-----"));
179    /// ```
180    pub fn public_key_spki_pem(&self) -> &str {
181        self.inner.material.public_key_spki_pem()
182    }
183
184    /// Write the PKCS#8 PEM private key to a tempfile and return the handle.
185    ///
186    /// # Examples
187    ///
188    /// ```no_run
189    /// # use uselesskey_core::{Factory, Seed};
190    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
191    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
192    /// let kp = fx.rsa("svc", RsaSpec::rs256());
193    /// let temp = kp.write_private_key_pkcs8_pem().unwrap();
194    /// assert!(temp.path().exists());
195    /// ```
196    pub fn write_private_key_pkcs8_pem(&self) -> Result<TempArtifact, Error> {
197        self.inner.material.write_private_key_pkcs8_pem()
198    }
199
200    /// Write the SPKI PEM public key to a tempfile and return the handle.
201    ///
202    /// # Examples
203    ///
204    /// ```no_run
205    /// # use uselesskey_core::{Factory, Seed};
206    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
207    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
208    /// let kp = fx.rsa("svc", RsaSpec::rs256());
209    /// let temp = kp.write_public_key_spki_pem().unwrap();
210    /// assert!(temp.path().exists());
211    /// ```
212    pub fn write_public_key_spki_pem(&self) -> Result<TempArtifact, Error> {
213        self.inner.material.write_public_key_spki_pem()
214    }
215
216    /// Produce a corrupted variant of the PKCS#8 PEM.
217    ///
218    /// # Examples
219    ///
220    /// ```no_run
221    /// # use uselesskey_core::{Factory, Seed};
222    /// # use uselesskey_core::negative::CorruptPem;
223    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
224    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
225    /// let kp = fx.rsa("svc", RsaSpec::rs256());
226    /// let bad = kp.private_key_pkcs8_pem_corrupt(CorruptPem::BadHeader);
227    /// assert!(bad.contains("CORRUPTED"));
228    /// ```
229    pub fn private_key_pkcs8_pem_corrupt(&self, how: CorruptPem) -> String {
230        self.inner.material.private_key_pkcs8_pem_corrupt(how)
231    }
232
233    /// Produce a deterministic corrupted PKCS#8 PEM using a variant string.
234    ///
235    /// # Examples
236    ///
237    /// ```no_run
238    /// # use uselesskey_core::{Factory, Seed};
239    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
240    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
241    /// let kp = fx.rsa("svc", RsaSpec::rs256());
242    /// let bad = kp.private_key_pkcs8_pem_corrupt_deterministic("corrupt:v1");
243    /// assert!(!bad.is_empty());
244    /// ```
245    pub fn private_key_pkcs8_pem_corrupt_deterministic(&self, variant: &str) -> String {
246        self.inner
247            .material
248            .private_key_pkcs8_pem_corrupt_deterministic(variant)
249    }
250
251    /// Produce a truncated variant of the PKCS#8 DER.
252    ///
253    /// # Examples
254    ///
255    /// ```no_run
256    /// # use uselesskey_core::{Factory, Seed};
257    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
258    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
259    /// let kp = fx.rsa("svc", RsaSpec::rs256());
260    /// let truncated = kp.private_key_pkcs8_der_truncated(10);
261    /// assert_eq!(truncated.len(), 10);
262    /// ```
263    pub fn private_key_pkcs8_der_truncated(&self, len: usize) -> Vec<u8> {
264        self.inner.material.private_key_pkcs8_der_truncated(len)
265    }
266
267    /// Produce a deterministic corrupted PKCS#8 DER using a variant string.
268    ///
269    /// # Examples
270    ///
271    /// ```no_run
272    /// # use uselesskey_core::{Factory, Seed};
273    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
274    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
275    /// let kp = fx.rsa("svc", RsaSpec::rs256());
276    /// let bad = kp.private_key_pkcs8_der_corrupt_deterministic("corrupt:v1");
277    /// assert!(!bad.is_empty());
278    /// ```
279    pub fn private_key_pkcs8_der_corrupt_deterministic(&self, variant: &str) -> Vec<u8> {
280        self.inner
281            .material
282            .private_key_pkcs8_der_corrupt_deterministic(variant)
283    }
284
285    /// Return a valid (parseable) public key that does *not* match this private key.
286    ///
287    /// # Examples
288    ///
289    /// ```no_run
290    /// # use uselesskey_core::{Factory, Seed};
291    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
292    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
293    /// let kp = fx.rsa("svc", RsaSpec::rs256());
294    /// let wrong_pub = kp.mismatched_public_key_spki_der();
295    /// assert_ne!(wrong_pub, kp.public_key_spki_der());
296    /// ```
297    pub fn mismatched_public_key_spki_der(&self) -> Vec<u8> {
298        let other = self.load_variant("mismatch");
299        other.material.public_key_spki_der().to_vec()
300    }
301
302    /// A stable key identifier derived from the public key (base64url blake3 hash prefix).
303    ///
304    /// # Examples
305    ///
306    /// ```no_run
307    /// # use uselesskey_core::{Factory, Seed};
308    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
309    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
310    /// let kp = fx.rsa("svc", RsaSpec::rs256());
311    /// let kid = kp.kid();
312    /// assert!(!kid.is_empty());
313    /// ```
314    #[cfg(feature = "jwk")]
315    pub fn kid(&self) -> String {
316        self.inner.material.kid()
317    }
318
319    /// Alias for [`Self::public_jwk`].
320    ///
321    /// Requires the `jwk` feature.
322    ///
323    /// # Examples
324    ///
325    /// ```no_run
326    /// # use uselesskey_core::{Factory, Seed};
327    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
328    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
329    /// let kp = fx.rsa("svc", RsaSpec::rs256());
330    /// let jwk = kp.public_key_jwk();
331    /// assert_eq!(jwk.to_value()["kty"], "RSA");
332    /// ```
333    #[cfg(feature = "jwk")]
334    pub fn public_key_jwk(&self) -> uselesskey_jwk::PublicJwk {
335        self.public_jwk()
336    }
337
338    /// Public JWK for this keypair (kty=RSA, use=sig, kid=...).
339    ///
340    /// Requires the `jwk` feature.
341    ///
342    /// # Examples
343    ///
344    /// ```no_run
345    /// # use uselesskey_core::{Factory, Seed};
346    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
347    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
348    /// let kp = fx.rsa("svc", RsaSpec::rs256());
349    /// let jwk = kp.public_jwk();
350    /// let val = jwk.to_value();
351    /// assert_eq!(val["kty"], "RSA");
352    /// assert_eq!(val["alg"], "RS256");
353    /// ```
354    #[cfg(feature = "jwk")]
355    pub fn public_jwk(&self) -> uselesskey_jwk::PublicJwk {
356        use base64::Engine as _;
357        use base64::engine::general_purpose::URL_SAFE_NO_PAD;
358        use rsa::traits::PublicKeyParts;
359        use uselesskey_jwk::{PublicJwk, RsaPublicJwk};
360
361        let n = self.inner.public.n().to_bytes_be();
362        let e = self.inner.public.e().to_bytes_be();
363
364        PublicJwk::Rsa(RsaPublicJwk {
365            kty: "RSA",
366            use_: "sig",
367            alg: self.jwk_alg(),
368            kid: self.kid(),
369            n: URL_SAFE_NO_PAD.encode(n),
370            e: URL_SAFE_NO_PAD.encode(e),
371        })
372    }
373
374    /// Private JWK for this keypair (kty=RSA, use=sig, kid=...).
375    ///
376    /// Requires the `jwk` feature.
377    ///
378    /// # Examples
379    ///
380    /// ```no_run
381    /// # use uselesskey_core::{Factory, Seed};
382    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
383    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
384    /// let kp = fx.rsa("svc", RsaSpec::rs256());
385    /// let jwk = kp.private_key_jwk();
386    /// let val = jwk.to_value();
387    /// assert_eq!(val["kty"], "RSA");
388    /// assert!(val["d"].is_string());
389    /// ```
390    #[cfg(feature = "jwk")]
391    pub fn private_key_jwk(&self) -> uselesskey_jwk::PrivateJwk {
392        use base64::Engine as _;
393        use base64::engine::general_purpose::URL_SAFE_NO_PAD;
394        use rsa::traits::{PrivateKeyParts, PublicKeyParts};
395        use uselesskey_jwk::{PrivateJwk, RsaPrivateJwk};
396
397        let private = &self.inner._private;
398        let primes = private.primes();
399        assert!(primes.len() >= 2, "expected at least two RSA primes");
400
401        let n = private.n().to_bytes_be();
402        let e = private.e().to_bytes_be();
403        let d = private.d().to_bytes_be();
404        let p = primes[0].to_bytes_be();
405        let q = primes[1].to_bytes_be();
406        let dp = private.dp().expect("dp").to_bytes_be();
407        let dq = private.dq().expect("dq").to_bytes_be();
408        let qi = private.qinv().expect("qinv").to_bytes_be().1;
409
410        PrivateJwk::Rsa(RsaPrivateJwk {
411            kty: "RSA",
412            use_: "sig",
413            alg: self.jwk_alg(),
414            kid: self.kid(),
415            n: URL_SAFE_NO_PAD.encode(n),
416            e: URL_SAFE_NO_PAD.encode(e),
417            d: URL_SAFE_NO_PAD.encode(d),
418            p: URL_SAFE_NO_PAD.encode(p),
419            q: URL_SAFE_NO_PAD.encode(q),
420            dp: URL_SAFE_NO_PAD.encode(dp),
421            dq: URL_SAFE_NO_PAD.encode(dq),
422            qi: URL_SAFE_NO_PAD.encode(qi),
423        })
424    }
425
426    /// JWKS containing a single public key.
427    ///
428    /// # Examples
429    ///
430    /// ```no_run
431    /// # use uselesskey_core::{Factory, Seed};
432    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
433    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
434    /// let kp = fx.rsa("svc", RsaSpec::rs256());
435    /// let jwks = kp.public_jwks();
436    /// assert!(jwks.to_value()["keys"].is_array());
437    /// ```
438    #[cfg(feature = "jwk")]
439    pub fn public_jwks(&self) -> uselesskey_jwk::Jwks {
440        use uselesskey_jwk::JwksBuilder;
441
442        let mut builder = JwksBuilder::new();
443        builder.push_public(self.public_jwk());
444        builder.build()
445    }
446
447    /// Public JWK serialized to `serde_json::Value`.
448    ///
449    /// Requires the `jwk` feature.
450    ///
451    /// # Examples
452    ///
453    /// ```no_run
454    /// # use uselesskey_core::{Factory, Seed};
455    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
456    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
457    /// let kp = fx.rsa("svc", RsaSpec::rs256());
458    /// let val = kp.public_jwk_json();
459    /// assert_eq!(val["kty"], "RSA");
460    /// ```
461    #[cfg(feature = "jwk")]
462    pub fn public_jwk_json(&self) -> serde_json::Value {
463        self.public_jwk().to_value()
464    }
465
466    /// JWKS serialized to `serde_json::Value`.
467    ///
468    /// Requires the `jwk` feature.
469    ///
470    /// # Examples
471    ///
472    /// ```no_run
473    /// # use uselesskey_core::{Factory, Seed};
474    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
475    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
476    /// let kp = fx.rsa("svc", RsaSpec::rs256());
477    /// let val = kp.public_jwks_json();
478    /// assert!(val["keys"].is_array());
479    /// ```
480    #[cfg(feature = "jwk")]
481    pub fn public_jwks_json(&self) -> serde_json::Value {
482        self.public_jwks().to_value()
483    }
484
485    /// Private JWK serialized to `serde_json::Value`.
486    ///
487    /// Requires the `jwk` feature.
488    ///
489    /// # Examples
490    ///
491    /// ```no_run
492    /// # use uselesskey_core::{Factory, Seed};
493    /// # use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
494    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
495    /// let kp = fx.rsa("svc", RsaSpec::rs256());
496    /// let val = kp.private_key_jwk_json();
497    /// assert_eq!(val["kty"], "RSA");
498    /// assert!(val["d"].is_string());
499    /// ```
500    #[cfg(feature = "jwk")]
501    pub fn private_key_jwk_json(&self) -> serde_json::Value {
502        self.private_key_jwk().to_value()
503    }
504}
505
506fn load_inner(factory: &Factory, label: &str, spec: RsaSpec, variant: &str) -> Arc<Inner> {
507    // Validate what we can, up front.
508    assert!(
509        spec.bits >= 1024,
510        "RSA bits too small for most parsers; got {}",
511        spec.bits
512    );
513    assert!(
514        spec.exponent == 65537,
515        "custom RSA public exponent not supported in v1; got {}",
516        spec.exponent
517    );
518
519    let spec_bytes = spec.stable_bytes();
520
521    factory.get_or_init(DOMAIN_RSA_KEYPAIR, label, &spec_bytes, variant, |rng| {
522        let private = RsaPrivateKey::new(rng, spec.bits).expect("RSA keygen failed");
523        let public = RsaPublicKey::from(&private);
524
525        let pkcs8_der_doc = private
526            .to_pkcs8_der()
527            .expect("failed to encode RSA private key as PKCS#8 DER");
528        let pkcs8_der: Arc<[u8]> = Arc::from(pkcs8_der_doc.as_bytes());
529
530        let pkcs8_pem = private
531            .to_pkcs8_pem(LineEnding::LF)
532            .expect("failed to encode RSA private key as PKCS#8 PEM")
533            .to_string();
534
535        let spki_der_doc = public
536            .to_public_key_der()
537            .expect("failed to encode RSA public key as SPKI DER");
538        let spki_der: Arc<[u8]> = Arc::from(spki_der_doc.as_bytes());
539
540        let spki_pem = public
541            .to_public_key_pem(LineEnding::LF)
542            .expect("failed to encode RSA public key as SPKI PEM")
543            .to_string();
544
545        let material = Pkcs8SpkiKeyMaterial::new(pkcs8_der, pkcs8_pem, spki_der, spki_pem);
546
547        Inner {
548            _private: private,
549            #[cfg(feature = "jwk")]
550            public,
551            material,
552        }
553    })
554}