1use std::convert::TryInto;
10use std::io;
11use std::io::BufRead;
12use std::str::FromStr;
13use std::time::SystemTime;
14
15use anyhow::{Context, Result};
16use chbs::probability::Probability;
17use sequoia_openpgp::armor;
18use sequoia_openpgp::cert;
19use sequoia_openpgp::cert::amalgamation::key::ValidKeyAmalgamation;
20use sequoia_openpgp::cert::amalgamation::{ValidAmalgamation, ValidateAmalgamation};
21use sequoia_openpgp::cert::prelude::ComponentAmalgamation;
22use sequoia_openpgp::cert::{CertParser, CipherSuite as SeqCipherSuite};
23use sequoia_openpgp::crypto::KeyPair;
24use sequoia_openpgp::packet::signature::SignatureBuilder;
25use sequoia_openpgp::packet::{signature, Signature, UserID};
26use sequoia_openpgp::parse::{PacketParser, Parse};
27use sequoia_openpgp::policy::StandardPolicy;
28use sequoia_openpgp::serialize::{Serialize, SerializeInto};
29use sequoia_openpgp::types::{KeyFlags, RevocationStatus, SignatureType};
30use sequoia_openpgp::{Cert, Fingerprint, KeyHandle, Packet};
31use sha2::Digest;
32
33pub(crate) const CA_KEY_NOTATION: &str = "openpgp-ca@notations.sequoia-pgp.org";
34
35pub(crate) const SECONDS_IN_DAY: u64 = 60 * 60 * 24;
36
37pub(crate) const SP: &StandardPolicy<'static> = &StandardPolicy::new();
38
39fn diceware() -> String {
41 use chbs::{config::BasicConfig, prelude::*};
42
43 let config = BasicConfig {
44 capitalize_first: Probability::Never,
45 capitalize_words: Probability::Never,
46 ..Default::default()
47 };
48 config.to_scheme().generate()
49}
50
51pub(crate) fn ca_user_id(email: &str, name: Option<&str>) -> UserID {
52 let name = match name {
53 Some(name) => Some(name),
54 None => Some("OpenPGP CA"),
55 };
56
57 user_id(email, name)
58}
59
60fn user_id(email: &str, name: Option<&str>) -> UserID {
61 if let Some(name) = name {
62 UserID::from(format!("{name} <{email}>"))
63 } else {
64 UserID::from(format!("<{email}>"))
65 }
66}
67
68pub(crate) fn add_ca_domain_notation(
70 sb: SignatureBuilder,
71 domain: &str,
72) -> Result<SignatureBuilder> {
73 sb.add_notation(
74 CA_KEY_NOTATION,
75 (format!("domain={domain}")).as_bytes(),
76 signature::subpacket::NotationDataFlags::empty().set_human_readable(),
77 false,
78 )
79}
80
81pub(crate) fn make_ca_cert(
90 domain: &str,
91 name: Option<&str>,
92 cipher_suite: Option<CipherSuite>,
93) -> Result<(Cert, Signature)> {
94 let (mut ca_key, revocation) = cert::CertBuilder::new()
96 .set_cipher_suite(cipher_suite.unwrap_or(CipherSuite::Cv25519).into())
97 .add_signing_subkey()
98 .generate()?;
101
102 let mut keypair = ca_key
104 .primary_key()
105 .key()
106 .clone()
107 .parts_into_secret()?
108 .into_keypair()?;
109
110 let dks = ca_key
112 .with_policy(SP, None)?
113 .direct_key_signature()
114 .cloned();
115
116 ca_key = ca_key
118 .into_tsk()
119 .into_packets()
120 .filter(|p| match p {
121 Packet::Signature(s) => s.typ() != SignatureType::DirectKey,
122 _ => true,
123 })
124 .collect::<Vec<_>>()
125 .try_into()?;
126
127 if let Ok(sig) = dks {
129 let sb = SignatureBuilder::from(sig);
130 let sb = add_ca_domain_notation(sb, domain)?;
131
132 let s = sb
133 .sign_direct_key(&mut keypair, None)?;
135
136 let p: Packet = s.into();
137 (ca_key, _) = ca_key.insert_packets2(vec![p])?;
138 } else {
139 return Err(anyhow::anyhow!(
140 "Unexpected missing DirectKey Signature in make_ca_cert()"
141 ));
142 }
143
144 let email = format!("openpgp-ca@{domain}");
146 let userid = ca_user_id(&email, name);
147
148 let direct_key_sig = ca_key
149 .primary_key()
150 .with_policy(SP, None)?
151 .binding_signature();
152
153 let builder = signature::SignatureBuilder::from(direct_key_sig.clone())
154 .set_type(SignatureType::PositiveCertification)
155 .set_key_flags(KeyFlags::empty().set_certification())?;
156
157 let binding = userid.bind(&mut keypair, &ca_key, builder)?;
158
159 let ca = ca_key.insert_packets(vec![Packet::from(userid), binding.into()])?;
161
162 Ok((ca, revocation))
163}
164
165#[allow(clippy::too_many_arguments)]
174pub(crate) fn make_user_cert(
175 emails: &[&str],
176 name: Option<&str>,
177 password: bool,
178 password_file: Option<String>,
179 cipher_suite: Option<CipherSuite>,
180 enable_encryption_subkey: bool,
181 enable_signing_subkey: bool,
182 enable_authentication_subkey: bool,
183) -> Result<(Cert, Signature, Option<String>)> {
184 let pass = if password {
185 let pw = match password_file {
187 None => diceware(), Some(file) => {
189 if &file == "-" {
191 let mut buffer = String::default();
193 io::stdin().lock().read_line(&mut buffer)?;
194
195 buffer
196 } else {
197 let mut f = std::fs::File::open(&file)?;
199 io::read_to_string(&mut f)?
200 }
201 }
202 };
203
204 Some(pw)
205 } else {
206 None
207 };
208
209 let mut builder = cert::CertBuilder::new()
210 .set_cipher_suite(cipher_suite.unwrap_or(CipherSuite::Cv25519).into());
211
212 if enable_encryption_subkey {
213 builder = builder.add_subkey(
214 KeyFlags::empty()
215 .set_transport_encryption()
216 .set_storage_encryption(),
217 None,
218 None,
219 );
220 }
221
222 if enable_signing_subkey {
223 builder = builder.add_signing_subkey();
224 }
225
226 if enable_authentication_subkey {
227 builder = builder.add_authentication_subkey();
228 }
229
230 if let Some(pass) = &pass {
231 builder = builder.set_password(Some(pass.to_owned().into()));
232 }
233
234 for email in emails {
235 builder = builder.add_userid(user_id(email, name));
236 }
237
238 let (cert, revocation) = builder.generate()?;
239 Ok((cert, revocation, pass))
240}
241
242pub fn cert_to_armored(cert: &Cert) -> Result<String> {
244 let v = cert.armored().to_vec().context("Cert serialize failed")?;
245
246 Ok(String::from_utf8(v)?)
247}
248
249pub fn certs_to_armored(certs: &[Cert]) -> Result<String> {
254 let mut writer = armor::Writer::new(Vec::new(), armor::Kind::PublicKey)?;
255
256 for cert in certs {
257 cert.export(&mut writer)?;
258 }
259 let buffer = writer.finalize()?;
260
261 Ok(String::from_utf8_lossy(&buffer).to_string())
262}
263
264pub fn cert_to_armored_private_key(cert: &Cert) -> Result<String> {
266 let mut buffer = vec![];
267
268 let headers: Vec<_> = cert
269 .armor_headers()
270 .into_iter()
271 .map(|value| ("Comment", value))
272 .collect();
273
274 let mut writer = armor::Writer::with_headers(&mut buffer, armor::Kind::SecretKey, headers)?;
275
276 cert.as_tsk().serialize(&mut writer)?;
277 writer.finalize()?;
278
279 Ok(String::from_utf8(buffer)?)
280}
281
282pub fn armored_keyring_to_certs<D: AsRef<[u8]> + Send + Sync>(armored: &D) -> Result<Vec<Cert>> {
284 let ppr = PacketParser::from_bytes(armored)?;
285
286 let mut res = vec![];
287 for cert in CertParser::from(ppr) {
288 res.push(cert?);
289 }
290
291 Ok(res)
292}
293
294pub fn to_cert(data: &[u8]) -> Result<Cert> {
296 let cert = Cert::from_bytes(data).context("Cert::from_bytes failed")?;
297
298 Ok(cert)
299}
300
301pub fn to_signature(data: &[u8]) -> Result<Signature> {
303 let p = Packet::from_bytes(data).context("Input could not be parsed")?;
304
305 if let Packet::Signature(s) = p {
306 Ok(s)
307 } else {
308 Err(anyhow::anyhow!("Couldn't convert to Signature"))
309 }
310}
311
312pub fn revoc_to_armored(sig: &Signature, headers: Option<Vec<(String, String)>>) -> Result<String> {
319 let mut buf = vec![];
320 {
321 let rev = Packet::Signature(sig.clone());
322
323 let mut writer = armor::Writer::with_headers(
324 &mut buf,
325 armor::Kind::PublicKey,
326 headers.unwrap_or_default(),
327 )?;
328 rev.export(&mut writer)?;
329 writer.finalize()?;
330 }
331
332 Ok(String::from_utf8(buf)?)
333}
334
335pub fn get_expiry(cert: &Cert) -> Result<Option<SystemTime>> {
337 let primary = cert.primary_key().with_policy(SP, None)?;
338 Ok(primary.key_expiration_time())
339}
340
341pub fn is_possibly_revoked(cert: &Cert) -> bool {
343 RevocationStatus::NotAsFarAsWeKnow != cert.revocation_status(SP, None)
344}
345
346pub(crate) fn normalize_fp(fp: &str) -> Result<String> {
349 Ok(Fingerprint::from_hex(fp)?.to_hex())
350}
351
352pub fn get_revoc_issuer_fp(revoc_cert: &Signature) -> Result<Option<Fingerprint>> {
353 let issuers = revoc_cert.get_issuers();
354 let sig_fingerprints: Vec<&Fingerprint> = issuers
355 .iter()
356 .filter_map(|keyhandle| {
357 if let KeyHandle::Fingerprint(fp) = keyhandle {
358 Some(fp)
359 } else {
360 None
361 }
362 })
363 .collect();
364
365 match sig_fingerprints.len() {
366 0 => Ok(None),
367 1 => Ok(Some(sig_fingerprints[0].clone())),
368 _ => Err(anyhow::anyhow!(
369 "ERROR: expected 0 or 1 issuer fingerprints in revocation"
370 )),
371 }
372}
373
374pub(crate) fn revocation_to_hash(data: &[u8]) -> Result<String> {
379 let sig = to_signature(data)?;
380
381 let p: Packet = sig.into();
382 let bits = p.to_vec()?;
383
384 use sha2::Sha256;
385
386 let mut hasher = Sha256::new();
387 hasher.update(bits);
388 let hash64 = &hasher.finalize()[0..8];
389
390 let hex = hash64
391 .iter()
392 .map(|d| format!("{d:02X}"))
393 .collect::<Vec<_>>()
394 .concat();
395
396 Ok(hex)
397}
398
399pub fn tsign(signee: Cert, signer: &Cert, pass: Option<&str>) -> Result<Cert> {
402 let mut cert_keys = get_cert_keys(signer, pass);
403
404 if cert_keys.is_empty() {
405 return Err(anyhow::anyhow!(
406 "tsign(): signer has no valid, certification capable subkey"
407 ));
408 }
409
410 let mut sigs: Vec<Signature> = Vec::new();
411
412 for ca_uidb in signee.userids() {
414 for signer in &mut cert_keys {
415 let builder = signature::SignatureBuilder::new(SignatureType::GenericCertification)
416 .set_trust_signature(255, 120)?;
417
418 let tsig = ca_uidb.userid().bind(signer, &signee, builder)?;
419 sigs.push(tsig);
420 }
421 }
422
423 let signed = signee.insert_packets(sigs)?;
424
425 Ok(signed)
426}
427
428pub(crate) fn merge_in_tsigs(ca_cert: Cert, import: Cert) -> Result<Cert> {
431 if ca_cert.fingerprint() != import.fingerprint() {
433 return Err(anyhow::anyhow!(
434 "The imported cert has an unexpected Fingerprint",
435 ));
436 }
437
438 let tsigs = get_trust_sigs(&import)?;
440
441 let mut packets: Vec<Packet> = Vec::new();
443 tsigs.iter().for_each(|s| packets.push(s.clone().into()));
444
445 ca_cert
446 .insert_packets(packets)
447 .context("Merging tsigs into CA Key failed")
448}
449
450pub(crate) fn get_cert_keys(cert: &Cert, password: Option<&str>) -> Vec<KeyPair> {
452 let keys = cert
453 .keys()
454 .with_policy(SP, None)
455 .alive()
456 .revoked(false)
457 .for_certification()
458 .secret();
459
460 keys.filter_map(|ka: ValidKeyAmalgamation<_, _, _>| {
461 let mut ka = ka.key().clone();
462
463 if let Some(password) = password {
464 ka = ka.decrypt_secret(&password.into()).ok()?
465 }
466
467 ka.into_keypair().ok()
468 })
469 .collect()
470}
471
472pub fn print_cert_info(data: &[u8]) -> Result<()> {
475 let c = to_cert(data)?;
476 for uid in c.userids() {
477 println!("User ID: {}", uid.userid());
478 }
479 println!("Fingerprint '{c}'");
480 Ok(())
481}
482
483pub(crate) fn cert_has_uid_in_domain(c: &Cert, domain: &str) -> Result<bool> {
485 for uid in c.userids() {
486 let email = uid.email2()?;
488 if let Some(email) = email {
489 let split: Vec<_> = email.split('@').collect();
490
491 if split.len() != 2 {
492 return Err(anyhow::anyhow!("unexpected email format"));
493 }
494
495 if split[1] == domain {
496 return Ok(true);
497 }
498 }
499 }
500
501 Ok(false)
502}
503
504pub(crate) fn get_trust_sigs(c: &Cert) -> Result<Vec<Signature>> {
506 Ok(get_third_party_sigs(c)?
507 .iter()
508 .filter(|s| s.trust_signature().is_some())
509 .cloned()
510 .collect())
511}
512
513fn get_third_party_sigs(c: &Cert) -> Result<Vec<Signature>> {
515 let mut res = Vec::new();
516
517 for uid in c.userids() {
518 let sigs = uid.with_policy(SP, None)?.bundle().certifications2();
519 sigs.for_each(|s| res.push(s.clone()));
520 }
521
522 Ok(res)
523}
524
525pub fn valid_certifications_by(
528 uid: &ComponentAmalgamation<UserID>,
529 cert: &Cert,
530 certifier: Cert,
531) -> Vec<Signature> {
532 let certifier_keys: Vec<_> = certifier
533 .keys()
534 .with_policy(SP, None)
535 .alive()
536 .revoked(false)
537 .for_certification()
538 .collect();
539
540 let certifier_fp = certifier.fingerprint();
541
542 let pk = cert.primary_key();
543
544 uid.certifications()
545 .filter(|&s| {
546 s.issuer_fingerprints()
548 .any(|issuer| issuer == &certifier_fp)
549 })
550 .filter(|&s| {
551 certifier_keys
553 .iter()
554 .any(|signer| s.clone().verify_userid_binding(signer, &pk, uid).is_ok())
555 })
556 .cloned()
557 .collect()
558}
559
560#[derive(Clone)]
561pub enum CipherSuite {
562 Cv25519,
563 RSA3k,
564 P256,
565 P384,
566 P521,
567 RSA2k,
568 RSA4k,
569}
570
571impl From<CipherSuite> for SeqCipherSuite {
572 fn from(value: CipherSuite) -> Self {
573 match value {
574 CipherSuite::Cv25519 => SeqCipherSuite::Cv25519,
575 CipherSuite::RSA3k => SeqCipherSuite::RSA3k,
576 CipherSuite::P256 => SeqCipherSuite::P256,
577 CipherSuite::P384 => SeqCipherSuite::P384,
578 CipherSuite::P521 => SeqCipherSuite::P521,
579 CipherSuite::RSA2k => SeqCipherSuite::RSA2k,
580 CipherSuite::RSA4k => SeqCipherSuite::RSA4k,
581 }
582 }
583}
584
585impl FromStr for CipherSuite {
586 type Err = &'static str;
587
588 fn from_str(s: &str) -> Result<Self, Self::Err> {
589 Ok(match s.to_lowercase().as_str() {
590 "cv25519" => CipherSuite::Cv25519,
591 "rsa3k" => CipherSuite::RSA3k,
592 "p256" => CipherSuite::P256,
593 "p384" => CipherSuite::P384,
594 "p521" => CipherSuite::P521,
595 "rsa2k" => CipherSuite::RSA2k,
596 "rsa4k" => CipherSuite::RSA4k,
597 _ => return Err("Unknown cipher suite"),
598 })
599 }
600}