Skip to main content

mail_auth/dkim/
mod.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7//! DKIM1 signing and verification.
8//!
9//! Implements DomainKeys Identified Mail as specified by:
10//!
11//! - RFC 6376: DomainKeys Identified Mail (DKIM) Signatures.
12//! - RFC 8301: Cryptographic Algorithm and Key Usage Update to DKIM.
13//! - RFC 8463: A New Cryptographic Signature Method for DKIM (Ed25519-SHA256).
14//! - RFC 6651: Extensions to DKIM for Failure Reporting (`r=` tag and
15//!   `_report._domainkey` records).
16//! - RFC 6541: DKIM Authorized Third-Party Signatures (ATPS).
17//! - RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA).
18//! - RFC 5672: DKIM Signatures, Update.
19//! - RFC 4686, RFC 5016, RFC 5585, RFC 5863 and RFC 6377: threat analysis,
20//!   requirements, service overview, operations and mailing list guidance.
21//!
22//! Messages are signed with [`DkimSigner`], which produces a [`Signature`]
23//! that is written as a `DKIM-Signature` header. Messages are verified with
24//! [`MessageAuthenticator::verify_dkim`](crate::MessageAuthenticator::verify_dkim),
25//! which returns one [`DkimOutput`] per signature.
26
27use crate::{
28    crypto::{Algorithm, HashAlgorithm, SigningKey},
29    signer::NeedDomain,
30};
31
32pub mod builder;
33pub mod canonicalize;
34#[cfg(feature = "generate")]
35pub mod generate;
36pub mod headers;
37pub mod output;
38pub mod parse;
39pub mod record;
40pub mod sign;
41pub mod streaming;
42pub mod verify;
43
44pub use output::{DkimOutput, DkimResult};
45pub use record::{AtpsRecord, DkimReportRecord, DomainKey, VerifySignature};
46pub(crate) use record::{
47    Flag, RR_DNS, RR_EXPIRATION, RR_OTHER, RR_POLICY, RR_SIGNATURE, RR_UNKNOWN_TAG,
48    RR_VERIFICATION, Service,
49};
50pub use streaming::DkimSigningStream;
51
52/// A DKIM canonicalization algorithm (RFC 6376, Section 3.4).
53///
54/// Selected separately for the header and the body through the `c=` tag of
55/// a `DKIM-Signature` header (`c=<header>/<body>`). Signers default to
56/// `relaxed`; when `c=` is absent from a parsed signature, `simple` applies.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58pub enum Canonicalization {
59    /// `relaxed` (RFC 6376, Sections 3.4.2 and 3.4.4): tolerates common
60    /// whitespace changes and header name case changes.
61    #[default]
62    Relaxed,
63    /// `simple` (RFC 6376, Sections 3.4.1 and 3.4.3): tolerates almost no
64    /// modification; only trailing empty body lines are ignored.
65    Simple,
66}
67
68/// A DKIM1 specific failure, carried by [`Error::Dkim`](crate::Error::Dkim).
69///
70/// Produced while parsing a `DKIM-Signature` header or a `_domainkey` key
71/// record, and while verifying a signature.
72#[derive(Debug, PartialEq, Eq, Clone)]
73pub enum DkimError {
74    /// The `v=` tag of the signature is not `1` (RFC 6376, Section 3.5).
75    UnsupportedVersion,
76    /// The `a=` tag names an algorithm this crate does not implement
77    /// (supported: `rsa-sha256`, `rsa-sha1` and `ed25519-sha256`).
78    UnsupportedAlgorithm,
79    /// The `c=` tag names an unknown canonicalization algorithm.
80    UnsupportedCanonicalization,
81    /// The `k=` tag of the key record names an unknown key type (supported:
82    /// `rsa` and `ed25519`).
83    UnsupportedKeyType,
84    /// The body hash computed from the message does not match the `bh=` tag
85    /// (RFC 6376, Section 6.1.3).
86    BodyHashMismatch,
87    /// The domain of the `i=` tag is not `d=` or a subdomain of it, or the key
88    /// record has `t=s` and the domains are not identical (RFC 6376,
89    /// Section 3.5).
90    AuidMismatch,
91    /// The key record has an empty `p=` tag, meaning the key was revoked
92    /// (RFC 6376, Section 3.6.1).
93    PublicKeyRevoked,
94    /// The `x=` expiration time has passed, or is not later than `t=`
95    /// (RFC 6376, Section 3.5).
96    SignatureExpired,
97    /// An `l=` body length tag was present and strict parsing ignores such
98    /// signatures, since content can be appended after the signed length.
99    BodyLengthTag,
100}
101
102impl std::fmt::Display for DkimError {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        match self {
105            DkimError::UnsupportedVersion => write!(f, "Unsupported version in DKIM Signature"),
106            DkimError::UnsupportedAlgorithm => {
107                write!(f, "Unsupported algorithm in DKIM Signature")
108            }
109            DkimError::UnsupportedCanonicalization => {
110                write!(f, "Unsupported canonicalization method in DKIM Signature")
111            }
112            DkimError::UnsupportedKeyType => write!(f, "Unsupported key type in DKIM DNS record"),
113            DkimError::BodyHashMismatch => {
114                write!(f, "Calculated body hash does not match signature hash")
115            }
116            DkimError::AuidMismatch => write!(f, "AUID does not match domain name"),
117            DkimError::PublicKeyRevoked => {
118                write!(f, "Public key for this signature has been revoked")
119            }
120            DkimError::SignatureExpired => write!(f, "Signature expired"),
121            DkimError::BodyLengthTag => write!(f, "Insecure 'l=' tag found in Signature"),
122        }
123    }
124}
125
126/// A DKIM1 message signer (RFC 6376, Section 5).
127///
128/// Built from a signing key with [`DkimSigner::from_key`], then configured
129/// in a fixed order that the type state enforces:
130/// `DkimSigner::from_key(key).domain(..).selector(..).headers(..)` yields a
131/// `DkimSigner<_, signer::Ready>`. Only a `Ready` signer can sign, and only a
132/// `Ready` signer accepts the optional settings (`atps`, `atps_hash`,
133/// `identity`, `expiration`, `body_length`, `reporting`,
134/// `header_canonicalization`, `body_canonicalization`).
135///
136/// Defaults: `relaxed/relaxed` canonicalization, no expiration (`x=`
137/// omitted), no body length (`l=` omitted), no failure reports requested
138/// (`r=` omitted), no ATPS and no `i=` tag. The algorithm is taken from the
139/// key.
140///
141/// Signing returns a [`Signature`]; write it in front of the message as a
142/// `DKIM-Signature` header with
143/// [`HeaderWriter`](crate::headers::HeaderWriter).
144///
145/// # Example
146///
147/// ```rust,no_run
148/// use mail_auth::{crypto::Ed25519Key, dkim::DkimSigner, headers::HeaderWriter};
149///
150/// # fn main() -> mail_auth::Result<()> {
151/// # let pkcs8_der: Vec<u8> = Vec::new();
152/// let message = b"From: bill@example.com\r\nTo: jdoe@example.com\r\n\
153///     Subject: TPS Report\r\n\r\nHello.\r\n";
154/// let key = Ed25519Key::from_pkcs8_der(&pkcs8_der)?;
155/// let signature = DkimSigner::from_key(key)
156///     .domain("example.com")
157///     .selector("ed")
158///     .headers(["From", "To", "Subject"])
159///     .sign(message)?;
160/// let signed = format!(
161///     "{}{}",
162///     signature.to_header(),
163///     std::str::from_utf8(message).unwrap_or_default()
164/// );
165/// # Ok(())
166/// # }
167/// ```
168#[derive(Debug, PartialEq, Eq, Clone)]
169pub struct DkimSigner<T: SigningKey, State = NeedDomain> {
170    _state: std::marker::PhantomData<State>,
171    pub(crate) key: T,
172    pub(crate) template: Signature,
173}
174
175impl<T: SigningKey, State> DkimSigner<T, State> {
176    /// Returns the signature template configured so far.
177    ///
178    /// The template holds the tags that will be copied into every produced
179    /// signature. Before signing, `x` holds the validity period in seconds
180    /// (not an absolute time) and `l` is `1` when body length signing is
181    /// enabled; both are resolved when a message is signed.
182    pub fn template(&self) -> &Signature {
183        &self.template
184    }
185}
186
187/// A DKIM1 signature: the tag list of a `DKIM-Signature` header (RFC 6376,
188/// Section 3.5).
189///
190/// Obtained by parsing a header value with [`Signature::parse`], from a
191/// verified message through [`DkimOutput::signature`], or as the result of
192/// signing with [`DkimSigner`]. It is written back as a header through
193/// [`HeaderWriter`](crate::headers::HeaderWriter) or [`Signature::write`].
194/// Each field is named after its tag.
195#[derive(Debug, PartialEq, Eq, Clone, Default)]
196pub struct Signature {
197    /// `v=`: signature version; `1`, or `0` when the tag is absent from a
198    /// parsed header.
199    pub v: u32,
200    /// `a=`: signing algorithm (RFC 6376, RFC 8301, RFC 8463).
201    pub a: Algorithm,
202    /// `d=`: signing domain identifier (SDID), the domain claiming
203    /// responsibility for the message.
204    pub d: String,
205    /// `s=`: selector; the key record is looked up at
206    /// `<s>._domainkey.<d>`.
207    pub s: String,
208    /// `b=`: signature data, base64 decoded.
209    pub b: Vec<u8>,
210    /// `bh=`: hash of the canonicalized body, base64 decoded.
211    pub bh: Vec<u8>,
212    /// `h=`: names of the signed header fields, in signing order.
213    pub h: Vec<String>,
214    /// `z=`: copied header fields, as `name:value` strings with quoted
215    /// printable decoded (diagnostic only).
216    pub z: Vec<String>,
217    /// `i=`: agent or user identifier (AUID); empty when absent.
218    pub i: String,
219    /// `l=`: body length in octets covered by the body hash; `0` means the
220    /// tag is absent and the whole body is signed.
221    pub l: u64,
222    /// `x=`: signature expiration, in seconds since the UNIX epoch; `0`
223    /// means no expiration.
224    pub x: u64,
225    /// `t=`: signing time, in seconds since the UNIX epoch; `0` when absent.
226    pub t: u64,
227    /// `r=y`: the signer requests failure reports (RFC 6651, Section 3.1).
228    pub r: bool,
229    /// `atps=`: the domain authorizing this third-party signature (RFC 6541,
230    /// Section 4.1).
231    pub atps: Option<String>,
232    /// `atpsh=`: hash algorithm applied to `d=` when building the ATPS query
233    /// name; `None` means `atpsh=none`, where `d=` is used verbatim
234    /// (RFC 6541, Section 4.1).
235    pub atpsh: Option<HashAlgorithm>,
236    /// Header canonicalization, the first half of the `c=` tag.
237    pub ch: Canonicalization,
238    /// Body canonicalization, the second half of the `c=` tag.
239    pub cb: Canonicalization,
240}
241
242impl From<HashAlgorithm> for u64 {
243    fn from(v: HashAlgorithm) -> Self {
244        v as u64
245    }
246}
247
248impl From<Algorithm> for HashAlgorithm {
249    fn from(a: Algorithm) -> Self {
250        match a {
251            Algorithm::RsaSha256 | Algorithm::Ed25519Sha256 | Algorithm::MlDsa44 => {
252                HashAlgorithm::Sha256
253            }
254            Algorithm::RsaSha1 => HashAlgorithm::Sha1,
255        }
256    }
257}
258
259impl VerifySignature for Signature {
260    fn signature(&self) -> &[u8] {
261        &self.b
262    }
263
264    fn algorithm(&self) -> Algorithm {
265        self.a
266    }
267
268    fn selector(&self) -> &str {
269        &self.s
270    }
271
272    fn domain(&self) -> &str {
273        &self.d
274    }
275}
276
277impl Signature {
278    /// Returns the agent or user identifier (the `i=` tag), or an empty
279    /// string when the tag is absent.
280    pub fn identity(&self) -> &str {
281        &self.i
282    }
283}