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:
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user