use crate::{
canonicalize::{self, BodyCanonicalizer},
crypto::{self, CountingHasher, HashAlgorithm, HashStatus, InsufficientInput},
header::{FieldName, HeaderFields},
signature::{CanonicalizationAlgorithm, DkimSignature},
};
use std::{
collections::{HashMap, HashSet},
error::Error,
fmt::{self, Display, Formatter},
};
pub fn compute_data_hash(
hash_alg: HashAlgorithm,
canon_alg: CanonicalizationAlgorithm,
headers: &HeaderFields,
selected_headers: &[FieldName],
dkim_sig_header_name: &str,
formatted_dkim_sig_header_value: &str,
) -> Box<[u8]> {
let mut cheaders = canonicalize::canonicalize_headers(canon_alg, headers, selected_headers);
canonicalize::canonicalize_header(
&mut cheaders,
canon_alg,
dkim_sig_header_name,
formatted_dkim_sig_header_value,
);
crypto::digest(hash_alg, &cheaders)
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[must_use]
pub enum BodyHasherStance {
Interested,
Done,
}
pub type BodyHasherKey = (Option<usize>, HashAlgorithm, CanonicalizationAlgorithm);
pub fn body_hasher_key(sig: &DkimSignature) -> BodyHasherKey {
let body_len = sig.body_length
.map(|len| len.try_into().expect("integer overflow"));
let hash_alg = sig.algorithm.hash_algorithm();
let canon_alg = sig.canonicalization.body;
(body_len, hash_alg, canon_alg)
}
#[derive(Clone)]
pub struct BodyHasherBuilder {
fail_on_truncate: bool, registrations: HashSet<BodyHasherKey>,
}
impl BodyHasherBuilder {
pub fn new(fail_on_partially_hashed_input: bool) -> Self {
Self {
fail_on_truncate: fail_on_partially_hashed_input,
registrations: HashSet::new(),
}
}
pub fn register_canonicalization(
&mut self,
len: Option<usize>,
hash: HashAlgorithm,
canon: CanonicalizationAlgorithm,
) {
self.registrations.insert((len, hash, canon));
}
pub fn build(self) -> BodyHasher {
use CanonicalizationAlgorithm::*;
let hashers = self.registrations.into_iter()
.map(|key @ (len, hash, _)| (key, (CountingHasher::new(hash, len), false)))
.collect();
BodyHasher {
fail_on_truncate: self.fail_on_truncate,
hashers,
canonicalizer_simple: BodyCanonicalizer::new(Simple),
canonicalizer_relaxed: BodyCanonicalizer::new(Relaxed),
}
}
}
pub struct BodyHasher {
fail_on_truncate: bool,
hashers: HashMap<BodyHasherKey, (CountingHasher, bool)>,
canonicalizer_simple: BodyCanonicalizer,
canonicalizer_relaxed: BodyCanonicalizer,
}
impl BodyHasher {
pub fn hash_chunk(&mut self, chunk: &[u8]) -> BodyHasherStance {
let mut canonicalized_chunk_simple = None;
let mut canonicalized_chunk_relaxed = None;
let mut all_done = true;
let active_hashers = self.hashers.iter_mut().filter(|(_, (hasher, truncated))| {
!hasher.is_done() || (self.fail_on_truncate && !truncated)
});
for ((_, _, canon), (hasher, truncated)) in active_hashers {
let canonicalized_chunk = match canon {
CanonicalizationAlgorithm::Simple => canonicalized_chunk_simple
.get_or_insert_with(|| self.canonicalizer_simple.canonicalize_chunk(chunk)),
CanonicalizationAlgorithm::Relaxed => canonicalized_chunk_relaxed
.get_or_insert_with(|| self.canonicalizer_relaxed.canonicalize_chunk(chunk)),
};
match hasher.update(canonicalized_chunk) {
HashStatus::AllConsumed => {
if self.fail_on_truncate || !hasher.is_done() {
all_done = false;
}
}
HashStatus::Truncated => {
*truncated = true;
}
}
}
if all_done {
BodyHasherStance::Done
} else {
BodyHasherStance::Interested
}
}
pub fn finish(self) -> BodyHashResults {
let mut finish_canonicalization_simple = Some(|| self.canonicalizer_simple.finish());
let mut finish_canonicalization_relaxed = Some(|| self.canonicalizer_relaxed.finish());
let mut canonicalized_chunk_simple = None;
let mut canonicalized_chunk_relaxed = None;
let mut results = HashMap::new();
for (key @ (_, _, canon), (mut hasher, mut truncated)) in self.hashers {
if !hasher.is_done() || (self.fail_on_truncate && !truncated) {
let canonicalized_chunk = match canon {
CanonicalizationAlgorithm::Simple => {
match finish_canonicalization_simple.take() {
Some(f) => canonicalized_chunk_simple.insert(f()),
None => canonicalized_chunk_simple.as_ref().unwrap(),
}
}
CanonicalizationAlgorithm::Relaxed => {
match finish_canonicalization_relaxed.take() {
Some(f) => canonicalized_chunk_relaxed.insert(f()),
None => canonicalized_chunk_relaxed.as_ref().unwrap(),
}
}
};
if let HashStatus::Truncated = hasher.update(canonicalized_chunk) {
truncated = true;
}
}
let res = if self.fail_on_truncate && truncated {
Err(BodyHashError::InputTruncated)
} else {
hasher.finish().map_err(|InsufficientInput| BodyHashError::InsufficientInput)
};
results.insert(key, res);
}
BodyHashResults { results }
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum BodyHashError {
InsufficientInput,
InputTruncated,
}
impl Display for BodyHashError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::InsufficientInput => write!(f, "insufficient input data"),
Self::InputTruncated => write!(f, "input not digested entirely"),
}
}
}
impl Error for BodyHashError {}
pub type BodyHashResult = Result<(Box<[u8]>, usize), BodyHashError>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BodyHashResults {
results: HashMap<BodyHasherKey, BodyHashResult>,
}
impl BodyHashResults {
pub fn get(&self, key: &BodyHasherKey) -> Option<&BodyHashResult> {
self.results.get(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{signature::CanonicalizationAlgorithm::*, util};
use rand::{
distributions::{Distribution, Slice},
Rng,
};
use std::ops::RangeInclusive;
fn key_simple() -> BodyHasherKey {
(None, HashAlgorithm::Sha256, Simple)
}
fn limited_key_simple(n: usize) -> BodyHasherKey {
(Some(n), HashAlgorithm::Sha256, Simple)
}
fn key_relaxed() -> BodyHasherKey {
(None, HashAlgorithm::Sha256, Relaxed)
}
fn limited_key_relaxed(n: usize) -> BodyHasherKey {
(Some(n), HashAlgorithm::Sha256, Relaxed)
}
#[test]
fn body_hasher_simple() {
let key1 @ (_, _, canon_alg1) = key_simple();
let key2 @ (len, hash_alg, canon_alg2) = key_relaxed();
let mut hasher = BodyHasherBuilder::new(false);
hasher.register_canonicalization(len, hash_alg, canon_alg1);
hasher.register_canonicalization(len, hash_alg, canon_alg2);
let mut hasher = hasher.build();
assert_eq!(hasher.hash_chunk(b"abc \r\n"), BodyHasherStance::Interested);
let results = hasher.finish();
let res1 = results.get(&key1).unwrap();
assert_eq!(res1.as_ref().unwrap().1, 6);
let res2 = results.get(&key2).unwrap();
assert_eq!(res2.as_ref().unwrap().1, 5);
}
#[test]
fn body_hasher_fail_on_partial() {
let key1 @ (len, hash_alg, canon_alg1) = limited_key_relaxed(4);
let mut hasher = BodyHasherBuilder::new(true);
hasher.register_canonicalization(len, hash_alg, canon_alg1);
let mut hasher = hasher.build();
assert_eq!(hasher.hash_chunk(b"ab"), BodyHasherStance::Interested);
assert_eq!(hasher.hash_chunk(b"c"), BodyHasherStance::Interested);
let results = hasher.finish();
let res1 = results.get(&key1).unwrap();
assert_eq!(res1, &Err(BodyHashError::InputTruncated));
}
#[test]
fn body_hasher_hash_with_length() {
let key1 @ (len, hash_alg, canon_alg1) = limited_key_simple(28);
let mut hasher = BodyHasherBuilder::new(false);
hasher.register_canonicalization(len, hash_alg, canon_alg1);
let mut hasher = hasher.build();
assert_eq!(hasher.hash_chunk(b"well hello \r\n"), BodyHasherStance::Interested);
assert_eq!(hasher.hash_chunk(b"\r\n what's up \r"), BodyHasherStance::Interested);
assert_eq!(hasher.hash_chunk(b"\n\r\n"), BodyHasherStance::Done);
let results = hasher.finish();
let res1 = results.get(&key1).unwrap();
assert_eq!(
res1.as_ref().unwrap().0,
sha256_digest(b"well hello \r\n\r\n what's up \r")
);
}
#[test]
fn body_hasher_known_hash_sample() {
let key1 @ (len, hash_alg, canon_alg1) = key_relaxed();
let mut hasher = BodyHasherBuilder::new(false);
hasher.register_canonicalization(len, hash_alg, canon_alg1);
let mut hasher = hasher.build();
let body = b"\
Hello Proff,\r\n\
\r\n\
Let\xe2\x80\x99s try this again, with line\r\n\
breaks and empty lines even.\r\n\
\r\n\
Ciao, und bis bald\r\n\
\r\n\
\r\n\
-- \r\n\
David\r\n\
";
assert_eq!(hasher.hash_chunk(body), BodyHasherStance::Interested);
let results = hasher.finish();
let res1 = results.get(&key1).unwrap();
assert_eq!(
util::encode_base64(&res1.as_ref().unwrap().0),
"RMSbeRTj/zCxWeWQXpEIbiqxH0Jqg5eYs4ORzOt3MT0="
);
}
#[cfg(feature = "pre-rfc8301")]
#[test]
fn body_hasher_reuse_canonicalized_chunk() {
let key1 @ (len, hash_alg1, canon_alg1) = key_relaxed();
let key2 @ (_, hash_alg2, canon_alg2) = (None, HashAlgorithm::Sha1, Relaxed);
let mut hasher = BodyHasherBuilder::new(false);
hasher.register_canonicalization(len, hash_alg1, canon_alg1);
hasher.register_canonicalization(len, hash_alg2, canon_alg2);
let mut hasher = hasher.build();
assert_eq!(hasher.hash_chunk(b"abc \r\n"), BodyHasherStance::Interested);
let results = hasher.finish();
let res1 = results.get(&key1).unwrap();
let res2 = results.get(&key2).unwrap();
assert_eq!(res1.as_ref().unwrap().1, res2.as_ref().unwrap().1);
}
fn sha256_digest(msg: &[u8]) -> Box<[u8]> {
crypto::digest(HashAlgorithm::Sha256, msg)
}
#[test]
#[ignore = "randomly generated test inputs"]
fn fuzz_body_hasher_plain() {
fuzz_body_hasher(false);
}
#[test]
#[ignore = "randomly generated test inputs"]
fn fuzz_body_hasher_fail_on_truncate() {
fuzz_body_hasher(true);
}
fn fuzz_body_hasher(fail_on_truncate: bool) {
let elems = ["x", "y", " ", "\r\n"];
let chunk_len = 0..=4;
let chunk_count = 1..=4;
let param_count = 1..=6;
let limit = 0..=13;
run_fuzz(1000, fail_on_truncate, &elems, chunk_len, chunk_count, param_count, limit);
}
fn run_fuzz(
repetitions: usize,
fail_on_truncate: bool,
elems: &[&str],
chunk_len: RangeInclusive<u8>,
chunk_count: RangeInclusive<u8>,
param_count: RangeInclusive<u8>,
limit: RangeInclusive<u8>,
) {
let elems = Slice::new(elems).unwrap();
let hashes = Slice::new(&[
HashAlgorithm::Sha256,
#[cfg(feature = "pre-rfc8301")]
HashAlgorithm::Sha1,
])
.unwrap();
let canons = Slice::new(&[Simple, Relaxed]).unwrap();
let mut rng = rand::thread_rng();
for _ in 0..repetitions {
let mut chunks = vec![];
for _ in 0..rng.gen_range(chunk_count.clone()) {
let n = rng.gen_range(chunk_len.clone()).into();
let s: String = elems.sample_iter(&mut rng).copied().take(n).collect();
chunks.push(s);
}
let chunks: Vec<_> = chunks.iter().map(|s| s.as_str()).collect();
let mut params = vec![];
for _ in 0..rng.gen_range(param_count.clone()) {
let l = if rng.gen_bool(1.0 / 4.0) {
None
} else {
Some(rng.gen_range(limit.clone()).into())
};
let h = hashes.sample(&mut rng);
let c = canons.sample(&mut rng);
params.push((l, *h, *c));
}
compare_impls(fail_on_truncate, &chunks, ¶ms);
}
}
fn compare_impls(fail_on_truncate: bool, chunks: &[&str], params: &[BodyHasherKey]) {
let mut hasher = BodyHasherBuilder::new(fail_on_truncate);
for &(l, h, c) in params {
hasher.register_canonicalization(l, h, c);
}
let mut hasher = hasher.build();
for ch in chunks {
if let BodyHasherStance::Done = hasher.hash_chunk(ch.as_bytes()) {
break;
}
}
let results = hasher.finish();
let s: String = chunks.iter().copied().collect();
let alt_impl = move |(l, h, c)| {
let mut bc = BodyCanonicalizer::new(c);
let mut result = bc.canonicalize_chunk(s.as_bytes()).into_owned();
result.extend(bc.finish().into_owned());
if let Some(n) = l {
if n > result.len() {
return Err(BodyHashError::InsufficientInput);
}
if fail_on_truncate && n < result.len() {
return Err(BodyHashError::InputTruncated);
}
result.truncate(n);
}
let hash = crypto::digest(h, &result);
Ok((hash, result.len()))
};
for &key in params {
let r1 = results.get(&key).unwrap();
let r2 = alt_impl(key);
assert_eq!(
r1, &r2,
"divergent results for inputs {chunks:?} and {key:?}",
);
}
}
}