Files
husk-milter/src/crypto.rs
T
Malte Meiboom eeda63f5ab Add encryption
- actually encrypt the body of a mail
2025-12-17 11:11:28 +01:00

283 lines
9.5 KiB
Rust

//
// Husk milter
//
// cryptographic functions
//
use std::io::Write;
use std::sync::Arc;
use anyhow;
use bytes::Bytes;
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_cert_store::{Store, CertStore, LazyCert};
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;
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)) => {
match Cert::from_bytes(cert.as_bytes()) {
Ok(c) => Some(c),
Err(_) => None,
}
},
Some(Err(_))
| None => None
}
},
Err(_) => None,
}
});
match root {
Some(r) => Ok(r),
None => Err(HuskError::NoLocalTrustRoot.into())
}
}
pub fn get_certificates<'hc>(context: &HuskContext<'hc>, email: &str)
-> Vec<Arc<LazyCert<'hc>>> {
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);
if paths.len() > 0 {
log::debug!("{} authenticated!!!", email);
for (path, _) in paths.iter() {
if let Ok(cert) = cert_store.lookup_by_cert_fpr(&path.target().fingerprint()) {
result.push(cert);
}
}
}
}
}
result
}
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
}
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
}
pub fn encrypt(context: &HuskContext<'_>, body: &Bytes, recipients: &Vec<Recipient>)
-> anyhow::Result<Bytes> {
let mut keys = Vec::new();
// XXX: handle the case where a certificate misses an encryption subkey
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.len() > 0 {
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())
}
}
#[cfg(test)]
pub mod tests {
use bytes::Bytes;
#[test]
pub fn test_is_encrypted() {
use crate::crypto::is_encrypted;
// 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));
}
}