Files
husk-milter/src/common/crypto.rs
T
Malte Meiboom 4ddc3db6c0 Add creating an introducer from a files
- Introducers can now be created by specifying a file. The certificate
  gets imported and declared as an introducer for the passed domains.
- Add some checks, so that the imported certificate can actually be used
  as an introducer (certificate is alive, not revoked, has certification
  capabilities)
2026-05-13 11:49:40 +02:00

569 lines
19 KiB
Rust

//! 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<Cert> {
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<Arc<LazyCert<'hc>>> {
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<Vec<Cert>> {
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<Result<Cert, anyhow::Error>>, 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<Fingerprint, Cert> = 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<Arc<LazyCert<'hc>>> {
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::<Vec<_>>());
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<Introducer> {
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<Recipient>)
-> anyhow::Result<Bytes> {
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<Vec<String>>)
-> 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));
}
}