Refactor online certificate lookup

- Allow searching for fingerprints - needed for the updater mechanism.
This commit is contained in:
Malte Meiboom
2026-08-05 11:05:53 +02:00
parent 5cf7357230
commit 173c32ff5a
+84 -37
View File
@@ -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<Vec<Cert>> {
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<Vec<Cert>> {
let fpr = fpr.parse::<Fingerprint>()?;
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<Vec<Cert>> {
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<Result<Cert, anyhow::Error>>, email: String, policy: &StandardPolicy| {
let collect_certs = |certs: Vec<Result<Cert, anyhow::Error>>, query: OnlineQuery, policy: &StandardPolicy| {
let mut result = Vec::new();
let mut seen: HashSet<Fingerprint> = 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::<Vec<_>>());
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) {