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}