Move crypto.rs into common
- Refactor code and move crypto.rs into the common subdirectory. - Fixed comments
This commit is contained in:
@@ -0,0 +1,456 @@
|
||||
//! 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 tokio::task::JoinSet;
|
||||
use bytes::Bytes;
|
||||
use sequoia_net::{KeyServer, wkd, dane};
|
||||
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, StoreUpdate};
|
||||
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;
|
||||
|
||||
/// 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);
|
||||
if !paths.is_empty() {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
#[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));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user