//! cryptographic functions use std::io::Write; use std::sync::Arc; use std::time::Duration; use std::collections::HashMap; use std::collections::hash_map::Entry::{Occupied, Vacant}; use anyhow; use sequoia_openpgp::cert::ValidCert; use tokio::task::JoinSet; use bytes::Bytes; use sequoia_net::{KeyServer, wkd, dane}; use sequoia_openpgp::packet::prelude::SignatureBuilder; use sequoia_openpgp::parse::{PacketParser, PacketParserResult, Parse}; use sequoia_openpgp::policy::StandardPolicy; use sequoia_openpgp::serialize::stream::{Message, Armorer, Encryptor, LiteralWriter}; use sequoia_openpgp::{Fingerprint, Cert, Packet}; use sequoia_openpgp::cert::raw::RawCertParser; use sequoia_openpgp::types::SignatureType; use sequoia_cert_store::{Store, CertStore, LazyCert, StoreUpdate}; use sequoia_cert_store::store::MergePublicCollectStats; use sequoia_wot::{self as wot}; use wot::store::Store as _; use wot::{Depth, Path}; use crate::types::errors::HuskError; use crate::types::husk_context::HuskContext; use crate::types::introducer::Introducer; use crate::types::recipient::Recipient; use crate::mail; use crate::common::escape; /// Returns the local trust root from the cert_store. If the trust root /// cannot be found, it returns an error. pub fn get_local_trust_root(cert_store: &CertStore) -> anyhow::Result { let root = cert_store.certd() .and_then(|certd| { match certd.certd().get(sequoia_cert_store::store::openpgp_cert_d::TRUST_ROOT) { Ok(Some((_tag, bytes))) => Some(bytes), Ok(None) => None, Err(_) => None } }) .and_then(|bytes| { match RawCertParser::from_bytes(&bytes[..]) { Ok(mut parser) => { match parser.next() { Some(Ok(cert)) => { Cert::from_bytes(cert.as_bytes()).ok() }, Some(Err(_)) | None => None } }, Err(_) => None, } }); match root { Some(r) => Ok(r), None => Err(HuskError::NoLocalTrustRoot.into()) } } /// Returns all local certificates with an authenticated binding to user id 'email'. /// The certificates are not checked for usefulness (for instance, they might /// miss an encryption subkey). pub fn get_local_certificates<'hc>(context: &HuskContext<'hc>, email: &str) -> Vec>> { log::debug!("local lookup for {}", email); let cert_store = &context.cert_store; let trust_roots = vec![(context.local_trust_root.fingerprint(), wot::FULLY_TRUSTED)]; let wot_store = wot::store::CertStore::from_store(cert_store, &context.policy, None); let nb = wot::NetworkBuilder::rooted(&wot_store, &*trust_roots); let n = nb.build(); let bindings: Vec<_> = n.lookup_synopses_by_email(email) .into_iter() .map(|(fp, userid)| (fp, Some(userid))) .collect(); let mut result = vec![]; for (fpr, some_userid) in bindings { if let Some(userid) = some_userid { let paths = n.authenticate(userid, &fpr, wot::FULLY_TRUSTED); for (path, _) in paths.iter() { if path.amount() >= wot::FULLY_TRUSTED { log::debug!("{} authenticated!!!", email); if let Ok(cert) = cert_store.lookup_by_cert_fpr(&path.target().fingerprint()) { result.push(cert); } } } } } result } const USER_AGENT: &'static str = concat!("husk/", env!("CARGO_PKG_VERSION")); const CONNECT_TIMEOUT: Duration = Duration::new(5, 0); const REQUEST_TIMEOUT: Duration = Duration::new(5, 0); /// Try to fetch certificates for `email` from online sources (keyservers, wkd, /// dane). pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str) -> anyhow::Result> { log::debug!("remote lookup for {}", email); let mut jobs = JoinSet::new(); let http_client = sequoia_net::reqwest::Client::builder() .user_agent(USER_AGENT) .connect_timeout(CONNECT_TIMEOUT) .timeout(REQUEST_TIMEOUT) .build()?; // local function to check certificates for usability let collect_certs = |certs: Vec>, email: String, policy: &StandardPolicy| { let mut result = Vec::new(); for cert in certs.into_iter().flatten() { if let Ok(vc) = cert.with_policy(policy, None) { // check if the returned certificate contains a userid with the // email address in question. let mut userid_found = false; for userid in vc.userids() { if let Ok(Some(u)) = userid.userid().email() { if u == email { userid_found = true; } } } if userid_found { // check if the certificate can be used (for encryption). vc.keys() .supported() .alive() .revoked(false) .for_transport_encryption() .for_each(|_| { // XXX: cert gets returned for each transport key // once would be enough result.push(cert.clone()); }); } } } result }; // Keyservers for keyserver in &context.keyservers { log::debug!("querying {}", keyserver); if let Ok(ks) = KeyServer::with_client(keyserver.as_str(), http_client.clone()) { let e = email.to_string(); let p = StandardPolicy::new(); jobs.spawn(async move { let mut partial_result = Vec::new(); if let Ok(certs) = ks.search(e.as_str()).await { partial_result = collect_certs(certs, e, &p); } partial_result }); } } // WKD let e = email.to_string(); let p = StandardPolicy::new(); jobs.spawn(async move { let mut partial_result = Vec::new(); if let Ok(certs) = wkd::get(&http_client.clone(), e.as_str()).await { partial_result = collect_certs(certs, e, &p); } partial_result }); // DANE let e = email.to_string(); let p = StandardPolicy::new(); jobs.spawn(async move { let mut partial_result = Vec::new(); if let Ok(certs) = dane::get(e.as_str()).await { partial_result = collect_certs(certs, e, &p); } partial_result }); // join the partial results. As the same certificate might be returned by // several sources, dedup and merge the certificates. let mut set: HashMap = HashMap::new(); let results = jobs.join_all().await; results.into_iter().flatten() .for_each(|c| { let fpr = c.fingerprint(); log::debug!("found cert {}", fpr); match set.entry(fpr.clone()) { Occupied(entry) => { let existing_cert = entry.into_mut(); if let Ok(merged_cert) = existing_cert.clone().merge_public(c) { *existing_cert = merged_cert; } }, Vacant(_entry) => { set.insert(fpr.clone(), c); }, } }); let result = set.into_values().collect(); Ok(result) } pub async fn get_certificates<'hc>(context: &HuskContext<'hc>, email: &str) -> Vec>> { log::debug!("get certificate for {}", email); let locals = get_local_certificates(context, email); if !locals.is_empty() { return locals; } let mut result = Vec::new(); for introducer in &context.introducers { log::debug!("consider introducer {:?}", introducer.cert.userids().map(|u| u.userid()).collect::>()); if introducer.can_introduce(format!("<{}>", email).as_str()) { log::debug!(" introducer regex matches!"); match lookup_certificates(context, email).await { Ok(certs) => { for cert in certs { if introducer.is_introducing(&cert) { let lazy_cert = Arc::new(LazyCert::from_cert(cert)); if context.cert_store.update(lazy_cert.clone()).is_ok() { result.push(lazy_cert); } } } }, Err(e) => { log::error!("{}", HuskError::KeyLookupError(email.to_string(), e.to_string())); } } } } result } /// Returns all introducers available in cert_store. pub fn get_introducers<'c>(cert_store: &CertStore<'c>, policy: &StandardPolicy, local_trust_root: Fingerprint) -> Vec { let mut result = Vec::new(); let trust_roots = vec![(local_trust_root, wot::FULLY_TRUSTED)]; let wot_store = wot::store::CertStore::from_store(cert_store, policy, None); let n = wot::NetworkBuilder::rooted(&wot_store, &*trust_roots) .build(); n.certified_userids().iter().for_each(|(fpr, user_id)| { let paths = n.authenticate(user_id, fpr, wot::FULLY_TRUSTED); // find the longest path (highest trust depth) let mut depth = Depth::Limit(0); let mut target: Option<&Path> = None; paths.iter().for_each(|(path, amount)| { if *amount == wot::FULLY_TRUSTED { let d = path.residual_depth(); if d > depth { depth = d; target = Some(path); } } }); if depth > Depth::Limit(0) { if let Some(p) = target { if let Some(c) = p.certifications().last() { result.push( Introducer { cert: p.target().clone(), certification: c.clone(), }); } } } }); result } /// Checks if a message contains the OpenPGP artifacts of an encrypted message. pub fn is_encrypted(body: &Bytes) -> bool { let mut ppr = match PacketParser::from_bytes(body) { Ok(pp) => pp, Err(_) => return false, // no OpenPGP artifacts }; let mut encrypted: bool = false; while let PacketParserResult::Some(pp) = ppr { if let Packet::SEIP(_) = pp.packet { encrypted = true; } // parse the whole message, don't stop at first SEIP ppr = match pp.recurse() { Ok((_, next_ppr)) => next_ppr, Err(_) => return false, // broken artifact -> not encrypted } } if let PacketParserResult::EOF(eof) = ppr { if eof.is_message().is_ok() && encrypted { return true; } } false } /// Encrypt a message for `recipients`. pub fn encrypt(context: &HuskContext<'_>, body: &Bytes, recipients: &Vec) -> anyhow::Result { let mut keys = Vec::new(); for rcpt in recipients { if rcpt.can_encrypt() { for cert in &rcpt.certs { cert.with_policy(&context.policy, None)? .keys() .supported() .alive() .revoked(false) .for_transport_encryption() .for_each(|k| { keys.push(k); }); } } } if !keys.is_empty() { let mut sink = Vec::new(); let message = Message::new(&mut sink); let message = Armorer::new(message).build()?; let message = Encryptor::for_recipients(message, keys).build()?; let mut message = LiteralWriter::new(message).build()?; message.write_all(&body[..])?; message.finalize()?; Ok(Bytes::from(sink)) } else { Err(HuskError::NoEncryptionKeys.into()) } } /// Authenticate a certificate. /// /// If `activate` is true, the certificate becomes an introducer for /// `domains`. Passing `None` or an empty list in this case is an error. /// If `activate` is false, create a certifiaction with minimal trust_roots /// amount and no depth. In this case `domains` is ignored. pub fn authenticate(context: &HuskContext, cert: &ValidCert, activate: bool, domains: Option>) -> anyhow::Result<()> { let policy = &context.policy; let root_signing_key = match context.local_trust_root.with_policy(policy, None)? .keys() .supported() .alive() .revoked(false) .for_certification() .next() { Some(k) => k.key(), None => { return Err(HuskError::DamangedLocalTrustRoot( "missing certification capability".to_string()) .into()); } }; let mut root_signer = root_signing_key .clone() .parts_into_secret()? .into_keypair()?; // Create the SignatureBuilder with a default preconfiguration. let mut builder = SignatureBuilder::new(SignatureType::GenericCertification) .set_signature_creation_time(std::time::SystemTime::now())? .set_exportable_certification(false)? .set_revocable(true)?; // set no expiration date // Add domains if activate { // set the trust level and depth for an active introducer. builder = builder.set_trust_signature(1, 120)?; if let Some(domains) = domains { for domain in domains { if !mail::is_domain(domain.as_str()) { return Err(HuskError::InvalidDomainName(domain).into()); } builder = builder.add_regular_expression(format!("<[^>]+[@.]{}>$", escape::regex_ctrl_escape(domain.as_str())? ))?; } } else { // An introducer without domains would be unlimited. return Err(HuskError::UnlimitedIntroducer.into()); } } else { // deactive the introducer builder = builder.set_trust_signature(0, 1)?; } // For a certification, user ids are needed. Build a list of // valid (non self revoked) user ids. let user_ids: Vec<_> = cert.userids() .filter(|u| { u.self_revocations().count() == 0 }) .collect(); if user_ids.is_empty() { return Err(HuskError::NoUserIDs(cert.fingerprint().to_hex()).into()); } // now certify let signatures: Vec<_> = user_ids.into_iter() .filter_map(|u| { let b = builder.clone(); b.sign_userid_binding( &mut root_signer, cert.primary_key().key(), u.userid()).ok() }) .collect(); let (new_cert, _) = cert.cert().clone().insert_packets(signatures)?; // and (re)import let new_cert = Arc::new(new_cert.into()); let stats = MergePublicCollectStats::new(); let cert_store = &context.cert_store; cert_store.update_by(new_cert, &stats)?; log::debug!("{:?}", stats); // XXX: do something with the created stats Ok(()) } pub fn has_certification_capability(cert: &ValidCert) -> bool { cert.keys() .for_certification() .alive() .supported() .revoked(false) .count() > 0 } #[cfg(test)] pub mod tests { #[test] pub fn test_is_encrypted() { use super::*; // cleartext let body = b"Hello world"; let body = Bytes::from(&body[..]); assert!(! is_encrypted(&body)); // a valid encrypted message let body = b"-----BEGIN PGP MESSAGE----- wWwGFQRhclhA/Qg3CiL81NAswyOWkyk8jxIBB0AmlBkVEVbQst60C/1+mcmIxLbG m0w+qv2aP5NQhAetCzDgA/KEhtim0Ebzf06jJG7bITPkYmfN2mizdChgv1f3y2eQ 80hYpgbD930RtKXTD1TSWgIJAgai2MelZrDAQXIaFKlRRY9u7R1YIXQmMa6uDced W/g9mJDd0ral30VL0FfuKRR2Je2TRfizqMb4tL4EIRFqrfSBesb9WcniEfBekG3B jk6lh01Ej9QTGQ== -----END PGP MESSAGE----- "; let body = Bytes::from(&body[..]); assert!(is_encrypted(&body)); // a PUBLIC KEY BLOCK is an artifact, but it's not encrypted let body = b"-----BEGIN PGP PUBLIC KEY BLOCK----- xjMEaUE/aRYJKwYBBAHaRw8BAQdAXScnsTjHmy1yZ/SmddPq7tmY4SZwsyWmRNei EX60UJDCwBEEHxYKAIMFgmlBP2kFiQWkj70DCwkHCRC9/0MioyzN/0cUAAAAAAAe ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfUHGvbmmp7vFwpMzGj36fZ Px+taozYHxyGljxsJnlV3AMVCggCmwECHgkWIQSQO2vdjfwfK4J6FaK9/0MioyzN /wAATokBAP9abr1BeV2qrvwF9TsQDlvgnaNPtzhPy00oDi17/8x5AP99DLK8hAJ3 FtejeAJ0/2+BfUZzRlF8tlSGe5/Iln9DCM0TPGFsaWNlQGV4YW1wbGUuY29tPsLA FAQTFgoAhgWCaUE/aQWJBaSPvQMLCQcJEL3/QyKjLM3/RxQAAAAAAB4AIHNhbHRA bm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZyWqUJWT+0+sGq1ci9IkqHvnsUN+c5LR B921ZbGlCwLXAxUKCAKZAQKbAQIeCRYhBJA7a92N/B8rgnoVor3/QyKjLM3/AAB5 +AEAvMCVVynlwsaFQvACga9NTfyZGQ7IjhFhjbCXY+CZbHoA/0FaajzhUF6mKQH7 RMwMK6Dp0YD2A34/DGb5KbDkLjIOzjMEaUE/aRYJKwYBBAHaRw8BAQdAd4Y3kt2s NszsrkMELS0FC2viTw+NEU0ZnAT/aD5eydLCwMUEGBYKATcFgmlBP2kFiQWkj70J EL3/QyKjLM3/RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9y Z1R29gVOHk9hllft18uHrwEV0MgvzykG3yWKF1B+vT9hApsCvqAEGRYKAG8FgmlB P2kJEHx7uc2lNPhjRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdw Lm9yZ9Q8ycbljQVzg2YapbQX+IJ9H+1KSgSLXQtVtCsaJh8RFiEEerJu6BkdLdse 21ngfHu5zaU0+GMAAD3BAQCzmPr7q3Wf1BFKO95WBhe66Cg+lAnoJZKbgHBpUsIB 6wEAmGzOcL1zYXRBWpquzFKS2bNxuFdpbewkxkf0axvMnA8WIQSQO2vdjfwfK4J6 FaK9/0MioyzN/wAAoD8A/2a7+DrKEjL7fAt7UZnPYZOXPEOpYXxUE3ZGQbnsKrdw AP0ae/B4LYXd89UwHpQUBEJHbY113W8NRUfpM/EJUQRdBM4zBGlBP2kWCSsGAQQB 2kcPAQEHQNhqbQtL5hS4w0n9H0hHMiD84WY39ukclu1yCtBii/+xwsDFBBgWCgE3 BYJpQT9pBYkFpI+9CRC9/0MioyzN/0cUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5z ZXF1b2lhLXBncC5vcmd6G9Mcoj01NqY/TIgPlhe4L5OMUS/R4cIc2wWl2EmQTAKb IL6gBBkWCgBvBYJpQT9pCRAR5pEu+j17CUcUAAAAAAAeACBzYWx0QG5vdGF0aW9u cy5zZXF1b2lhLXBncC5vcmeJ47SGs3Kf1A20PTM/BFkWz2zd2dfaiVx8zKNbXoEZ 9BYhBK5UVQP/gdjl2RIb3RHmkS76PXsJAACjYgD/SfQ8WoCnBDnywpVknkW8FtHd akewRjkT17Q5gK1NEWUA/jOMj8chhN4R6jnnWEb0ClujkkVMBWP7h2N5HWGN4DoD FiEEkDtr3Y38HyuCehWivf9DIqMszf8AAC7NAQCNZWDduo/vseZGLDNOK9y+krFZ fDSGVBmvBHGiGjX6agD/SHcsROm0C/HXe+KxS/SAhHob2KdZ8FQ+dCtr3oY/egXO OARpQT9pEgorBgEEAZdVAQUBAQdA1dxpaBPWnqr8lhzucvP2F746IIdfYk+UVImg pe3GD1UDAQgHwsAGBBgWCgB4BYJpQT9pBYkFpI+9CRC9/0MioyzN/0cUAAAAAAAe ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeDJU2fBZAzwn32lcgdEyjB /LtqMEQ7Qv5HwfJFRlseCAKbDBYhBJA7a92N/B8rgnoVor3/QyKjLM3/AADjHQEA yvN5w7wxf50yriNlrHCwRzQ4MljfJawXfAfOthJlh8gBANycfude13YmUeFvwuyQ Vmac4mRS69ivzHMLDNXUsMwB =WPEh -----END PGP PUBLIC KEY BLOCK----- "; let body = Bytes::from(&body[..]); assert!(! is_encrypted(&body)); // broken message let body = b"-----BEGIN PGP MESSAGE----- wWwGFQRhclhA/Qg3CiL81NAswyOWkyk8jxIBB0AmlBkVEVbQst60C/1+mcmIxLbG W/g9mJDd0ral30VL0FfuKRR2Je2TRfizqMb4tL4EIRFqrfSBesb9WcniEfBekG3B jk6lh01Ej9QTGQ== -----END PGP MESSAGE----- "; let body = Bytes::from(&body[..]); assert!(! is_encrypted(&body)); } }