openpgp_ca_lib/
pgp.rs

1// SPDX-FileCopyrightText: 2019-2022 Heiko Schaefer <heiko@schaefer.name>
2// SPDX-License-Identifier: GPL-3.0-or-later
3//
4// This file is part of OpenPGP CA
5// https://gitlab.com/openpgp-ca/openpgp-ca
6
7//! PGP helper functions.
8
9use 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
39// FIXME: configurable dictionaries, ... ?
40fn 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
68/// notation: "openpgp-ca:domain=domain1;domain2"
69pub(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
81/// Generate a new CA key (and a revocation).
82///
83/// `domain` is the domainname for the CA (such as `example.org`).
84/// A UserID for the CA is generated with the localpart `openpgp-ca`
85/// (so for example `openpgp-ca@example.org`).
86///
87/// `name` is an optional additional identifier that is added to the
88/// UserID, if it is supplied.
89pub(crate) fn make_ca_cert(
90    domain: &str,
91    name: Option<&str>,
92    cipher_suite: Option<CipherSuite>,
93) -> Result<(Cert, Signature)> {
94    // Generate key for a new CA
95    let (mut ca_key, revocation) = cert::CertBuilder::new()
96        .set_cipher_suite(cipher_suite.unwrap_or(CipherSuite::Cv25519).into())
97        .add_signing_subkey()
98        // FIXME: set expiration from CLI
99        // .set_validity_period()
100        .generate()?;
101
102    // Get keypair for the CA primary key, as a Signer
103    let mut keypair = ca_key
104        .primary_key()
105        .key()
106        .clone()
107        .parts_into_secret()?
108        .into_keypair()?;
109
110    // Get a copy of the current DKS
111    let dks = ca_key
112        .with_policy(SP, None)?
113        .direct_key_signature()
114        .cloned();
115
116    // Remove DKS from cert
117    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    // Add notation to DKS
128    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            // Update the direct key signature.
134            .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    // Generate a userid and a binding signature
145    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    // Merge the User ID and binding signature into the Cert.
160    let ca = ca_key.insert_packets(vec![Packet::from(userid), binding.into()])?;
161
162    Ok((ca, revocation))
163}
164
165/// Make a user Cert (with User IDs for each of `emails`).
166///
167///
168/// The optional additional identifier `name` is added to each User ID,
169/// if supplied.
170///
171/// If `password` is true, the generated private key will be password
172/// protected (with a generated diceware password).
173#[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        // The user wants to set a password, figure out how we acquire it
186        let pw = match password_file {
187            None => diceware(), // We generate a new, random password
188            Some(file) => {
189                // A password is provided by the user
190                if &file == "-" {
191                    // Get password from stdin
192                    let mut buffer = String::default();
193                    io::stdin().lock().read_line(&mut buffer)?;
194
195                    buffer
196                } else {
197                    // Get password from `file`
198                    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
242/// make a "public key" ascii-armored representation of a Cert
243pub 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
249/// Get the armored "public keyring" representation of a set of Certs.
250///
251/// This transformation strips non-exportable signatures, and any components bound merely by
252/// non-exportable signatures.
253pub 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
264/// Get "private key" armored representation of a Cert
265pub 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
282/// Make a Vec of Cert from an armored key(ring)
283pub 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
294/// Returns the first Cert found in 'data'.
295pub 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
301/// Get a Signature object from signature data (optionally armored)
302pub 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
312/// Make an armored representation of a revocation signature.
313///
314/// Errors for non-exportable signatures.
315///
316/// Note:this uses `armor::Kind::PublicKey`, because GnuPG doesn't
317/// seem to accept revocations with the `armor::Kind::Signature` kind.
318pub 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
335/// Get expiration time of cert as a SystemTime
336pub 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
341/// Is cert (possibly) revoked?
342pub fn is_possibly_revoked(cert: &Cert) -> bool {
343    RevocationStatus::NotAsFarAsWeKnow != cert.revocation_status(SP, None)
344}
345
346/// Normalize pretty-printed fingerprint strings (with spaces etc)
347/// into a format with no spaces and uppercase characters
348pub(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
374/// Generate a 64 bit sized hash of a revocation certificate
375/// (represented as 16 character hex strings).
376///
377/// These hashes can be used to refer to specific revocations.
378pub(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
399/// `signer` tsigns the `signee` key.
400/// Each User ID of signee gets certified.
401pub 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    // Create a tsig for each UserID
413    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
428/// Merge new CA tsigs from `import` into `ca_cert`.
429/// Return merged Cert as TSK (if available).
430pub(crate) fn merge_in_tsigs(ca_cert: Cert, import: Cert) -> Result<Cert> {
431    // The imported cert must have the same Fingerprint as the CA cert
432    if ca_cert.fingerprint() != import.fingerprint() {
433        return Err(anyhow::anyhow!(
434            "The imported cert has an unexpected Fingerprint",
435        ));
436    }
437
438    // Get the third party tsig(s) from the imported cert
439    let tsigs = get_trust_sigs(&import)?;
440
441    // add tsig(s) to our "own" version of the CA key
442    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
450/// Get all valid, certification capable keys (with secret key material)
451pub(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
472// -------- helper functions
473
474pub 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
483/// Does any User ID of this cert use an email address in "domain"?
484pub(crate) fn cert_has_uid_in_domain(c: &Cert, domain: &str) -> Result<bool> {
485    for uid in c.userids() {
486        // is any uid in domain
487        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
504/// Get all trust sigs on User IDs in this Cert
505pub(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
513/// Get all third party sigs on User IDs in this Cert
514fn 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
525/// For User ID `uid` (which is a part of `cert`):
526/// find all valid certifications that have been made by `certifier`.
527pub 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            // does the signature appear to be issued by `certifier`?
547            s.issuer_fingerprints()
548                .any(|issuer| issuer == &certifier_fp)
549        })
550        .filter(|&s| {
551            // check if the apparent certification by `certifier` is valid
552            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}