mod header;
mod lookup;
mod query;
mod verify;
pub use lookup::LookupTxt;
use crate::{
crypto,
header::{FieldName, HeaderFields},
message_hash::{
body_hasher_key, BodyHashError, BodyHashResults, BodyHasher, BodyHasherBuilder,
BodyHasherStance,
},
record::{DkimKeyRecord, DkimKeyRecordError},
signature::{DkimSignature, DkimSignatureError, DkimSignatureErrorKind},
util::CanonicalStr,
verifier::header::{HeaderVerifier, VerifyStatus},
};
use std::{
error::Error,
fmt::{self, Display, Formatter},
sync::Arc,
time::{Duration, SystemTime},
};
use tracing::trace;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Config {
pub lookup_timeout: Duration,
pub max_signatures: usize,
pub headers_required_in_signature: Vec<FieldName>,
pub headers_forbidden_to_be_unsigned: Vec<FieldName>,
pub min_key_bits: usize,
pub allow_sha1: bool,
pub forbid_unsigned_content: bool,
pub allow_expired: bool,
pub allow_timestamp_in_future: bool,
pub time_tolerance: Duration,
pub fixed_system_time: Option<SystemTime>,
}
impl Config {
fn current_timestamp(&self) -> u64 {
self.fixed_system_time
.unwrap_or_else(SystemTime::now)
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
}
impl Default for Config {
fn default() -> Self {
Self {
lookup_timeout: Duration::from_secs(10),
max_signatures: 10,
headers_required_in_signature: vec![],
headers_forbidden_to_be_unsigned: vec![FieldName::new("From").unwrap()],
min_key_bits: 1024,
allow_sha1: false,
forbid_unsigned_content: false,
allow_expired: false,
allow_timestamp_in_future: false,
time_tolerance: Duration::from_secs(5 * 60),
fixed_system_time: None,
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct VerificationResult {
pub status: VerificationStatus,
pub index: usize,
pub signature: Option<DkimSignature>,
pub key_record: Option<Arc<DkimKeyRecord>>,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum VerificationStatus {
Success,
Failure(VerificationError),
}
impl VerificationStatus {
pub fn to_dkim_result(&self) -> DkimResult {
use VerificationError::*;
match self {
Self::Success => DkimResult::Pass,
Self::Failure(error) => match error {
DkimSignatureFormat(error) => match error.kind {
DkimSignatureErrorKind::Utf8Encoding
| DkimSignatureErrorKind::TagListFormat
| DkimSignatureErrorKind::IncompatibleVersion
| DkimSignatureErrorKind::UnsupportedAlgorithm
| DkimSignatureErrorKind::InvalidBase64
| DkimSignatureErrorKind::UnsupportedCanonicalization
| DkimSignatureErrorKind::InvalidDomain
| DkimSignatureErrorKind::InvalidSignedHeaderName
| DkimSignatureErrorKind::InvalidIdentity
| DkimSignatureErrorKind::InvalidBodyLength
| DkimSignatureErrorKind::InvalidQueryMethod
| DkimSignatureErrorKind::NoSupportedQueryMethods
| DkimSignatureErrorKind::InvalidSelector
| DkimSignatureErrorKind::InvalidTimestamp
| DkimSignatureErrorKind::InvalidExpiration
| DkimSignatureErrorKind::InvalidCopiedHeaderField => DkimResult::Neutral,
DkimSignatureErrorKind::HistoricAlgorithm
| DkimSignatureErrorKind::EmptySignatureTag
| DkimSignatureErrorKind::EmptyBodyHashTag
| DkimSignatureErrorKind::EmptySignedHeadersTag
| DkimSignatureErrorKind::FromHeaderNotSigned
| DkimSignatureErrorKind::MissingVersionTag
| DkimSignatureErrorKind::MissingAlgorithmTag
| DkimSignatureErrorKind::MissingSignatureTag
| DkimSignatureErrorKind::MissingBodyHashTag
| DkimSignatureErrorKind::MissingDomainTag
| DkimSignatureErrorKind::MissingSignedHeadersTag
| DkimSignatureErrorKind::MissingSelectorTag
| DkimSignatureErrorKind::DomainMismatch
| DkimSignatureErrorKind::ExpirationNotAfterTimestamp => DkimResult::Permerror,
},
Overflow => DkimResult::Neutral,
NoKeyFound
| InvalidKeyDomain
| WrongKeyType
| KeyRevoked
| DisallowedHashAlgorithm
| DomainMismatch
| InsufficientContent => DkimResult::Permerror,
Timeout | KeyLookup => DkimResult::Temperror,
KeyRecordFormat(error) => match error {
DkimKeyRecordError::RecordFormat
| DkimKeyRecordError::TagListFormat
| DkimKeyRecordError::IncompatibleVersion
| DkimKeyRecordError::InvalidHashAlgorithm
| DkimKeyRecordError::NoSupportedHashAlgorithms
| DkimKeyRecordError::UnsupportedKeyType
| DkimKeyRecordError::InvalidQuotedPrintable
| DkimKeyRecordError::InvalidBase64
| DkimKeyRecordError::InvalidServiceType
| DkimKeyRecordError::NoSupportedServiceTypes
| DkimKeyRecordError::InvalidFlag => DkimResult::Neutral,
DkimKeyRecordError::MisplacedVersionTag
| DkimKeyRecordError::MissingKeyTag => DkimResult::Permerror,
},
VerificationFailure(error) => match error {
crypto::VerificationError::InvalidKey
| crypto::VerificationError::InsufficientKeySize => DkimResult::Permerror,
crypto::VerificationError::VerificationFailure => DkimResult::Fail,
},
BodyHashMismatch => DkimResult::Fail,
Policy(_) => DkimResult::Policy,
},
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum PolicyError {
RequiredHeaderNotSigned,
UnsignedHeaderOccurrence,
UnsignedContent,
SignatureExpired,
TimestampInFuture,
Sha1HashAlgorithm,
KeyTooSmall,
}
impl Display for PolicyError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::RequiredHeaderNotSigned => write!(f, "required header not signed"),
Self::UnsignedHeaderOccurrence => write!(f, "unsigned occurrence of signed header"),
Self::UnsignedContent => write!(f, "unsigned content in message body"),
Self::SignatureExpired => write!(f, "signature expired"),
Self::TimestampInFuture => write!(f, "timestamp in future"),
Self::Sha1HashAlgorithm => write!(f, "SHA-1 hash algorithm not acceptable"),
Self::KeyTooSmall => write!(f, "public key too small"),
}
}
}
impl Error for PolicyError {}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum VerificationError {
DkimSignatureFormat(DkimSignatureError),
Overflow,
NoKeyFound,
InvalidKeyDomain,
Timeout,
KeyLookup,
KeyRecordFormat(DkimKeyRecordError),
WrongKeyType,
KeyRevoked,
DisallowedHashAlgorithm,
DomainMismatch,
VerificationFailure(crypto::VerificationError),
InsufficientContent,
BodyHashMismatch,
Policy(PolicyError),
}
impl Display for VerificationError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::DkimSignatureFormat(_) => write!(f, "unusable DKIM signature header"),
Self::Overflow => write!(f, "integer too large"),
Self::NoKeyFound => write!(f, "no key record found"),
Self::InvalidKeyDomain => write!(f, "invalid key record domain name"),
Self::Timeout => write!(f, "key record lookup timed out"),
Self::KeyLookup => write!(f, "key record lookup failed"),
Self::KeyRecordFormat(_) => write!(f, "unusable public key record"),
Self::WrongKeyType => write!(f, "wrong key type in key record"),
Self::KeyRevoked => write!(f, "key revoked"),
Self::DisallowedHashAlgorithm => write!(f, "hash algorithm disallowed in key record"),
Self::DomainMismatch => write!(f, "domain mismatch"),
Self::VerificationFailure(_) => write!(f, "signature verification failed"),
Self::InsufficientContent => write!(f, "not enough message body content"),
Self::BodyHashMismatch => write!(f, "body hash did not verify"),
Self::Policy(_) => write!(f, "local policy violation"),
}
}
}
impl Error for VerificationError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Overflow
| Self::NoKeyFound
| Self::InvalidKeyDomain
| Self::Timeout
| Self::KeyLookup
| Self::WrongKeyType
| Self::KeyRevoked
| Self::DisallowedHashAlgorithm
| Self::DomainMismatch
| Self::InsufficientContent
| Self::BodyHashMismatch => None,
Self::DkimSignatureFormat(error) => Some(error),
Self::KeyRecordFormat(error) => Some(error),
Self::VerificationFailure(error) => Some(error),
Self::Policy(error) => Some(error),
}
}
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub enum DkimResult {
None,
Pass,
Fail,
Policy,
Neutral,
Temperror,
Permerror,
}
impl CanonicalStr for DkimResult {
fn canonical_str(&self) -> &'static str {
match self {
Self::None => "none",
Self::Pass => "pass",
Self::Fail => "fail",
Self::Policy => "policy",
Self::Neutral => "neutral",
Self::Temperror => "temperror",
Self::Permerror => "permerror",
}
}
}
impl Display for DkimResult {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.canonical_str())
}
}
impl fmt::Debug for DkimResult {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}
struct VerifierTask {
status: VerificationStatus,
index: usize,
signature: Option<DkimSignature>,
key_record: Option<Arc<DkimKeyRecord>>,
}
pub struct Verifier {
tasks: Vec<VerifierTask>, body_hasher: BodyHasher,
}
impl Verifier {
pub async fn verify_header<T>(
resolver: &T,
headers: &HeaderFields,
config: &Config,
) -> Option<Self>
where
T: LookupTxt + Clone + 'static,
{
let verifier = HeaderVerifier::find_signatures(headers, config)?;
let verified_tasks = verifier.verify_all(resolver).await;
let mut tasks = vec![];
let mut body_hasher = BodyHasherBuilder::new(config.forbid_unsigned_content);
for task in verified_tasks {
let status = match task.status {
VerifyStatus::InProgress => panic!("verification unexpectedly skipped"),
VerifyStatus::Failed(e) => VerificationStatus::Failure(e),
VerifyStatus::Successful => {
let sig = task.signature.as_ref().unwrap();
let (body_len, hash_alg, canon_alg) = body_hasher_key(sig);
body_hasher.register_canonicalization(body_len, hash_alg, canon_alg);
VerificationStatus::Success
}
};
tasks.push(VerifierTask {
status,
index: task.index,
signature: task.signature,
key_record: task.key_record,
});
}
let body_hasher = body_hasher.build();
Some(Self { tasks, body_hasher })
}
pub fn process_body_chunk(&mut self, chunk: &[u8]) -> BodyHasherStance {
self.body_hasher.hash_chunk(chunk)
}
pub fn finish(self) -> Vec<VerificationResult> {
let bh_results = self.body_hasher.finish();
let mut result = vec![];
for task in self.tasks {
let final_status = match task.status {
VerificationStatus::Success => {
let sig = task.signature.as_ref()
.expect("successful verification missing signature");
verify_body_hash(sig, &bh_results)
}
status @ VerificationStatus::Failure(_) => status,
};
result.push(VerificationResult {
status: final_status,
index: task.index,
signature: task.signature,
key_record: task.key_record,
});
}
result
}
}
fn verify_body_hash(sig: &DkimSignature, bh_results: &BodyHashResults) -> VerificationStatus {
trace!(domain = %sig.domain, selector = %sig.selector, "checking body hash for signature");
let key = body_hasher_key(sig);
let bh_result = bh_results.get(&key)
.expect("requested body hash result not available");
match bh_result {
Ok((h, _)) => {
if h == &sig.body_hash {
trace!("body hash matched");
VerificationStatus::Success
} else {
trace!("body hash did not match");
VerificationStatus::Failure(VerificationError::BodyHashMismatch)
}
}
Err(BodyHashError::InsufficientInput) => {
trace!("insufficient message body content for body hash");
VerificationStatus::Failure(VerificationError::InsufficientContent)
}
Err(BodyHashError::InputTruncated) => {
trace!("unsigned content in message body not allowed due to local policy");
VerificationStatus::Failure(VerificationError::Policy(PolicyError::UnsignedContent))
}
}
}
pub async fn verify<T>(
resolver: &T,
header: &HeaderFields,
body: &[u8],
config: &Config,
) -> Vec<VerificationResult>
where
T: LookupTxt + Clone + 'static,
{
let mut verifier = match Verifier::verify_header(resolver, header, config).await {
Some(verifier) => verifier,
None => return vec![],
};
let _ = verifier.process_body_chunk(body);
verifier.finish()
}