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