From e4bb5d674d9af940cd1d98d43163ffb58c782dbf Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Thu, 14 Sep 2023 11:24:35 -0700 Subject: [PATCH] add msg:dkim_verify() This method returns an array of AuthenticationResult reflecting the verification status. refs: https://github.com/KumoCorp/kumomta/issues/82 --- Cargo.lock | 1 + crates/dns-resolver/src/resolver.rs | 4 ++ crates/mailparsing/src/rfc5322_parser.rs | 5 +- crates/message/Cargo.toml | 1 + crates/message/src/message.rs | 71 +++++++++++++++++++++++- simple_policy.lua | 3 + 6 files changed, 82 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6380efc4..f561c6a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2904,6 +2904,7 @@ dependencies = [ "chrono-tz", "config", "data-loader", + "dns-resolver", "futures", "k9", "kumo-dkim", diff --git a/crates/dns-resolver/src/resolver.rs b/crates/dns-resolver/src/resolver.rs index bbfc166f..1637cce1 100644 --- a/crates/dns-resolver/src/resolver.rs +++ b/crates/dns-resolver/src/resolver.rs @@ -50,6 +50,10 @@ pub enum Resolver { } impl Resolver { + pub async fn resolve_txt(&self, name: N) -> anyhow::Result { + self.resolve(name, RecordType::TXT).await + } + pub async fn resolve( &self, name: N, diff --git a/crates/mailparsing/src/rfc5322_parser.rs b/crates/mailparsing/src/rfc5322_parser.rs index bbfda687..d63fa7d4 100644 --- a/crates/mailparsing/src/rfc5322_parser.rs +++ b/crates/mailparsing/src/rfc5322_parser.rs @@ -9,6 +9,7 @@ use nom::combinator::{all_consuming, map, opt, recognize}; use nom::error::context; use nom::multi::{many0, many1, separated_list1}; use nom::sequence::{delimited, preceded, separated_pair, terminated, tuple}; +use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::fmt::Debug; @@ -1333,7 +1334,7 @@ impl Parser { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AuthenticationResults { pub serv_id: String, pub version: Option, @@ -1389,7 +1390,7 @@ impl EncodeHeaderValue for AuthenticationResults { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AuthenticationResult { pub method: String, pub method_version: Option, diff --git a/crates/message/Cargo.toml b/crates/message/Cargo.toml index 2f1556ad..3d8580c5 100644 --- a/crates/message/Cargo.toml +++ b/crates/message/Cargo.toml @@ -14,6 +14,7 @@ config = {path="../config"} chrono = {version="0.4", default-features=false, features=["serde", "clock"]} chrono-tz = {version="0.8", features=["serde"]} data-loader = {path="../data-loader"} +dns-resolver = {path="../dns-resolver"} futures = "0.3" kumo-log-types = {path="../kumo-log-types"} lazy_static = "1.4" diff --git a/crates/message/src/message.rs b/crates/message/src/message.rs index c75bb7a0..5f2daf81 100644 --- a/crates/message/src/message.rs +++ b/crates/message/src/message.rs @@ -5,10 +5,12 @@ use crate::EnvelopeAddress; use anyhow::Context; use chrono::{DateTime, Utc}; use config::{any_err, from_lua_value}; +use dns_resolver::resolver::Resolver; +use futures::future::BoxFuture; use futures::FutureExt; use kumo_log_types::rfc3464::Report; use kumo_log_types::rfc5965::ARFReport; -use mailparsing::{Header, HeaderParseResult, MessageConformance, MimePart}; +use mailparsing::{AuthenticationResult, Header, HeaderParseResult, MessageConformance, MimePart}; use mlua::{LuaSerdeExt, UserData, UserDataMethods}; use prometheus::IntGauge; use serde::{Deserialize, Serialize}; @@ -554,6 +556,67 @@ impl Message { }) } + pub async fn dkim_verify(&self) -> anyhow::Result> { + let resolver = dns_resolver::get_resolver(); + let data = self.get_data(); + let bytes = mailparsing::SharedString::try_from(data.as_ref().as_ref())?; + + let parsed = mailparsing::Header::parse_headers(bytes.clone())?; + if parsed + .overall_conformance + .contains(MessageConformance::NON_CANONICAL_LINE_ENDINGS) + { + return Ok(vec![AuthenticationResult { + method: "dkim".to_string(), + method_version: None, + result: "permerror".to_string(), + reason: Some("message has non-canonical line endings".to_string()), + props: Default::default(), + }]); + } + let message = kumo_dkim::ParsedEmail::HeaderOnlyParse { bytes, parsed }; + + let from = message + .get_headers() + .from() + .map_err(any_err)? + .ok_or_else(|| anyhow::anyhow!("Missing or invalid From header"))? + .0; + if from.len() != 1 { + anyhow::bail!( + "From header must have a single sender, found {}", + from.len() + ); + } + let from_domain = &from[0].address.domain; + + struct ResolverAdapater { + resolver: Arc, + } + + impl kumo_dkim::dns::Lookup for ResolverAdapater { + fn lookup_txt<'a>( + &'a self, + name: &'a str, + ) -> BoxFuture<'a, Result, kumo_dkim::DKIMError>> { + Box::pin(async move { + match self.resolver.resolve_txt(name).await { + Ok(answer) => Ok(answer.as_txt()), + Err(err) => Err(kumo_dkim::DKIMError::KeyUnavailable(format!("{err}"))), + } + }) + } + } + + let results = kumo_dkim::verify_email_with_resolver( + from_domain, + &message, + &ResolverAdapater { resolver }, + ) + .await?; + Ok(results) + } + pub fn parse_rfc3464(&self) -> anyhow::Result> { let data = self.get_data(); Report::parse(&data) @@ -876,6 +939,12 @@ impl UserData for Message { methods.add_method("dkim_sign", move |_, this, signer: Signer| { Ok(this.dkim_sign(&signer).map_err(any_err)?) }); + + methods.add_async_method("dkim_verify", |lua, this, ()| async move { + let results = this.dkim_verify().await.map_err(any_err)?; + lua.to_value(&results) + }); + methods.add_method( "prepend_header", move |_, this, (name, value): (String, String)| { diff --git a/simple_policy.lua b/simple_policy.lua index 1993eb5c..3f67fb45 100644 --- a/simple_policy.lua +++ b/simple_policy.lua @@ -125,6 +125,9 @@ end) -- Called once the body has been received. -- For multi-recipient mail, this is called for each recipient. kumo.on('smtp_server_message_received', function(msg) + local verify = msg:dkim_verify() + print('dkim', kumo.json_encode(verify)) + local failed = msg:check_fix_conformance( -- check for and reject messages with these issues: 'MISSING_COLON_VALUE',