use super::Dkim2Hash;
use crate::{
crypto::{HashContext, HashImpl, HashOutput, Sha256, Sha512},
dkim::{
Canonicalization,
canonicalize::{SpacedTokens, write_relaxed_name},
},
headers::{Writable, Writer},
};
use std::cmp::Ordering;
#[cfg(test)]
impl crate::crypto::HashAlgorithm {
pub(crate) fn headers_hash<'x>(
&self,
headers: impl IntoIterator<Item = (&'x [u8], &'x [u8])>,
) -> HashOutput {
let headers = headers.into_iter();
let mut signed: Vec<(&[u8], &[u8])> = Vec::with_capacity(headers.size_hint().0);
signed.extend(headers.filter(|(name, _)| !is_non_signed_header(name)));
signed.reverse();
signed.sort_by(|(a, _), (b, _)| cmp_ignore_ascii_case(a.trim_ascii(), b.trim_ascii()));
self.hash(RelaxedHeaders(&signed))
}
pub(crate) fn body_hash(&self, body: &[u8]) -> HashOutput {
self.hash(Canonicalization::Simple.canonical_body(body, u64::MAX))
}
}
pub(crate) type Digests = [Option<HashOutput>; Dkim2Hash::COUNT];
impl Dkim2Hash {
pub(crate) const COUNT: usize = 2;
pub(crate) fn index(self) -> usize {
match self {
Dkim2Hash::Sha256 => 0,
Dkim2Hash::Sha512 => 1,
}
}
}
pub(crate) fn header_digests<'x>(
headers: impl IntoIterator<Item = (&'x [u8], &'x [u8])>,
wanted: [bool; Dkim2Hash::COUNT],
) -> Digests {
if wanted == [false; Dkim2Hash::COUNT] {
return Default::default();
}
let headers = headers.into_iter();
let mut signed: Vec<(&[u8], &[u8])> = Vec::with_capacity(headers.size_hint().0);
signed.extend(headers.filter(|(name, _)| !is_non_signed_header(name)));
signed.reverse();
signed.sort_by(|(a, _), (b, _)| cmp_ignore_ascii_case(a.trim_ascii(), b.trim_ascii()));
digests(RelaxedHeaders(&signed), wanted)
}
pub(crate) fn body_digests(body: &[u8], wanted: [bool; Dkim2Hash::COUNT]) -> Digests {
digests(
Canonicalization::Simple.canonical_body(body, u64::MAX),
wanted,
)
}
fn digests(data: impl Writable, wanted: [bool; Dkim2Hash::COUNT]) -> Digests {
match wanted {
[true, true] => {
let mut both = Tee(Sha256::hasher(), Sha512::hasher());
data.write(&mut both);
[Some(both.0.complete()), Some(both.1.complete())]
}
[true, false] => [Some(Sha256::digest(data)), None],
[false, true] => [None, Some(Sha512::digest(data))],
[false, false] => [None, None],
}
}
struct Tee<A, B>(A, B);
impl<A: Writer, B: Writer> Writer for Tee<A, B> {
fn write(&mut self, buf: &[u8]) {
self.0.write(buf);
self.1.write(buf);
}
}
struct RelaxedHeaders<'a, 'x>(&'a [(&'x [u8], &'x [u8])]);
impl Writable for RelaxedHeaders<'_, '_> {
fn write(self, writer: &mut impl Writer) {
Canonicalization::Relaxed.canonicalize_headers(self.0.iter().copied(), writer);
}
}
pub(crate) struct CanonicalizedHeaderWriter<'x, W: Writer> {
inner: &'x mut W,
}
impl<'x, W: Writer> CanonicalizedHeaderWriter<'x, W> {
pub fn new(inner: &'x mut W, field: &[u8]) -> Self {
write_relaxed_name(field, inner);
Self { inner }
}
pub fn finalize(self) {
self.inner.write(b"\r\n");
}
}
impl<'x, W: Writer> Writer for CanonicalizedHeaderWriter<'x, W> {
fn write(&mut self, buf: &[u8]) {
for token in SpacedTokens::new(buf) {
self.inner.write(token.token);
}
}
}
pub(crate) fn cmp_ignore_ascii_case(a: &[u8], b: &[u8]) -> Ordering {
for (x, y) in a.iter().zip(b.iter()) {
if x != y {
let (x, y) = (x.to_ascii_lowercase(), y.to_ascii_lowercase());
if x != y {
return x.cmp(&y);
}
}
}
a.len().cmp(&b.len())
}
pub(super) fn is_non_signed_header(name: &[u8]) -> bool {
let name = name.trim_ascii();
hashify::set_ignore_case!(
name,
b"apparently-to",
b"arc-authentication-results",
b"arc-message-signature",
b"arc-seal",
b"authentication-results",
b"delivered-to",
b"dkim-signature",
b"dkim2-signature",
b"dl-expansion-history",
b"message-instance",
b"original-recipient",
b"received",
b"return-path",
b"sio-label-history",
b"x400-received",
b"x400-trace",
) || matches!(name, [b'x' | b'X', b'-', ..])
|| name
.get(..RECEIVED_PREFIX.len())
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(RECEIVED_PREFIX))
}
const RECEIVED_PREFIX: &[u8] = b"received-";
#[cfg(test)]
mod test {
use super::is_non_signed_header;
use crate::crypto::HashAlgorithm;
#[test]
fn excluded_headers_are_classified() {
for name in [
"received",
"Received",
"return-path",
"delivered-to",
"Delivered-To",
"authentication-results",
"dkim-signature",
"DKIM-Signature",
"message-instance",
"dkim2-signature",
"arc-seal",
"arc-message-signature",
"arc-authentication-results",
"x-spam-score",
"X-Anything",
"Received-SPF",
"received-dkim2",
"Apparently-To",
"DL-Expansion-History",
"Original-Recipient",
"SIO-Label-History",
"X400-Received",
"x400-trace",
] {
assert!(
is_non_signed_header(name.as_bytes()),
"{name} must be ignored"
);
}
for name in [
"from",
"to",
"subject",
"date",
"message-id",
"list-unsubscribe",
"receivedx",
"x400",
"x",
"Auto-Submitted",
"vbr-info",
] {
assert!(
!is_non_signed_header(name.as_bytes()),
"{name} must be signed"
);
}
}
#[test]
fn digests_match_single_hashes() {
use super::{Dkim2Hash, body_digests, header_digests};
let headers: &[(&[u8], &[u8])] = &[
(b"Subject", b" one \r\n"),
(b"From", b" a@example.com\r\n"),
(b"Received", b" x\r\n"),
(b"subject", b" two\r\n"),
];
let body = b"line one\r\nline two\r\n\r\n\r\n";
for wanted in [[true, true], [true, false], [false, true]] {
let header = header_digests(headers.iter().copied(), wanted);
let body_hashes = body_digests(body, wanted);
for (hash, enabled) in [Dkim2Hash::Sha256, Dkim2Hash::Sha512]
.into_iter()
.zip(wanted)
{
let expected_header =
enabled.then(|| hash.algorithm().headers_hash(headers.iter().copied()));
let expected_body = enabled.then(|| hash.algorithm().body_hash(body));
assert_eq!(header.get(hash.index()).cloned().flatten(), expected_header);
assert_eq!(
body_hashes.get(hash.index()).cloned().flatten(),
expected_body
);
}
}
assert_eq!(
header_digests(headers.iter().copied(), [false, false]),
[None, None]
);
}
#[test]
fn body_hash_ignores_trailing_blank_lines() {
let none = HashAlgorithm::Sha256.body_hash(b"Hello, world.");
let one = HashAlgorithm::Sha256.body_hash(b"Hello, world.\r\n");
let many = HashAlgorithm::Sha256.body_hash(b"Hello, world.\r\n\r\n\r\n");
assert_eq!(none, one);
assert_eq!(one, many);
}
#[test]
fn empty_body_hashes_as_single_crlf() {
assert_eq!(
HashAlgorithm::Sha256.body_hash(b""),
HashAlgorithm::Sha256.body_hash(b"\r\n\r\n")
);
}
}