Start milestone 3

- Add functionality to query online sources (Keyservers, etc) for
  certificates.
- Use introducers to determine which UserIDs should be fetched from
  online soources. Check certifications before importing certifiactes
  into the local cert store.
- start docker image for local end-to-end testing.
This commit is contained in:
Malte Meiboom
2026-01-23 12:57:26 +01:00
parent 4270bf4ceb
commit 30b1d29274
23 changed files with 685 additions and 10 deletions
+10
View File
@@ -14,6 +14,7 @@ pub struct HuskConfig {
pub connection: String,
pub sequoia_home: String,
logfile_config: Option<String>,
pub keyservers: Option<Vec<String>>,
}
impl HuskConfig {
@@ -43,6 +44,15 @@ impl HuskConfig {
},
}
}
pub fn get_keyservers(&self) -> Vec<String> {
if let Some(kss) = &self.keyservers {
kss.clone()
} else {
Vec::new()
}
}
}
pub type HuskConfigContainer = Arc<Mutex<HuskConfig>>;
+180 -4
View File
@@ -6,22 +6,27 @@
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};
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::introducer::{self, Introducer};
use crate::types::recipient::Recipient;
/// Returns the local trust root from the cert_store. If the trust root
@@ -61,12 +66,14 @@ pub fn get_local_trust_root(cert_store: &CertStore) -> anyhow::Result<Cert> {
}
/// Returns all certificates with an authenticated binding to user id 'email'.
/// 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_certificates<'hc>(context: &HuskContext<'hc>, email: &str)
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);
@@ -95,6 +102,175 @@ pub fn get_certificates<'hc>(context: &HuskContext<'hc>, email: &str)
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 {
if let Ok(c) = cert {
if let Ok(vc) = c.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(c.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.len() > 0 {
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> {
+1 -1
View File
@@ -143,7 +143,7 @@ impl Daemon {
if rcpt.is_none() {
Status::Tempfail
} else {
context.add_recipient(rcpt.unwrap()).unwrap();
context.add_recipient(rcpt.unwrap()).await;
Status::Continue
}
+2
View File
@@ -18,4 +18,6 @@ pub enum HuskError {
NoCertStore,
#[error("No encryption keys found")]
NoEncryptionKeys,
#[error("Error while looking up email {0}: {1}")]
KeyLookupError(String, String),
}
+4 -2
View File
@@ -21,6 +21,7 @@ pub struct HuskContext<'hc> {
pub cert_store: CertStore<'hc>,
pub local_trust_root: Cert,
pub introducers: Vec<Introducer>,
pub keyservers: Vec<String>,
pub mail: MailContext<'hc>,
}
@@ -43,6 +44,7 @@ impl<'hc> HuskContext<'hc> {
cert_store,
local_trust_root,
introducers,
keyservers: config.get_keyservers(),
mail: MailContext::new(),
})
}
@@ -55,9 +57,9 @@ impl<'hc> HuskContext<'hc> {
self.mail = MailContext::new();
}
pub fn add_recipient(&mut self, rcpt: String) -> anyhow::Result<()> {
pub async fn add_recipient(&mut self, rcpt: String) {
let certs = crypto::get_certificates(&self, rcpt.as_str());
let certs = crypto::get_certificates(&self, rcpt.as_str()).await;
self.mail.add_recipient(rcpt, certs)
}
+24
View File
@@ -29,5 +29,29 @@ impl Introducer {
false
}
}
pub fn is_introducing(&self, cert: &Cert) -> bool {
let policy = StandardPolicy::new();
let mut result = false;
let my_keyid = self.cert.keyid();
if let Ok(vc) = cert.with_policy(&policy, None) {
vc.userids()
.revoked(false)
.for_each(|u| {
u.certifications().for_each(|s| {
if s.signature_alive(None, None).is_ok() {
s.issuers().for_each(|i| {
if i == &my_keyid { result = true; }
});
}
})
});
}
result
}
}
+1 -3
View File
@@ -44,13 +44,11 @@ impl<'mc> MailContext<'mc> {
self.sender = Some(sender);
}
pub fn add_recipient(&mut self, rcpt: String, certs: Vec<Arc<LazyCert<'mc>>>) -> anyhow::Result<()> {
pub fn add_recipient(&mut self, rcpt: String, certs: Vec<Arc<LazyCert<'mc>>>) {
let mut recipient: Recipient = rcpt.into();
recipient.certs = certs;
self.recipients.push(recipient);
Ok(())
}
pub fn add_chunk(&mut self, chunk: Bytes) {