1use std::fmt;
2use std::sync::Arc;
3
4use pgp::composed::{EncryptionCaps, KeyType, SecretKeyParamsBuilder, SignedPublicKey};
5use pgp::ser::Serialize;
6use pgp::types::KeyDetails;
7use rand_chacha::ChaCha20Rng;
8use rand_core::SeedableRng;
9use uselesskey_core::negative::{
10 CorruptPem, corrupt_der_deterministic, corrupt_pem, corrupt_pem_deterministic, truncate_der,
11};
12use uselesskey_core::sink::TempArtifact;
13use uselesskey_core::{Error, Factory};
14
15use crate::PgpSpec;
16
17pub const DOMAIN_PGP_KEYPAIR: &str = "uselesskey:pgp:keypair";
21
22#[derive(Clone)]
23pub struct PgpKeyPair {
24 factory: Factory,
25 label: String,
26 spec: PgpSpec,
27 inner: Arc<Inner>,
28}
29
30struct Inner {
31 user_id: String,
32 fingerprint: String,
33 private_binary: Arc<[u8]>,
34 private_armor: String,
35 public_binary: Arc<[u8]>,
36 public_armor: String,
37}
38
39impl fmt::Debug for PgpKeyPair {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 f.debug_struct("PgpKeyPair")
42 .field("label", &self.label)
43 .field("spec", &self.spec)
44 .field("fingerprint", &self.inner.fingerprint)
45 .finish_non_exhaustive()
46 }
47}
48
49pub trait PgpFactoryExt {
51 fn pgp(&self, label: impl AsRef<str>, spec: PgpSpec) -> PgpKeyPair;
52}
53
54impl PgpFactoryExt for Factory {
55 fn pgp(&self, label: impl AsRef<str>, spec: PgpSpec) -> PgpKeyPair {
56 PgpKeyPair::new(self.clone(), label.as_ref(), spec)
57 }
58}
59
60impl PgpKeyPair {
61 fn new(factory: Factory, label: &str, spec: PgpSpec) -> Self {
62 let inner = load_inner(&factory, label, spec, "good");
63 Self {
64 factory,
65 label: label.to_string(),
66 spec,
67 inner,
68 }
69 }
70
71 fn load_variant(&self, variant: &str) -> Arc<Inner> {
72 load_inner(&self.factory, &self.label, self.spec, variant)
73 }
74
75 pub fn spec(&self) -> PgpSpec {
77 self.spec
78 }
79
80 pub fn label(&self) -> &str {
92 &self.label
93 }
94
95 pub fn user_id(&self) -> &str {
97 &self.inner.user_id
98 }
99
100 pub fn fingerprint(&self) -> &str {
102 &self.inner.fingerprint
103 }
104
105 pub fn private_key_binary(&self) -> &[u8] {
107 &self.inner.private_binary
108 }
109
110 pub fn private_key_armored(&self) -> &str {
112 &self.inner.private_armor
113 }
114
115 pub fn public_key_binary(&self) -> &[u8] {
117 &self.inner.public_binary
118 }
119
120 pub fn public_key_armored(&self) -> &str {
122 &self.inner.public_armor
123 }
124
125 pub fn write_private_key_armored(&self) -> Result<TempArtifact, Error> {
127 TempArtifact::new_string("uselesskey-", ".pgp.priv.asc", self.private_key_armored())
128 }
129
130 pub fn write_public_key_armored(&self) -> Result<TempArtifact, Error> {
132 TempArtifact::new_string("uselesskey-", ".pgp.pub.asc", self.public_key_armored())
133 }
134
135 pub fn private_key_armored_corrupt(&self, how: CorruptPem) -> String {
137 corrupt_pem(self.private_key_armored(), how)
138 }
139
140 pub fn private_key_armored_corrupt_deterministic(&self, variant: &str) -> String {
142 corrupt_pem_deterministic(self.private_key_armored(), variant)
143 }
144
145 pub fn private_key_binary_truncated(&self, len: usize) -> Vec<u8> {
147 truncate_der(self.private_key_binary(), len)
148 }
149
150 pub fn private_key_binary_corrupt_deterministic(&self, variant: &str) -> Vec<u8> {
152 corrupt_der_deterministic(self.private_key_binary(), variant)
153 }
154
155 pub fn mismatched_public_key_binary(&self) -> Vec<u8> {
157 let other = self.load_variant("mismatch");
158 other.public_binary.as_ref().to_vec()
159 }
160
161 pub fn mismatched_public_key_armored(&self) -> String {
163 let other = self.load_variant("mismatch");
164 other.public_armor.clone()
165 }
166}
167
168fn load_inner(factory: &Factory, label: &str, spec: PgpSpec, variant: &str) -> Arc<Inner> {
169 let spec_bytes = spec.stable_bytes();
170
171 factory.get_or_init(DOMAIN_PGP_KEYPAIR, label, &spec_bytes, variant, |seed| {
172 let mut rng = ChaCha20Rng::from_seed(*seed.bytes());
173 let user_id = build_user_id(label);
174
175 let mut key_params = SecretKeyParamsBuilder::default();
176 key_params
177 .key_type(spec_to_key_type(spec))
178 .can_certify(true)
179 .can_sign(true)
180 .can_encrypt(EncryptionCaps::None)
181 .primary_user_id(user_id.clone());
182
183 let secret_key_params = key_params
184 .build()
185 .expect("failed to build OpenPGP secret key params");
186
187 let secret_key = secret_key_params
188 .generate(&mut rng)
189 .expect("OpenPGP key generation failed");
190 let public_key = SignedPublicKey::from(secret_key.clone());
191
192 let mut private_binary = Vec::new();
193 secret_key
194 .to_writer(&mut private_binary)
195 .expect("failed to encode OpenPGP private key bytes");
196
197 let mut public_binary = Vec::new();
198 public_key
199 .to_writer(&mut public_binary)
200 .expect("failed to encode OpenPGP public key bytes");
201
202 let private_armor = secret_key
203 .to_armored_string(None.into())
204 .expect("failed to armor OpenPGP private key");
205 let public_armor = public_key
206 .to_armored_string(None.into())
207 .expect("failed to armor OpenPGP public key");
208
209 Inner {
210 user_id,
211 fingerprint: secret_key.fingerprint().to_string(),
212 private_binary: Arc::from(private_binary),
213 private_armor,
214 public_binary: Arc::from(public_binary),
215 public_armor,
216 }
217 })
218}
219
220fn spec_to_key_type(spec: PgpSpec) -> KeyType {
221 match spec {
222 PgpSpec::Rsa2048 => KeyType::Rsa(2048),
223 PgpSpec::Rsa3072 => KeyType::Rsa(3072),
224 PgpSpec::Ed25519 => KeyType::Ed25519,
225 }
226}
227
228fn build_user_id(label: &str) -> String {
229 let display = if label.trim().is_empty() {
230 "fixture"
231 } else {
232 label.trim()
233 };
234
235 let mut local = String::new();
236 for ch in display.chars() {
237 if ch.is_ascii_alphanumeric() {
238 local.push(ch.to_ascii_lowercase());
239 } else if !local.ends_with('-') {
240 local.push('-');
241 }
242 }
243
244 let local = local.trim_matches('-');
245 let local = if local.is_empty() { "fixture" } else { local };
246
247 format!("{display} <{local}@uselesskey.test>")
248}
249
250#[cfg(test)]
251mod tests {
252 use std::io::Cursor;
253
254 use pgp::composed::{Deserializable, SignedPublicKey, SignedSecretKey};
255 use pgp::types::KeyDetails;
256 use uselesskey_core::Seed;
257
258 use super::*;
259
260 #[test]
261 fn deterministic_key_is_stable() {
262 let fx = Factory::deterministic(Seed::from_env_value("pgp-det").unwrap());
263 let a = fx.pgp("issuer", PgpSpec::ed25519());
264 let b = fx.pgp("issuer", PgpSpec::ed25519());
265
266 assert_eq!(a.private_key_armored(), b.private_key_armored());
267 assert_eq!(a.public_key_armored(), b.public_key_armored());
268 assert_eq!(a.fingerprint(), b.fingerprint());
269 }
270
271 #[test]
272 fn random_mode_caches_per_identity() {
273 let fx = Factory::random();
274 let a = fx.pgp("issuer", PgpSpec::rsa_2048());
275 let b = fx.pgp("issuer", PgpSpec::rsa_2048());
276
277 assert_eq!(a.private_key_armored(), b.private_key_armored());
278 }
279
280 #[test]
281 fn different_labels_produce_different_keys() {
282 let fx = Factory::deterministic(Seed::from_env_value("pgp-label").unwrap());
283 let a = fx.pgp("a", PgpSpec::rsa_3072());
284 let b = fx.pgp("b", PgpSpec::rsa_3072());
285
286 assert_ne!(a.private_key_binary(), b.private_key_binary());
287 assert_ne!(a.fingerprint(), b.fingerprint());
288 }
289
290 #[test]
291 fn armored_outputs_have_expected_headers() {
292 let fx = Factory::random();
293 let key = fx.pgp("issuer", PgpSpec::ed25519());
294
295 assert!(
296 key.private_key_armored()
297 .contains("BEGIN PGP PRIVATE KEY BLOCK")
298 );
299 assert!(
300 key.public_key_armored()
301 .contains("BEGIN PGP PUBLIC KEY BLOCK")
302 );
303 }
304
305 #[test]
306 fn armored_outputs_parse_and_match_fingerprint() {
307 let fx = Factory::random();
308 let key = fx.pgp("parser", PgpSpec::ed25519());
309
310 let (secret, _) =
311 SignedSecretKey::from_armor_single(Cursor::new(key.private_key_armored()))
312 .expect("parse armored private key");
313 secret.verify_bindings().expect("verify private bindings");
314
315 let (public, _) = SignedPublicKey::from_armor_single(Cursor::new(key.public_key_armored()))
316 .expect("parse armored public key");
317 public.verify_bindings().expect("verify public bindings");
318
319 assert_eq!(secret.fingerprint().to_string(), key.fingerprint());
320 assert_eq!(public.fingerprint().to_string(), key.fingerprint());
321 }
322
323 #[test]
324 fn binary_outputs_parse() {
325 let fx = Factory::random();
326 let key = fx.pgp("binary", PgpSpec::rsa_2048());
327
328 let secret = SignedSecretKey::from_bytes(Cursor::new(key.private_key_binary()))
329 .expect("parse private key bytes");
330 let public = SignedPublicKey::from_bytes(Cursor::new(key.public_key_binary()))
331 .expect("parse public key bytes");
332
333 assert_eq!(secret.fingerprint().to_string(), key.fingerprint());
334 assert_eq!(public.fingerprint().to_string(), key.fingerprint());
335 }
336
337 #[test]
338 fn mismatched_public_key_differs() {
339 let fx = Factory::deterministic(Seed::from_env_value("pgp-mismatch").unwrap());
340 let key = fx.pgp("issuer", PgpSpec::ed25519());
341
342 let mismatch = key.mismatched_public_key_binary();
343 assert_ne!(mismatch, key.public_key_binary());
344 }
345
346 #[test]
347 fn user_id_is_exposed_and_sanitized() {
348 let fx = Factory::deterministic(Seed::from_env_value("pgp-user-id").unwrap());
349 let key = fx.pgp("Test User!@#", PgpSpec::ed25519());
350 let blank = fx.pgp(" ", PgpSpec::ed25519());
351
352 assert_eq!(key.user_id(), "Test User!@# <test-user@uselesskey.test>");
353 assert_eq!(blank.user_id(), "fixture <fixture@uselesskey.test>");
354 }
355
356 #[test]
357 fn armored_corruption_helpers_are_invalid_and_stable() {
358 let fx = Factory::deterministic(Seed::from_env_value("pgp-corrupt-armor").unwrap());
359 let key = fx.pgp("issuer", PgpSpec::ed25519());
360
361 let bad = key.private_key_armored_corrupt(CorruptPem::BadBase64);
362 assert_ne!(bad, key.private_key_armored());
363 assert!(bad.contains("THIS_IS_NOT_BASE64!!!"));
364 assert!(SignedSecretKey::from_armor_single(Cursor::new(&bad)).is_err());
365
366 let det_a = key.private_key_armored_corrupt_deterministic("corrupt:v1");
367 let det_b = key.private_key_armored_corrupt_deterministic("corrupt:v1");
368 assert_eq!(det_a, det_b);
369 assert_ne!(det_a, key.private_key_armored());
370 assert!(det_a.starts_with('-'));
371 assert!(SignedSecretKey::from_armor_single(Cursor::new(&det_a)).is_err());
372 }
373
374 #[test]
375 fn binary_corruption_helpers_are_invalid_and_stable() {
376 let fx = Factory::deterministic(Seed::from_env_value("pgp-corrupt-bin").unwrap());
377 let key = fx.pgp("issuer", PgpSpec::ed25519());
378
379 let truncated = key.private_key_binary_truncated(32);
380 assert_eq!(truncated.len(), 32);
381 assert!(SignedSecretKey::from_bytes(Cursor::new(&truncated)).is_err());
382
383 let det_a = key.private_key_binary_corrupt_deterministic("corrupt:v1");
384 let det_b = key.private_key_binary_corrupt_deterministic("corrupt:v1");
385 assert_eq!(det_a, det_b);
386 assert_ne!(det_a, key.private_key_binary());
387 assert_eq!(det_a.len(), key.private_key_binary().len());
388 }
389
390 #[test]
391 fn mismatched_public_key_variants_parse_and_fingerprint_differs() {
392 let fx = Factory::deterministic(Seed::from_env_value("pgp-mismatch-parse").unwrap());
393 let key = fx.pgp("issuer", PgpSpec::ed25519());
394
395 let mismatch_bin = key.mismatched_public_key_binary();
396 let mismatch_pub = SignedPublicKey::from_bytes(Cursor::new(&mismatch_bin))
397 .expect("parse mismatched public binary");
398 assert_ne!(mismatch_pub.fingerprint().to_string(), key.fingerprint());
399
400 let mismatch_arm = key.mismatched_public_key_armored();
401 assert_ne!(mismatch_arm, key.public_key_armored());
402 let (mismatch_pub_arm, _) = SignedPublicKey::from_armor_single(Cursor::new(&mismatch_arm))
403 .expect("parse mismatched public armor");
404 assert_ne!(
405 mismatch_pub_arm.fingerprint().to_string(),
406 key.fingerprint()
407 );
408 }
409
410 #[test]
411 fn debug_does_not_leak_key_material() {
412 let fx = Factory::random();
413 let key = fx.pgp("debug", PgpSpec::ed25519());
414 let dbg = format!("{key:?}");
415
416 assert!(dbg.contains("PgpKeyPair"));
417 assert!(dbg.contains("debug"));
418 assert!(!dbg.contains("BEGIN PGP PRIVATE KEY BLOCK"));
419 }
420}