use base64::Engine as _;
use sha2::{Digest, Sha256};
use crate::canon::{canonicalize_body, canonicalize_header};
use crate::crypto::{extract_public_key, verify_signature};
use crate::error::{DkimError, DkimResult};
use crate::header::DkimHeader;
use crate::headers::{
body_offset_minus_blank, clear_b_value, collect_signed_headers,
find_all_header_values_in_raw, find_body_offset, find_header_value_in_raw,
};
use crate::resolver::DkimResolver;
#[derive(Debug, Clone)]
pub struct SignatureOutput {
pub result: DkimResult,
pub header: Option<DkimHeader>,
}
impl SignatureOutput {
pub fn domain(&self) -> &str {
self.header
.as_ref()
.map(|h| h.domain.as_str())
.unwrap_or("")
}
pub fn is_pass(&self) -> bool {
matches!(self.result, DkimResult::Pass)
}
}
pub async fn verify<R: DkimResolver + ?Sized>(resolver: &R, raw_message: &[u8]) -> DkimResult {
let (header_value, headers_raw, body_offset) = match extract_dkim_signature(raw_message) {
Ok(v) => v,
Err(e) => return e.to_result(),
};
match verify_one(
resolver,
raw_message,
&header_value,
headers_raw,
body_offset,
)
.await
{
Ok(r) => r,
Err(e) => e.to_result(),
}
}
pub async fn verify_all<R: DkimResolver + ?Sized>(
resolver: &R,
raw_message: &[u8],
) -> Vec<SignatureOutput> {
let body_offset = match find_body_offset(raw_message) {
Some(o) => o,
None => return Vec::new(),
};
let headers_raw = &raw_message[..body_offset_minus_blank(body_offset, raw_message)];
let values = find_all_header_values_in_raw(headers_raw, b"DKIM-Signature");
let mut out = Vec::with_capacity(values.len());
for value in values {
let header = match DkimHeader::parse(&value) {
Ok(h) => h,
Err(e) => {
out.push(SignatureOutput {
result: e.to_result(),
header: None,
});
continue;
}
};
let result = match verify_one(resolver, raw_message, &value, headers_raw, body_offset).await
{
Ok(r) => r,
Err(e) => e.to_result(),
};
out.push(SignatureOutput {
result,
header: Some(header),
});
}
out
}
async fn verify_one<R: DkimResolver + ?Sized>(
resolver: &R,
raw_message: &[u8],
header_value: &str,
signed_headers_raw: &[u8],
body_offset: usize,
) -> Result<DkimResult, DkimError> {
let header = DkimHeader::parse(header_value)?;
if let Some(x) = header.expiration {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if now > x {
return Err(DkimError::Expired);
}
}
let body = &raw_message[body_offset..];
let canon_body_bytes = canonicalize_body(body, header.canon_body, header.body_length);
let mut body_hasher = Sha256::new();
body_hasher.update(&canon_body_bytes);
let actual_body_hash = body_hasher.finalize();
let expected_body_hash = base64::engine::general_purpose::STANDARD
.decode(&header.body_hash_b64)
.map_err(|_| DkimError::InvalidBase64("bh".into()))?;
if actual_body_hash.as_slice() != expected_body_hash.as_slice() {
return Err(DkimError::BodyHashMismatch);
}
let pubkey_domain = format!("{}._domainkey.{}", header.selector, header.domain);
let txts = resolver.lookup_txt(&pubkey_domain).await?;
if txts.is_empty() {
return Err(DkimError::DnsPermError(format!(
"no TXT at {pubkey_domain}"
)));
}
let key_txt = txts
.iter()
.find(|s| s.contains("p="))
.ok_or_else(|| DkimError::InvalidKey("no p= tag in TXT".into()))?;
let key_bytes = extract_public_key(key_txt)?;
let mut signed_block = Vec::new();
let collected = collect_signed_headers(signed_headers_raw, &header.signed_headers);
for (name, value_opt) in &collected {
let Some(value) = value_opt else { continue };
let canon = canonicalize_header(name, value, header.canon_header);
signed_block.extend_from_slice(&canon);
}
let dkim_sig_b_cleared = clear_b_value(header_value);
let canon_dkim =
canonicalize_header("DKIM-Signature", &dkim_sig_b_cleared, header.canon_header);
let canon_dkim_trimmed = if canon_dkim.ends_with(b"\r\n") {
&canon_dkim[..canon_dkim.len() - 2]
} else {
&canon_dkim
};
signed_block.extend_from_slice(canon_dkim_trimmed);
let signature_bytes = base64::engine::general_purpose::STANDARD
.decode(&header.signature_b64)
.map_err(|_| DkimError::InvalidBase64("b".into()))?;
verify_signature(
header.algorithm,
&key_bytes,
&signed_block,
&signature_bytes,
)?;
Ok(DkimResult::Pass)
}
fn extract_dkim_signature(raw: &[u8]) -> Result<(String, &[u8], usize), DkimError> {
let body_offset = find_body_offset(raw).ok_or(DkimError::MissingHeader)?;
let headers_raw = &raw[..body_offset_minus_blank(body_offset, raw)];
let value = find_header_value_in_raw(headers_raw, b"DKIM-Signature")?;
Ok((value, headers_raw, body_offset))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_dkim_signature_finds_header_and_body_offset() {
let raw = b"DKIM-Signature: v=1; a=rsa-sha256\r\nFrom: a\r\n\r\nbody";
let (value, headers_raw, body_offset) = extract_dkim_signature(raw).unwrap();
assert_eq!(value, " v=1; a=rsa-sha256\r");
assert!(headers_raw.starts_with(b"DKIM-Signature"));
assert_eq!(&raw[body_offset..], b"body");
}
#[test]
fn extract_dkim_signature_errors_when_missing() {
let raw = b"From: a\r\n\r\nbody";
assert!(extract_dkim_signature(raw).is_err());
}
}