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, find_body_offset, find_header_value,
find_header_value_in_raw,
};
use crate::resolver::DkimResolver;
pub async fn verify<R: DkimResolver + ?Sized>(resolver: &R, raw_message: &[u8]) -> DkimResult {
match verify_inner(resolver, raw_message).await {
Ok(r) => r,
Err(e) => e.to_result(),
}
}
async fn verify_inner<R: DkimResolver + ?Sized>(
resolver: &R,
raw_message: &[u8],
) -> Result<DkimResult, DkimError> {
let (header_value, signed_headers_raw, body_offset) = extract_dkim_signature(raw_message)?;
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();
for name in &header.signed_headers {
if let Some(value) = find_header_value(signed_headers_raw, name) {
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());
}
}