use std::io::{self, Write};
use std::mem;
use super::{BodyCanonicaliser, Error, Failure, Header};
use crate::mime::header::FULL_HEADER_LINE;
pub(super) fn header_hash_data(
header: &Header<'_>,
header_block: &[u8],
) -> Vec<u8> {
let mut out = Vec::<u8>::with_capacity(header_block.len() / 4);
let mut resolved_headers: Vec<&str> = vec![""; header.signed_headers.len()];
for m in FULL_HEADER_LINE.captures_iter(header_block) {
let Ok(header_name) = std::str::from_utf8(m.get(2).unwrap().as_bytes())
else {
continue;
};
let Ok(mut header_line) =
std::str::from_utf8(m.get(1).unwrap().as_bytes())
else {
continue;
};
for (resolved, target_name) in
resolved_headers.iter_mut().zip(&header.signed_headers)
{
if target_name.eq_ignore_ascii_case(header_name) {
mem::swap(&mut header_line, resolved);
}
}
}
for h in resolved_headers {
if h.is_empty() {
continue;
}
header
.canonicalisation
.header
.write(&mut out, h, "")
.expect("writing to a vec never fails");
out.extend_from_slice(b"\r\n");
}
let header_raw = header.raw();
header
.canonicalisation
.header
.write(
&mut out,
&header_raw.text[..header_raw.b.start],
&header_raw.text[header_raw.b.end..],
)
.expect("writing to a vec never fails");
out
}
pub(super) struct BodyHasher {
body_hash: BodyCanonicaliser<DigestWriter>,
}
impl BodyHasher {
pub fn new(header: &Header<'_>) -> Self {
Self {
body_hash: BodyCanonicaliser::new(
DigestWriter {
digest: openssl::hash::Hasher::new(
header.algorithm.hash.message_digest(),
),
limit: header.body_length.unwrap_or(u64::MAX),
bytes_written: 0,
},
header.canonicalisation.body,
),
}
}
pub fn finish(self, header: &Header<'_>) -> Result<Vec<u8>, Error> {
let body_hash = self
.body_hash
.finish()
.map_err(Error::Io)?;
let hash = body_hash
.digest
.and_then(|mut h| h.finish())
.map_err(Error::Ssl)?
.to_vec();
if let Some(body_length) = header.body_length {
if body_hash.bytes_written < body_length {
return Err(Error::Fail(Failure::BodyTruncated));
}
}
Ok(hash)
}
}
impl Write for BodyHasher {
fn write(&mut self, src: &[u8]) -> io::Result<usize> {
self.body_hash.write(src)
}
fn flush(&mut self) -> io::Result<()> {
self.body_hash.flush()
}
}
struct DigestWriter {
digest: Result<openssl::hash::Hasher, openssl::error::ErrorStack>,
limit: u64,
bytes_written: u64,
}
impl Write for DigestWriter {
fn write(&mut self, src: &[u8]) -> io::Result<usize> {
let bytes_to_hash = usize::try_from(self.limit - self.bytes_written)
.unwrap_or(usize::MAX)
.min(src.len());
if bytes_to_hash > 0 {
let result = match self.digest {
Ok(ref mut digest) => digest.update(src),
Err(_) => Ok(()),
};
if let Err(e) = result {
self.digest = Err(e);
}
self.bytes_written += bytes_to_hash as u64;
}
Ok(src.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[cfg(test)]
mod test {
use std::borrow::Cow;
use chrono::prelude::*;
use super::super::{
split_message, test_domain_keys, Algorithm, BodyCanonicalisation,
Canonicalisation, HashAlgorithm, HeaderCanonicalisation,
SignatureAlgorithm, TxtRecord, HEADER_NAME,
};
use super::*;
use crate::test_data::*;
fn extract_nth_dkim_header(header_block: &[u8], n: usize) -> Header<'_> {
FULL_HEADER_LINE
.captures_iter(header_block)
.filter(|m| {
std::str::from_utf8(m.get(2).unwrap().as_bytes())
.unwrap()
.eq_ignore_ascii_case(HEADER_NAME)
})
.skip(n)
.next()
.map(|m| {
Header::parse(
std::str::from_utf8(m.get(1).unwrap().as_bytes()).unwrap(),
)
.unwrap()
})
.expect("couldn't find DKIM-Signature header")
}
fn validate_body_hash(message: &[u8]) {
let (header_block, body) = split_message(message);
let header = extract_nth_dkim_header(header_block, 0);
let mut hasher = BodyHasher::new(&header);
hasher.write_all(body).unwrap();
let hash = hasher.finish(&header).unwrap();
assert_eq!(header.body_hash, hash);
}
fn collect_header_hash_data(message: &[u8], n: usize) -> String {
let (header_block, _) = split_message(message);
let header = extract_nth_dkim_header(header_block, n);
let out = header_hash_data(&header, header_block);
String::from_utf8(out).unwrap()
}
#[test]
fn test_header_hash_data() {
assert_eq!(
"message-id:<10d8b2e8-acd3-98d9-5312-6e2a9c900286@lin.gl>\r\n\
date:Mon, 8 May 2023 20:19:29 +0000\r\n\
mime-version:1.0\r\n\
to:lindotgl@gmail.com\r\n\
from:Jason Lingle <jason@lin.gl>\r\n\
subject:Test email\r\n\
content-type:text/plain; charset=UTF-8; format=flowed\r\n\
content-transfer-encoding:7bit\r\n\
dkim-signature:v=1; a=rsa-sha1; c=relaxed; d=lin.gl; h=message-id:date \
:mime-version:to:from:subject:content-type \
:content-transfer-encoding; s=selector1; bh=rnQpHRF2D2lVmnkKkePd \
zkry2F8=; b=",
collect_header_hash_data(DKIM_LINGL_RSA_SHA1, 0),
);
assert_eq!(
"date:Tue, 12 Sep 2023 01:14:07 +0000\r\n\
from:\"Amazon.co.jp\" <store-news@amazon.co.jp>\r\n\
to:jason@lin.gl\r\n\
message-id:<0101018a86f3ed7f-fe8d791b-adcd-4244-\
8b46-fa7d227050bc-000000@us-west-2.amazonses.com>\r\n\
subject:=?UTF-8?B?6LOt44Kx44Kw44Or44Kk5Y+MKDE0KSAo44Ks44Oz44Ks44Oz?= \
=?UTF-8?B?44Kz44Of44OD44Kv44K5Sk9LRVIp44Gq44Gp44GK55+l44KJ44Gb?=\r\n\
mime-version:1.0\r\n\
content-type:multipart/alternative; \
boundary=\"----=_Part_375994_2040052076.1694481247599\"\r\n\
dkim-signature:v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple; \
s=55v6dsnbko3asrylf5mgtqv5mgll5any; d=amazon.co.jp; t=1694481247; \
h=Date:From:To:Message-ID:Subject:MIME-Version:Content-Type; \
bh=HcKSAdwhXQ1MiHCZKbdFHJoJhb1uuMH9sTy0jsyxiew=; \
b=",
collect_header_hash_data(DKIM_AMAZONCOJP_RSA_SHA256, 0),
);
assert_eq!(
"date:Tue, 12 Sep 2023 01:14:07 +0000\r\n\
from:\"Amazon.co.jp\" <store-news@amazon.co.jp>\r\n\
to:jason@lin.gl\r\n\
message-id:<0101018a86f3ed7f-fe8d791b-adcd-4244-\
8b46-fa7d227050bc-000000@us-west-2.amazonses.com>\r\n\
subject:=?UTF-8?B?6LOt44Kx44Kw44Or44Kk5Y+MKDE0KSAo44Ks44Oz44Ks44Oz?= \
=?UTF-8?B?44Kz44Of44OD44Kv44K5Sk9LRVIp44Gq44Gp44GK55+l44KJ44Gb?=\r\n\
mime-version:1.0\r\n\
content-type:multipart/alternative; \
boundary=\"----=_Part_375994_2040052076.1694481247599\"\r\n\
feedback-id:1.us-west-2.Ci45k5OkUuH90u7dO0Ory1StkcbFm601BtN95yGGkr4=:AmazonSES\r\n\
dkim-signature:v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple; \
s=hsbnp7p3ensaochzwyq5wwmceodymuwv; d=amazonses.com; t=1694481247; \
h=Date:From:To:Message-ID:Subject:MIME-Version:Content-Type:Feedback-ID; \
bh=HcKSAdwhXQ1MiHCZKbdFHJoJhb1uuMH9sTy0jsyxiew=; \
b=",
collect_header_hash_data(DKIM_AMAZONCOJP_RSA_SHA256, 1),
);
fn utc(ts: i64) -> DateTime<Utc> {
DateTime::from_timestamp(ts, 0).unwrap()
}
let signing_header = Header {
raw: None,
version: 1,
algorithm: Algorithm {
signature: SignatureAlgorithm::Ed25519,
hash: HashAlgorithm::Sha256,
},
signature: b"\xb2\x28\x27\x6a\xdb\xab\x79\xe7\x9e".to_vec(),
body_hash: b"\x6e\x87\x72\x85\xab\x21".to_vec(),
canonicalisation: Canonicalisation {
body: BodyCanonicalisation::Simple,
header: HeaderCanonicalisation::Simple,
},
sdid: Cow::Borrowed("example.com"),
auid: Some(Cow::Borrowed("@example.com")),
signed_headers: vec![
Cow::Borrowed("From"),
Cow::Borrowed("To"),
Cow::Borrowed("From"),
Cow::Borrowed("Content-Location"),
Cow::Borrowed("Content-Transfer-Encoding"),
Cow::Borrowed("Subject"),
],
body_length: None,
dns_txt: true,
selector: Cow::Borrowed("selector0"),
signature_timestamp: Some(utc(42)),
signature_expiration: Some(utc(54)),
};
let out = header_hash_data(
&signing_header,
b"FrOm: first from header\r\n\
To: to header\r\n\
from: second from header\r\n\
foo: bar\r\n\
FROM: third from header\r\n",
);
assert_eq!(
"FROM: third from header\r\n\
To: to header\r\n\
from: second from header\r\n\
DKIM-Signature: v=1;a=ed25519-sha256;c=simple/simple;d=example.com;\r\n\
\x20i=@example.com;h=From:To:From:Content-Location:Content-Transfer-Encoding:\r\n\
\x20Subject;s=selector0;t=42;x=54;bh=bodyhash;b=",
String::from_utf8(out).unwrap(),
);
}
#[test]
fn test_body_hash_lingl_rsa_sha1() {
validate_body_hash(DKIM_LINGL_RSA_SHA1);
}
#[test]
fn test_body_hash_amazoncojp_2x_rsa_sha256() {
validate_body_hash(DKIM_AMAZONCOJP_RSA_SHA256);
}
#[test]
fn test_body_hash_yahoo_rsa_sha256() {
validate_body_hash(DKIM_YAHOO_RSA_SHA256);
}
#[test]
fn test_body_hash_limited_length() {
let header = Header::parse(
"DKIM-Signature: v=1;a=rsa-sha256;b=;\
bh=Nsx3H8hrN69TdEBLtV66Rt2u82rLWvpdZQpUyTcsqE4=;\
c=relaxed/relaxed;d=example.com;h=from:date;l=7;\
s=selector",
)
.unwrap();
let mut hasher = BodyHasher::new(&header);
hasher.write_all(b"foo \t\r\nbar").unwrap();
let hash = hasher.finish(&header).unwrap();
assert_eq!(header.body_hash, hash);
}
#[test]
fn test_body_hash_truncated() {
let header = Header::parse(
"DKIM-Signature: v=1;a=rsa-sha256;b=;\
bh=Nsx3H8hrN69TdEBLtV66Rt2u82rLWvpdZQpUyTcsqE4=;\
c=relaxed/relaxed;d=example.com;h=from:date;l=7;\
s=selector",
)
.unwrap();
let mut hasher = BodyHasher::new(&header);
hasher.write_all(b"foo").unwrap();
assert_matches!(
Err(Error::Fail(Failure::BodyTruncated)),
hasher.finish(&header),
);
}
fn primitive_dkim_verify(message: &[u8], dns_txt: &str, n: usize) {
let (header_block, body) = split_message(message);
let header = extract_nth_dkim_header(header_block, n);
assert_eq!(SignatureAlgorithm::Rsa, header.algorithm.signature);
let dns_txt = TxtRecord::parse(dns_txt).unwrap();
assert_eq!(SignatureAlgorithm::Rsa, dns_txt.key_type);
let public_key =
openssl::rsa::Rsa::public_key_from_der(&dns_txt.public_key)
.unwrap();
let public_key = openssl::pkey::PKey::from_rsa(public_key).unwrap();
let mut verifier = openssl::sign::Verifier::new(
header.algorithm.hash.message_digest(),
&public_key,
)
.unwrap();
let mut hasher = BodyHasher::new(&header);
hasher.write_all(body).unwrap();
let body_hash = hasher.finish(&header).unwrap();
assert_eq!(header.body_hash, body_hash);
let header_data = header_hash_data(&header, header_block);
assert!(
verifier
.verify_oneshot(&header.signature, &header_data)
.unwrap(),
"verification failed",
);
}
#[test]
fn test_signature_lingl_rsa_sha1() {
primitive_dkim_verify(
DKIM_LINGL_RSA_SHA1,
test_domain_keys::SELECTOR1_LIN_GL,
0,
);
}
#[test]
fn test_signature_amazoncojp_rsa_sha256() {
primitive_dkim_verify(
DKIM_AMAZONCOJP_RSA_SHA256,
test_domain_keys::HSG_AMAZONSES_COM,
1,
);
}
#[test]
fn test_signature_yahoo_rsa256() {
primitive_dkim_verify(
DKIM_YAHOO_RSA_SHA256,
test_domain_keys::S2048_YAHOO_COM,
0,
);
}
}