mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 17:36:29 +03:00
Fix some things, move to new file src/authentication_results_handling.rs
This commit is contained in:
341
src/authentication_results_handling.rs
Normal file
341
src/authentication_results_handling.rs
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
//! TODO file comment.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use anyhow::{Context as _, Result};
|
||||||
|
use mailparse::MailHeaderMap;
|
||||||
|
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::headerdef::HeaderDef;
|
||||||
|
|
||||||
|
use crate::tools;
|
||||||
|
use crate::tools::EmailAddress;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) struct AuthenticationResults {
|
||||||
|
dkim_passed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) type AuthservId = String;
|
||||||
|
|
||||||
|
pub(crate) fn parse_authentication_results(
|
||||||
|
headers: &mailparse::headers::Headers<'_>,
|
||||||
|
from: &str,
|
||||||
|
) -> Result<HashMap<AuthservId, AuthenticationResults>> {
|
||||||
|
// TODO old comment:
|
||||||
|
// TODO this doesn't work for e.g. GMX which sells @gmx.de addresses, but uses gmx.net as its server
|
||||||
|
// Config::ConfiguredProvider doesn't work for e.g. Gmail which uses mx.google.com.
|
||||||
|
//
|
||||||
|
// We could self-send a message during configure and use the Authentication-Results header from there -
|
||||||
|
// this works for e.g. GMX, but not for Testrun and GMAIL.
|
||||||
|
// -> Alternatively, we could send a message to nonexistent@example.com and wait for the NDN. This works
|
||||||
|
// for Gmail, but the Testrun NDN doesn't contain such a header, and GMX returns an error directly
|
||||||
|
// while sending.
|
||||||
|
//
|
||||||
|
// We could save this info in the provider db, but this only works for these providers.
|
||||||
|
|
||||||
|
// let from = match from.first() {
|
||||||
|
// Some(f) => &f.addr,
|
||||||
|
// None => return Ok(HashMap::new()),
|
||||||
|
// }; // TODO
|
||||||
|
let sender_domain = crate::tools::EmailAddress::new(from)?.domain;
|
||||||
|
|
||||||
|
let mut header_map: HashMap<AuthservId, Vec<String>> = HashMap::new();
|
||||||
|
for header_value in headers.get_all_values(HeaderDef::AuthenticationResults.into()) {
|
||||||
|
// TODO there could be a comment [CFWS] before the self domain. Do we care? Probably not.
|
||||||
|
let authserv_id = header_value
|
||||||
|
.split_whitespace()
|
||||||
|
.next()
|
||||||
|
.context("Empty header")?; // TODO do we really want to return Err here if it's empty
|
||||||
|
header_map
|
||||||
|
.entry(authserv_id.to_string())
|
||||||
|
.or_default()
|
||||||
|
.push(header_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut authresults_map = HashMap::new();
|
||||||
|
for (authserv_id, headers) in header_map {
|
||||||
|
let dkim_passed = authresults_dkim_passed(&headers, &sender_domain)?;
|
||||||
|
authresults_map.insert(authserv_id, AuthenticationResults { dkim_passed });
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(authresults_map)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the Authentication-Results headers belonging to a specific authserv-id
|
||||||
|
/// and returns whether they say that DKIM passed.
|
||||||
|
/// TODO document better
|
||||||
|
/// TODO if there are multiple headers and one says `pass`, one says `fail`, `none`
|
||||||
|
/// or whatever, then we still interpret that as `pass` - is this a problem?
|
||||||
|
fn authresults_dkim_passed(headers: &[String], sender_domain: &str) -> Result<bool> {
|
||||||
|
for header_value in headers {
|
||||||
|
if let Some((_start, dkim_to_end)) = header_value.split_once("dkim=") {
|
||||||
|
let dkim_part = dkim_to_end
|
||||||
|
.split(';')
|
||||||
|
.next()
|
||||||
|
.context("what the hell TODO")?;
|
||||||
|
let dkim_parts: Vec<_> = dkim_part.split_whitespace().collect();
|
||||||
|
if let Some(&"pass") = dkim_parts.first() {
|
||||||
|
let header_d: &str = &format!("header.d={}", &sender_domain);
|
||||||
|
let header_i: &str = &format!("header.i=@{}", &sender_domain);
|
||||||
|
|
||||||
|
if dkim_parts.contains(&header_d) || dkim_parts.contains(&header_i) {
|
||||||
|
// We have found a `dkim=pass` header!
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO this is only half of the algorithm we thought of; we also wanted to save how sure we are
|
||||||
|
// about the authserv id. Like, a same-domain email is more trustworthy.
|
||||||
|
pub(crate) async fn update_authservid_candidates(
|
||||||
|
context: &Context,
|
||||||
|
authentication_results: &HashMap<AuthservId, AuthenticationResults>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut new_ids: HashSet<_> = authentication_results.keys().map(String::as_str).collect();
|
||||||
|
if new_ids.is_empty() {
|
||||||
|
// The incoming message doesn't contain any authentication results, maybe it's a
|
||||||
|
// self-sent or a mailer-daemon message
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let ids_config;
|
||||||
|
if let Some(ids_config_temp) = context
|
||||||
|
.get_config(crate::config::Config::AuthservIdCandidates)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
ids_config = ids_config_temp;
|
||||||
|
let old_ids: HashSet<_> = ids_config.split(' ').collect();
|
||||||
|
if !old_ids.is_empty() {
|
||||||
|
new_ids = old_ids.intersection(&new_ids).copied().collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If there were no AuthservIdCandidates previously, just start with
|
||||||
|
// the ones from the incoming email
|
||||||
|
|
||||||
|
let new_config = new_ids.into_iter().collect::<Vec<_>>().join(" ");
|
||||||
|
context
|
||||||
|
.set_config(
|
||||||
|
crate::config::Config::AuthservIdCandidates,
|
||||||
|
Some(&new_config),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// We disallow changes to the autocrypt key if DKIM failed, but worked in the past,
|
||||||
|
/// because we then assume that the From header is forged.
|
||||||
|
pub(crate) async fn should_allow_keychange(
|
||||||
|
context: &Context,
|
||||||
|
authentication_results: &HashMap<String, AuthenticationResults>,
|
||||||
|
from: &str,
|
||||||
|
) -> Result<bool> {
|
||||||
|
// TODO code duplication with update_authservid_candidates()
|
||||||
|
let mut dkim_passed = true; // TODO what do we want to do if there are multiple or no authservid candidates?
|
||||||
|
if let Some(ids_config) = context
|
||||||
|
.get_config(crate::config::Config::AuthservIdCandidates)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
let ids = ids_config.split(' ').filter(|s| !s.is_empty());
|
||||||
|
dbg!(&ids_config);
|
||||||
|
if let Some(authserv_id) = tools::single_value(ids) {
|
||||||
|
// dbg!(&authentication_results, &ids_config);
|
||||||
|
// TODO unwrap
|
||||||
|
dkim_passed = authentication_results.get(authserv_id).unwrap().dkim_passed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sending_domain = from.parse::<EmailAddress>().unwrap().domain; // TODO unwrap
|
||||||
|
let dkim_known_to_work = context
|
||||||
|
.sql
|
||||||
|
.query_get_value(
|
||||||
|
"SELECT correct_dkim FROM sending_domains WHERE domain=?;",
|
||||||
|
paramsv![sending_domain],
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !dkim_known_to_work && dkim_passed {
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.execute(
|
||||||
|
"UPDATE sending_domains SET correct_dkim=1 WHERE domain=?;",
|
||||||
|
paramsv![sending_domain],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(dkim_passed || !dkim_known_to_work)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use tokio::fs;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::headerdef::HeaderDefMap;
|
||||||
|
use crate::test_utils::*;
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_parse_authentication_results() -> Result<()> {
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
t.configure_addr("alice@gmx.net").await;
|
||||||
|
let bytes = b"From: info@slack.com
|
||||||
|
Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
|
||||||
|
Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com";
|
||||||
|
let mail = mailparse::parse_mail(bytes)?;
|
||||||
|
let actual = parse_authentication_results(&mail.get_headers(), "info@slack.com").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
[(
|
||||||
|
"gmx.net".to_string(),
|
||||||
|
AuthenticationResults { dkim_passed: true }
|
||||||
|
)]
|
||||||
|
.into()
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO test that foreign Auth-Res headers are ignored
|
||||||
|
|
||||||
|
// check_parse_authentication_results_combination(
|
||||||
|
// "alice@testrun.org",
|
||||||
|
// // TODO actually the address is alice@gmx.de, but then it doesn't work because `header.d=gmx.net`:
|
||||||
|
// b"From: alice@gmx.net
|
||||||
|
// Authentication-Results: testrun.org;
|
||||||
|
// dkim=pass header.d=gmx.net header.s=badeba3b8450 header.b=Gug6p4zD;
|
||||||
|
// dmarc=pass (policy=none) header.from=gmx.de;
|
||||||
|
// spf=pass (testrun.org: domain of alice@gmx.de designates 212.227.17.21 as permitted sender) smtp.mailfrom=alice@gmx.de",
|
||||||
|
// AuthenticationResults::Passed,
|
||||||
|
// )
|
||||||
|
// .await;
|
||||||
|
|
||||||
|
// check_parse_authentication_results_combination(
|
||||||
|
// "alice@testrun.org",
|
||||||
|
// br#"From: hocuri@testrun.org
|
||||||
|
// Authentication-Results: box.hispanilandia.net; dmarc=none (p=none dis=none) header.from=nauta.cu
|
||||||
|
// Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@nauta.cu
|
||||||
|
// Authentication-Results: testrun.org;
|
||||||
|
// dkim=fail ("body hash did not verify") header.d=nauta.cu header.s=nauta header.b=YrWhU6qk;
|
||||||
|
// dmarc=none;
|
||||||
|
// spf=pass (testrun.org: domain of "test1-bounces+hocuri=testrun.org@hispanilandia.net" designates 51.15.127.36 as permitted sender) smtp.mailfrom="test1-bounces+hocuri=testrun.org@hispanilandia.net"
|
||||||
|
// "#,
|
||||||
|
// AuthenticationResults::Failed,
|
||||||
|
// )
|
||||||
|
// .await;
|
||||||
|
|
||||||
|
// check_parse_authentication_results_combination(
|
||||||
|
|
||||||
|
// // TODO fails because mx.google.com, not google.com
|
||||||
|
// "alice@gmail.com",
|
||||||
|
// br#"From: not-so-fake@hispanilandia.net
|
||||||
|
// Authentication-Results: mx.google.com;
|
||||||
|
// dkim=pass header.i=@hispanilandia.net header.s=mail header.b="Ih5Sz2/P";
|
||||||
|
// spf=pass (google.com: domain of not-so-fake@hispanilandia.net designates 51.15.127.36 as permitted sender) smtp.mailfrom=not-so-fake@hispanilandia.net;
|
||||||
|
// dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=hispanilandia.net"#,
|
||||||
|
// AuthenticationResults::Passed,
|
||||||
|
// )
|
||||||
|
// .await;
|
||||||
|
|
||||||
|
// check_parse_authentication_results_combination(
|
||||||
|
// "alice@nauta.cu",
|
||||||
|
// br#"From: adb <adbenitez@disroot.org>
|
||||||
|
// Authentication-Results: box.hispanilandia.net;
|
||||||
|
// dkim=fail reason="signature verification failed" (2048-bit key; secure) header.d=disroot.org header.i=@disroot.org header.b="kqh3WUKq";
|
||||||
|
// dkim-atps=neutral
|
||||||
|
// Authentication-Results: box.hispanilandia.net; dmarc=pass (p=quarantine dis=none) header.from=disroot.org
|
||||||
|
// Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@disroot.org"#,
|
||||||
|
// AuthenticationResults::Passed,
|
||||||
|
// )
|
||||||
|
// .await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_realworld_authentication_results() -> Result<()> {
|
||||||
|
let mut dir = fs::read_dir("test-data/message/dkimchecks-2022-09-28/")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
while let Some(entry) = dir.next_entry().await.unwrap() {
|
||||||
|
let self_addr = entry.file_name().into_string().unwrap();
|
||||||
|
let mut dir = fs::read_dir(entry.path()).await.unwrap();
|
||||||
|
|
||||||
|
let t = TestContext::new().await;
|
||||||
|
t.configure_addr(&self_addr).await;
|
||||||
|
|
||||||
|
while let Some(entry) = dir.next_entry().await.unwrap() {
|
||||||
|
let mut file = fs::File::open(entry.path()).await?;
|
||||||
|
println!("{:?}", entry.path());
|
||||||
|
bytes.clear();
|
||||||
|
file.read_to_end(&mut bytes).await.unwrap();
|
||||||
|
if bytes.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mail = mailparse::parse_mail(&bytes)?;
|
||||||
|
// TODO code duplication with create_decryption_info()
|
||||||
|
let from = mail
|
||||||
|
.headers
|
||||||
|
.get_header(HeaderDef::From_)
|
||||||
|
.and_then(|from_addr| mailparse::addrparse_header(from_addr).ok())
|
||||||
|
.and_then(|from| from.extract_single_info())
|
||||||
|
.map(|from| from.addr)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// TODO code duplication with create_decryption_info()
|
||||||
|
let authentication_results =
|
||||||
|
parse_authentication_results(&mail.get_headers(), &from)?;
|
||||||
|
update_authservid_candidates(&t, &authentication_results).await?;
|
||||||
|
let allow_keychange =
|
||||||
|
should_allow_keychange(&t, &authentication_results, &from).await?;
|
||||||
|
|
||||||
|
assert!(allow_keychange);
|
||||||
|
|
||||||
|
// check_parse_authentication_results_combination(
|
||||||
|
// &self_addr,
|
||||||
|
// &bytes,
|
||||||
|
// AuthenticationResults::Passed,
|
||||||
|
// )
|
||||||
|
// .await;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::mem::forget(t) // TODO dbg
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// async fn check_parse_authentication_results_combination(
|
||||||
|
// self_addr: &str,
|
||||||
|
// header_bytes: &[u8],
|
||||||
|
// expected_result: AuthenticationResults,
|
||||||
|
// ) {
|
||||||
|
// let t = TestContext::new().await;
|
||||||
|
// t.set_primary_self_addr(self_addr).await.unwrap();
|
||||||
|
// let mail = mailparse::parse_mail(body)?;
|
||||||
|
|
||||||
|
// let actual = parse_authentication_results(&t, &mail.get_headers(), &from)?;
|
||||||
|
// //assert_eq!(message.authentication_results, expected_result);
|
||||||
|
// if message.authentication_results != expected_result {
|
||||||
|
// eprintln!(
|
||||||
|
// "EXPECTED {expected_result:?}, GOT {:?}, SELF {}, FROM {:?}",
|
||||||
|
// message.authentication_results,
|
||||||
|
// self_addr,
|
||||||
|
// message.from.first().map(|i| &i.addr),
|
||||||
|
// )
|
||||||
|
// } else {
|
||||||
|
// eprintln!(
|
||||||
|
// "CORRECT {:?}, SELF {}, FROM {:?}",
|
||||||
|
// message.authentication_results,
|
||||||
|
// self_addr,
|
||||||
|
// message.from.first().map(|i| &i.addr),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -544,6 +544,12 @@ impl Context {
|
|||||||
.await?
|
.await?
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
res.insert(
|
||||||
|
"authserv_id_candidates",
|
||||||
|
self.get_config(Config::AuthservIdCandidates)
|
||||||
|
.await?
|
||||||
|
.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
|
||||||
let elapsed = self.creation_time.elapsed();
|
let elapsed = self.creation_time.elapsed();
|
||||||
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
|
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
|
||||||
|
|||||||
150
src/decrypt.rs
150
src/decrypt.rs
@@ -1,13 +1,15 @@
|
|||||||
//! End-to-end decryption support.
|
//! End-to-end decryption support.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use mailparse::MailHeaderMap;
|
|
||||||
use mailparse::ParsedMail;
|
use mailparse::ParsedMail;
|
||||||
|
|
||||||
use crate::aheader::Aheader;
|
use crate::aheader::Aheader;
|
||||||
|
use crate::authentication_results_handling::parse_authentication_results;
|
||||||
|
use crate::authentication_results_handling::should_allow_keychange;
|
||||||
|
use crate::authentication_results_handling::update_authservid_candidates;
|
||||||
use crate::contact::addr_cmp;
|
use crate::contact::addr_cmp;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::headerdef::HeaderDef;
|
use crate::headerdef::HeaderDef;
|
||||||
@@ -17,7 +19,6 @@ use crate::keyring::Keyring;
|
|||||||
use crate::log::LogExt;
|
use crate::log::LogExt;
|
||||||
use crate::peerstate::Peerstate;
|
use crate::peerstate::Peerstate;
|
||||||
use crate::pgp;
|
use crate::pgp;
|
||||||
use crate::tools;
|
|
||||||
|
|
||||||
/// Tries to decrypt a message, but only if it is structured as an
|
/// Tries to decrypt a message, but only if it is structured as an
|
||||||
/// Autocrypt message.
|
/// Autocrypt message.
|
||||||
@@ -58,126 +59,6 @@ pub async fn try_decrypt(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO move somewhere else
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct AuthenticationResults {
|
|
||||||
dkim_passed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthservId = String;
|
|
||||||
|
|
||||||
fn parse_authentication_results(
|
|
||||||
context: &Context,
|
|
||||||
headers: &mailparse::headers::Headers<'_>,
|
|
||||||
from: &str,
|
|
||||||
) -> Result<HashMap<AuthservId, AuthenticationResults>> {
|
|
||||||
// TODO old comment:
|
|
||||||
// TODO this doesn't work for e.g. GMX which sells @gmx.de addresses, but uses gmx.net as its server
|
|
||||||
// Config::ConfiguredProvider doesn't work for e.g. Gmail which uses mx.google.com.
|
|
||||||
//
|
|
||||||
// We could self-send a message during configure and use the Authentication-Results header from there -
|
|
||||||
// this works for e.g. GMX, but not for Testrun and GMAIL.
|
|
||||||
// -> Alternatively, we could send a message to nonexistent@example.com and wait for the NDN. This works
|
|
||||||
// for Gmail, but the Testrun NDN doesn't contain such a header, and GMX returns an error directly
|
|
||||||
// while sending.
|
|
||||||
//
|
|
||||||
// We could save this info in the provider db, but this only works for these providers.
|
|
||||||
|
|
||||||
// let from = match from.first() {
|
|
||||||
// Some(f) => &f.addr,
|
|
||||||
// None => return Ok(HashMap::new()),
|
|
||||||
// }; // TODO
|
|
||||||
let sender_domain = crate::tools::EmailAddress::new(from)?.domain;
|
|
||||||
|
|
||||||
let mut header_map: HashMap<AuthservId, Vec<String>> = HashMap::new();
|
|
||||||
for header_value in headers.get_all_values(HeaderDef::AuthenticationResults.into()) {
|
|
||||||
// TODO there could be a comment [CFWS] before the self domain. Do we care? Probably not.
|
|
||||||
let authserv_id = header_value
|
|
||||||
.split_whitespace()
|
|
||||||
.next()
|
|
||||||
.context("Empty header")?; // TODO do we really want to return Err here if it's empty
|
|
||||||
header_map
|
|
||||||
.entry(authserv_id.to_string())
|
|
||||||
.or_default()
|
|
||||||
.push(header_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut authresults_map = HashMap::new();
|
|
||||||
for (authserv_id, headers) in header_map {
|
|
||||||
let dkim_passed = authresults_dkim_passed(&headers, &sender_domain)?;
|
|
||||||
authresults_map.insert(authserv_id, AuthenticationResults { dkim_passed });
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(authresults_map)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses the Authentication-Results headers belonging to a specific authserv-id
|
|
||||||
/// and returns whether they say that DKIM passed.
|
|
||||||
/// TODO document better
|
|
||||||
/// TODO if there are multiple headers and one says `pass`, one says `fail`, `none`
|
|
||||||
/// or whatever, then we still interpret that as `pass` - is this a problem?
|
|
||||||
fn authresults_dkim_passed(headers: &[String], sender_domain: &str) -> Result<bool> {
|
|
||||||
for header_value in headers {
|
|
||||||
if let Some((_start, dkim_to_end)) = header_value.split_once("dkim=") {
|
|
||||||
let dkim_part = dkim_to_end
|
|
||||||
.split(';')
|
|
||||||
.next()
|
|
||||||
.context("what the hell TODO")?;
|
|
||||||
let dkim_parts: Vec<_> = dkim_part.split_whitespace().collect();
|
|
||||||
if let Some(&"pass") = dkim_parts.first() {
|
|
||||||
let header_d: &str = &format!("header.d={}", &sender_domain);
|
|
||||||
let header_i: &str = &format!("header.i=@{}", &sender_domain);
|
|
||||||
|
|
||||||
if dkim_parts.contains(&header_d) || dkim_parts.contains(&header_i) {
|
|
||||||
// We have found a `dkim=pass` header!
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO this is only half of the algorithm we thought of; we also wanted to save how sure we are
|
|
||||||
// about the authserv id. Like, a same-domain email is more trustworthy.
|
|
||||||
async fn update_authservid_candidates(
|
|
||||||
context: &Context,
|
|
||||||
authentication_results: &HashMap<AuthservId, AuthenticationResults>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut new_ids: HashSet<_> = authentication_results.keys().map(String::as_str).collect();
|
|
||||||
if new_ids.is_empty() {
|
|
||||||
// The incoming message doesn't contain any authentication results, maybe it's a
|
|
||||||
// self-sent or a mailer-daemon message
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let ids_config;
|
|
||||||
if let Some(ids_config_temp) = context
|
|
||||||
.get_config(crate::config::Config::AuthservIdCandidates)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
ids_config = ids_config_temp;
|
|
||||||
let old_ids: HashSet<_> = ids_config.split(' ').collect();
|
|
||||||
if !old_ids.is_empty() {
|
|
||||||
new_ids = old_ids.intersection(&new_ids).copied().collect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If there were no AuthservIdCandidates previously, just start with
|
|
||||||
// the ones from the incoming email
|
|
||||||
|
|
||||||
let new_config = new_ids.into_iter().collect::<Vec<_>>().join(" ");
|
|
||||||
context
|
|
||||||
.set_config(
|
|
||||||
crate::config::Config::AuthservIdCandidates,
|
|
||||||
Some(&new_config),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_decryption_info(
|
pub async fn create_decryption_info(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
mail: &ParsedMail<'_>,
|
mail: &ParsedMail<'_>,
|
||||||
@@ -195,33 +76,16 @@ pub async fn create_decryption_info(
|
|||||||
.ok_or_log_msg(context, "Failed to parse Autocrypt header")
|
.ok_or_log_msg(context, "Failed to parse Autocrypt header")
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
let authentication_results = parse_authentication_results(context, &mail.get_headers(), &from)?;
|
let authentication_results = parse_authentication_results(&mail.get_headers(), &from)?;
|
||||||
update_authservid_candidates(context, &authentication_results).await?;
|
update_authservid_candidates(context, &authentication_results).await?;
|
||||||
// TODO code duplication with update_authservid_candidates()
|
let allow_keychange = should_allow_keychange(context, &authentication_results, &from).await?;
|
||||||
// TODO too much low-level code
|
|
||||||
let mut dkim_passed = true; // TODO what do we want to do if there are multiple or no authservid candidates?
|
|
||||||
if let Some(ids_config) = context
|
|
||||||
.get_config(crate::config::Config::AuthservIdCandidates)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
let ids: HashSet<_> = ids_config.split(' ').collect();
|
|
||||||
if let Some(authserv_id) = tools::single_value(ids) {
|
|
||||||
// TODO unwrap
|
|
||||||
dkim_passed = authentication_results.get(authserv_id).unwrap().dkim_passed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO old comment Allow changes to the autocrypt key if DKIM passed.
|
|
||||||
// If DKIM failed, we assume that the From address may have been forged
|
|
||||||
// and therefore we prohibit changes to the autocrypt key.
|
|
||||||
|
|
||||||
let peerstate = get_autocrypt_peerstate(
|
let peerstate = get_autocrypt_peerstate(
|
||||||
context,
|
context,
|
||||||
&from,
|
&from,
|
||||||
autocrypt_header.as_ref(),
|
autocrypt_header.as_ref(),
|
||||||
message_time,
|
message_time,
|
||||||
true, // TODO key changes should not be allowed if the sending domain sent DKIM-valid emails
|
allow_keychange,
|
||||||
// until now, but this one is DKIM-invalid.
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ mod update_helper;
|
|||||||
pub mod webxdc;
|
pub mod webxdc;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod dehtml;
|
mod dehtml;
|
||||||
|
mod authentication_results_handling;
|
||||||
mod color;
|
mod color;
|
||||||
pub mod html;
|
pub mod html;
|
||||||
pub mod plaintext;
|
pub mod plaintext;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::pin::Pin;
|
|||||||
use anyhow::{bail, Context as _, Result};
|
use anyhow::{bail, Context as _, Result};
|
||||||
use deltachat_derive::{FromSql, ToSql};
|
use deltachat_derive::{FromSql, ToSql};
|
||||||
use lettre_email::mime::{self, Mime};
|
use lettre_email::mime::{self, Mime};
|
||||||
use mailparse::headers::Headers;
|
|
||||||
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
|
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ use crate::peerstate::Peerstate;
|
|||||||
use crate::simplify::{simplify, SimplifiedText};
|
use crate::simplify::{simplify, SimplifiedText};
|
||||||
use crate::stock_str;
|
use crate::stock_str;
|
||||||
use crate::sync::SyncItems;
|
use crate::sync::SyncItems;
|
||||||
use crate::tools::{get_filemeta, parse_receive_headers, truncate_by_lines, EmailAddress};
|
use crate::tools::{get_filemeta, parse_receive_headers, truncate_by_lines};
|
||||||
|
|
||||||
/// A parsed MIME message.
|
/// A parsed MIME message.
|
||||||
///
|
///
|
||||||
@@ -3333,124 +3333,4 @@ Message.
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_parse_authentication_results() -> Result<()> {
|
|
||||||
// TODO test that foreign Auth-Res headers are ignored
|
|
||||||
check_parse_authentication_results_combination(
|
|
||||||
"alice@gmx.net",
|
|
||||||
b"From: info@slack.com
|
|
||||||
Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
|
|
||||||
Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com",
|
|
||||||
AuthenticationResults::Passed,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
check_parse_authentication_results_combination(
|
|
||||||
"alice@testrun.org",
|
|
||||||
// TODO actually the address is alice@gmx.de, but then it doesn't work because `header.d=gmx.net`:
|
|
||||||
b"From: alice@gmx.net
|
|
||||||
Authentication-Results: testrun.org;
|
|
||||||
dkim=pass header.d=gmx.net header.s=badeba3b8450 header.b=Gug6p4zD;
|
|
||||||
dmarc=pass (policy=none) header.from=gmx.de;
|
|
||||||
spf=pass (testrun.org: domain of alice@gmx.de designates 212.227.17.21 as permitted sender) smtp.mailfrom=alice@gmx.de",
|
|
||||||
AuthenticationResults::Passed,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
check_parse_authentication_results_combination(
|
|
||||||
"alice@testrun.org",
|
|
||||||
br#"From: hocuri@testrun.org
|
|
||||||
Authentication-Results: box.hispanilandia.net; dmarc=none (p=none dis=none) header.from=nauta.cu
|
|
||||||
Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@nauta.cu
|
|
||||||
Authentication-Results: testrun.org;
|
|
||||||
dkim=fail ("body hash did not verify") header.d=nauta.cu header.s=nauta header.b=YrWhU6qk;
|
|
||||||
dmarc=none;
|
|
||||||
spf=pass (testrun.org: domain of "test1-bounces+hocuri=testrun.org@hispanilandia.net" designates 51.15.127.36 as permitted sender) smtp.mailfrom="test1-bounces+hocuri=testrun.org@hispanilandia.net"
|
|
||||||
"#,
|
|
||||||
AuthenticationResults::Failed,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
check_parse_authentication_results_combination(
|
|
||||||
|
|
||||||
// TODO fails because mx.google.com, not google.com
|
|
||||||
"alice@gmail.com",
|
|
||||||
br#"From: not-so-fake@hispanilandia.net
|
|
||||||
Authentication-Results: mx.google.com;
|
|
||||||
dkim=pass header.i=@hispanilandia.net header.s=mail header.b="Ih5Sz2/P";
|
|
||||||
spf=pass (google.com: domain of not-so-fake@hispanilandia.net designates 51.15.127.36 as permitted sender) smtp.mailfrom=not-so-fake@hispanilandia.net;
|
|
||||||
dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=hispanilandia.net"#,
|
|
||||||
AuthenticationResults::Passed,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
check_parse_authentication_results_combination(
|
|
||||||
"alice@nauta.cu",
|
|
||||||
br#"From: adb <adbenitez@disroot.org>
|
|
||||||
Authentication-Results: box.hispanilandia.net;
|
|
||||||
dkim=fail reason="signature verification failed" (2048-bit key; secure) header.d=disroot.org header.i=@disroot.org header.b="kqh3WUKq";
|
|
||||||
dkim-atps=neutral
|
|
||||||
Authentication-Results: box.hispanilandia.net; dmarc=pass (p=quarantine dis=none) header.from=disroot.org
|
|
||||||
Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@disroot.org"#,
|
|
||||||
AuthenticationResults::Passed,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_realworld_authentication_results() -> Result<()> {
|
|
||||||
let mut dir = fs::read_dir("test-data/message/dkimchecks-2022-09-28/")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let mut bytes = Vec::new();
|
|
||||||
while let Some(entry) = dir.next_entry().await.unwrap() {
|
|
||||||
let self_addr = entry.file_name().into_string().unwrap();
|
|
||||||
let mut dir = fs::read_dir(entry.path()).await.unwrap();
|
|
||||||
while let Some(entry) = dir.next_entry().await.unwrap() {
|
|
||||||
let mut file = fs::File::open(entry.path()).await?;
|
|
||||||
println!("{:?}", entry.path());
|
|
||||||
bytes.clear();
|
|
||||||
file.read_to_end(&mut bytes).await.unwrap();
|
|
||||||
if bytes.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
check_parse_authentication_results_combination(
|
|
||||||
&self_addr,
|
|
||||||
&bytes,
|
|
||||||
AuthenticationResults::Passed,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn check_parse_authentication_results_combination(
|
|
||||||
self_addr: &str,
|
|
||||||
header_bytes: &[u8],
|
|
||||||
expected_result: AuthenticationResults,
|
|
||||||
) {
|
|
||||||
let t = TestContext::new().await;
|
|
||||||
t.set_primary_self_addr(self_addr).await.unwrap();
|
|
||||||
let message = MimeMessage::from_bytes(&t, header_bytes).await.unwrap();
|
|
||||||
//assert_eq!(message.authentication_results, expected_result);
|
|
||||||
if message.authentication_results != expected_result {
|
|
||||||
eprintln!(
|
|
||||||
"EXPECTED {expected_result:?}, GOT {:?}, SELF {}, FROM {:?}",
|
|
||||||
message.authentication_results,
|
|
||||||
self_addr,
|
|
||||||
message.from.first().map(|i| &i.addr),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
eprintln!(
|
|
||||||
"CORRECT {:?}, SELF {}, FROM {:?}",
|
|
||||||
message.authentication_results,
|
|
||||||
self_addr,
|
|
||||||
message.from.first().map(|i| &i.addr),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -599,10 +599,7 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
|
|||||||
if dbversion < 92 {
|
if dbversion < 92 {
|
||||||
info!(context, "[migration] v92");
|
info!(context, "[migration] v92");
|
||||||
sql.execute_migration(
|
sql.execute_migration(
|
||||||
// TODO Is this really the database scheme we want?
|
"CREATE TABLE sending_domains(domain TEXT PRIMARY KEY, correct_dkim INTEGER DEFAULT 0);",
|
||||||
// Would be possible to save the timestamp here until when it was correct (change it as soon as it becomes incorrect).
|
|
||||||
// Then if this is old enough, accept a deviating key again
|
|
||||||
"ALTER TABLE acpeerstates ADD COLUMN dkim_status INTEGER DEFAULT 0;",
|
|
||||||
92,
|
92,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
Reference in New Issue
Block a user