add msg:dkim_verify()

This method returns an array of AuthenticationResult reflecting
the verification status.

refs: https://github.com/KumoCorp/kumomta/issues/82
This commit is contained in:
Wez Furlong
2023-09-14 11:24:35 -07:00
parent a13cb785ae
commit e4bb5d674d
6 changed files with 82 additions and 3 deletions
Generated
+1
View File
@@ -2904,6 +2904,7 @@ dependencies = [
"chrono-tz",
"config",
"data-loader",
"dns-resolver",
"futures",
"k9",
"kumo-dkim",
+4
View File
@@ -50,6 +50,10 @@ pub enum Resolver {
}
impl Resolver {
pub async fn resolve_txt<N: IntoName + TryParseIp>(&self, name: N) -> anyhow::Result<Answer> {
self.resolve(name, RecordType::TXT).await
}
pub async fn resolve<N: IntoName + TryParseIp>(
&self,
name: N,
+3 -2
View File
@@ -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<u32>,
@@ -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<u32>,
+1
View File
@@ -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"
+70 -1
View File
@@ -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<Vec<AuthenticationResult>> {
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<Resolver>,
}
impl kumo_dkim::dns::Lookup for ResolverAdapater {
fn lookup_txt<'a>(
&'a self,
name: &'a str,
) -> BoxFuture<'a, Result<Vec<String>, 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<Option<Report>> {
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)| {
+3
View File
@@ -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',