From 173c32ff5abc655a586547c7b93b8cbd986cab97 Mon Sep 17 00:00:00 2001 From: Malte Meiboom Date: Wed, 5 Aug 2026 11:05:53 +0200 Subject: [PATCH] Refactor online certificate lookup - Allow searching for fingerprints - needed for the updater mechanism. --- src/common/crypto.rs | 121 ++++++++++++++++++++++++++++++------------- 1 file changed, 84 insertions(+), 37 deletions(-) diff --git a/src/common/crypto.rs b/src/common/crypto.rs index 12db06c..ef40920 100644 --- a/src/common/crypto.rs +++ b/src/common/crypto.rs @@ -96,12 +96,33 @@ 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, +#[derive(Debug, Clone)] +pub enum OnlineQuery { + Fingerprint(Fingerprint), + Email(String), +} + +/// Try to fetch certificates for email from online sources (keyservers, wkd, /// dane). -pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str) +pub async fn lookup_email<'hc>(context: &HuskContext<'hc>, email: &str) + -> anyhow::Result> { + query_online_sources(context, OnlineQuery::Email(email.into())).await +} + +/// Try to fetch certificates for fingerprint from keyservers. wkd +/// and dane can only be queried for email addresses. +pub async fn lookup_fingerprint<'hc>(context: &HuskContext<'hc>, fpr: &str) + -> anyhow::Result> { + let fpr = fpr.parse::()?; + query_online_sources(context, OnlineQuery::Fingerprint(fpr)).await +} + +/// Try to fetch certificates for query from online sources (keyservers, wkd, +/// dane). +async fn query_online_sources<'hc>(context: &HuskContext<'hc>, query: OnlineQuery) -> anyhow::Result> { - log::debug!("remote lookup for {}", email); + log::debug!("remote lookup for {:?}", query); let mut jobs = JoinSet::new(); let http_client = sequoia_net::reqwest::Client::builder() @@ -111,25 +132,32 @@ pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str) .build()?; // local function to check certificates for usability - let collect_certs = |certs: Vec>, email: String, policy: &StandardPolicy| { + let collect_certs = |certs: Vec>, query: OnlineQuery, policy: &StandardPolicy| { let mut result = Vec::new(); let mut seen: HashSet = HashSet::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; + let mut found = false; + match query.clone() { + OnlineQuery::Email(email) => { + // check if the returned certificate contains a userid with the + // email address in question. + for userid in vc.userids() { + if let Ok(Some(u)) = userid.userid().email() { + if u == email { + found = true; + } + } } + }, + OnlineQuery::Fingerprint(_) => { + found = true; } } - if userid_found { + if found { // check if the certificate can be used (for encryption). vc.keys() .supported() @@ -156,43 +184,62 @@ pub async fn lookup_certificates<'hc>(context: &HuskContext<'hc>, email: &str) if let Ok(ks) = KeyServer::with_client(keyserver.as_str(), http_client.clone()) { - let e = email.to_string(); let p = StandardPolicy::new(); + let query = query.clone(); 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); + let q = query.clone(); + match query { + OnlineQuery::Email(e) => { + if let Ok(certs) = ks.search(e.as_str()).await { + partial_result = collect_certs(certs, q, &p); + } + partial_result + }, + OnlineQuery::Fingerprint(f) => { + if let Ok(certs) = ks.get(f).await { + partial_result = collect_certs(certs, q, &p); + } + partial_result + }, } - partial_result }); } } - // WKD - let e = email.to_string(); - let p = StandardPolicy::new(); - jobs.spawn(async move { - let mut partial_result = Vec::new(); + // WKD and DANE need an email address + if let OnlineQuery::Email(_) = query { + let p = StandardPolicy::new(); + let q = query.clone(); - if let Ok(certs) = wkd::get(&http_client.clone(), e.as_str()).await { - partial_result = collect_certs(certs, e, &p); - } - partial_result - }); + // WKD + jobs.spawn(async move { + let mut partial_result = Vec::new(); - // DANE - let e = email.to_string(); - let p = StandardPolicy::new(); - jobs.spawn(async move { - let mut partial_result = Vec::new(); + if let OnlineQuery::Email(e) = q.clone() { + if let Ok(certs) = wkd::get(&http_client.clone(), e.as_str()).await { + partial_result = collect_certs(certs, q, &p); + } + } + partial_result + }); - if let Ok(certs) = dane::get(e.as_str()).await { - partial_result = collect_certs(certs, e, &p); - } - partial_result - }); + // DANE + let p = StandardPolicy::new(); + let q = query.clone(); + jobs.spawn(async move { + let mut partial_result = Vec::new(); + + if let OnlineQuery::Email(e) = q { + if let Ok(certs) = dane::get(e.as_str()).await { + partial_result = collect_certs(certs, query, &p); + } + } + partial_result + }); + } // join the partial results. As the same certificate might be returned by // several sources, dedup and merge the certificates. @@ -240,7 +287,7 @@ pub async fn get_certificates<'hc>(context: &HuskContext<'hc>, email: &str) introducer.cert.self_signed_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 { + match lookup_email(context, email).await { Ok(certs) => { for cert in certs { if introducer.is_introducing(&cert) {