Skip to main content

mail_auth/dkim2/
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//! DomainKeys Identified Mail version 2 (DKIM2).
8//!
9//! Every hop that relays a message adds a `DKIM2-Signature` header field. A
10//! hop that changes the message also adds a `Message-Instance` header field
11//! with hashes of the new content and a recipe that recreates the previous
12//! content. Each signature binds the SMTP envelope (`MAIL FROM` and `RCPT TO`)
13//! used by its hop, or names the domain of the next signer, so a verifier can
14//! follow the chain of custody from the originator to the last hop.
15//!
16//! Implemented specifications:
17//!
18//! - [draft-ietf-dkim-dkim2-spec-07](https://datatracker.ietf.org/doc/draft-ietf-dkim-dkim2-spec/):
19//!   DKIM2 Signatures. Section references (`§`) in this module point to this draft.
20//! - [draft-ietf-dkim-dkim2-dns](https://datatracker.ietf.org/doc/draft-ietf-dkim-dkim2-dns/):
21//!   DKIM2 DNS Records, including the `mldsa44-ed25519-sha512` key type.
22//!
23//! Sign with [`Dkim2Signer`]. Verify a message with
24//! [`MessageAuthenticator::verify_dkim2`](crate::MessageAuthenticator::verify_dkim2)
25//! (or check that it is Provisionally Verified with
26//! [`MessageAuthenticator::verify_dkim2_provisionally`](crate::MessageAuthenticator::verify_dkim2_provisionally))
27//! and an inbound delivery status notification with
28//! [`MessageAuthenticator::verify_dkim2_dsn`](crate::MessageAuthenticator::verify_dkim2_dsn).
29
30use crate::{
31    crypto::{Algorithm, DkimKey, HashAlgorithm},
32    signer::NeedDomain,
33};
34
35pub mod builder;
36pub mod canonicalize;
37pub mod dsn;
38mod error;
39pub mod headers;
40pub mod output;
41pub mod parse;
42pub mod recipe;
43pub mod sign;
44pub mod verify;
45
46#[cfg(test)]
47mod tests;
48
49pub use dsn::{Dkim2Dsn, Dkim2DsnFailure, Dkim2DsnOutput};
50pub use error::Dkim2Error;
51pub use output::{ChainLink, Dkim2Output, Dkim2Result, Verification};
52pub use recipe::{BodyRecipe, HeaderRecipe, Recipe, Step};
53pub use sign::{Dkim2Signed, Envelope, Hop};
54
55/// DKIM2 message signer (§12).
56///
57/// Built in typestate steps. [`Dkim2Signer::from_key`] takes the first
58/// private key, [`domain`](Dkim2Signer::domain) sets the signing domain (`d=`)
59/// and [`selector`](Dkim2Signer::selector) sets the selector of the first key,
60/// which yields a `Dkim2Signer<`[`Ready`](crate::signer::Ready)`>`. Only a
61/// ready signer can sign. Optional settings on a ready signer:
62/// [`additional_key`](Dkim2Signer::additional_key) adds one more signature
63/// under another selector and algorithm, [`hashes`](Dkim2Signer::hashes)
64/// selects the `Message-Instance` hash algorithms, [`flags`](Dkim2Signer::flags)
65/// sets the `f=` tag, [`nonce`](Dkim2Signer::nonce) sets the `n=` tag and
66/// [`out_of_band_arrangement`](Dkim2Signer::out_of_band_arrangement) allows
67/// signing messages that are not Validly Signed DKIM2 Messages.
68///
69/// The signing methods ([`sign`](Dkim2Signer::sign),
70/// [`sign_revised`](Dkim2Signer::sign_revised),
71/// [`sign_with_recipe`](Dkim2Signer::sign_with_recipe) and
72/// [`sign_with_message_instance`](Dkim2Signer::sign_with_message_instance))
73/// return a [`Dkim2Signed`] holding the header fields to prepend to the
74/// message. A message that already carries DKIM2 header fields is signed only
75/// with the [`Dkim2Output`] of a full verification that passed (§12).
76///
77/// # Example
78///
79/// ```rust,no_run
80/// use mail_auth::{
81///     crypto::{Ed25519Key, RsaKey, Sha256},
82///     dkim2::{Dkim2Signer, Flag, Hop},
83/// };
84/// use rustls_pki_types::{PrivateKeyDer, pem::PemObject};
85///
86/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
87/// # let rsa_pem = "";
88/// # let ed25519_pkcs8: &[u8] = &[];
89/// # let message = "From: sender@example.com\r\n\r\nHello\r\n";
90/// let rsa_key = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::from_pem_slice(rsa_pem.as_bytes())?)?;
91/// let signed = Dkim2Signer::from_key(rsa_key)
92///     .domain("example.com")
93///     .selector("rsa-sel")
94///     .additional_key(Ed25519Key::from_pkcs8_der(ed25519_pkcs8)?, "ed-sel")
95///     .flags([Flag::Feedback])
96///     .sign(
97///         message.as_bytes(),
98///         Hop::real("sender@example.com", ["recipient@example.org"]),
99///         None,
100///     )?;
101///
102/// let signed_message = format!("{}{}", signed.to_header(), message);
103/// # Ok(())
104/// # }
105/// ```
106pub struct Dkim2Signer<State = NeedDomain> {
107    _state: std::marker::PhantomData<State>,
108    pub(crate) keys: Vec<KeyEntry>,
109    pub(crate) domain: String,
110    pub(crate) hashes: Vec<Dkim2Hash>,
111    pub(crate) flags: Vec<Flag>,
112    pub(crate) nonce: Option<String>,
113    pub(crate) out_of_band: bool,
114}
115
116pub(crate) struct KeyEntry {
117    pub(crate) key: DkimKey,
118    pub(crate) selector: String,
119}
120
121/// A parsed `DKIM2-Signature` header field (§10).
122///
123/// Produced by [`Signature::parse`] when a message is parsed into an
124/// [`AuthenticatedMessage`](crate::AuthenticatedMessage), and by the
125/// [`Dkim2Signer`] signing methods in [`Dkim2Signed::signature`]. Verified
126/// signatures are exposed through [`ChainLink::signature`].
127#[derive(Debug, PartialEq, Eq, Clone, Default)]
128pub struct Signature {
129    /// `i=`: sequence number of this signature in the chain, starting at 1
130    /// for the originator (§10.1).
131    pub i: u32,
132    /// `m=`: highest `Message-Instance` revision number (`m=`) present when
133    /// this signature was created (§10.2).
134    pub m: u32,
135    /// `t=`: signature creation time, in seconds since the UNIX epoch (§10.4).
136    pub t: u64,
137    /// `d=`: signing domain, used to look up the public key (§10.8).
138    pub d: String,
139    /// `s=`: one signature value per selector and algorithm (§10.9).
140    pub s: Vec<SignatureValue>,
141    /// `mf=` and `rt=` (the SMTP envelope of the hop), or `nd=` (the domain
142    /// of the next signer) (§10.5 to §10.7).
143    pub chain: ChainBinding,
144    /// `n=`: opaque nonce meaningful only to the signer, at most 64
145    /// printable ASCII characters (§10.3).
146    pub n: Option<String>,
147    /// `f=`: flags reporting what the signer did or requesting behaviour
148    /// from later hops (§10.10).
149    pub flags: Vec<Flag>,
150}
151
152/// One entry of the `s=` tag of a `DKIM2-Signature` (§10.9).
153///
154/// A signature carries one entry per signing key, which gives algorithmic
155/// dexterity: each entry names its own selector and algorithm.
156#[derive(Debug, PartialEq, Eq, Clone)]
157pub struct SignatureValue {
158    /// Selector under which the public key is published, queried as
159    /// `<selector>._domainkey.<d>`.
160    pub selector: String,
161    /// Signing algorithm (`rsa-sha256`, `ed25519-sha256` or
162    /// `mldsa44-ed25519-sha512`).
163    pub a: Algorithm,
164    /// Raw signature bytes (base64-decoded).
165    pub b: Vec<u8>,
166}
167
168/// How a `DKIM2-Signature` links to the next hop in the chain of custody
169/// (§10.5 to §10.7, §12.2 and §12.3).
170#[derive(Debug, PartialEq, Eq, Clone)]
171pub enum ChainBinding {
172    /// The SMTP envelope used when the signer sent the message (`mf=` and
173    /// `rt=`). Addresses are RFC 5321 paths including the angle brackets;
174    /// `mail_from` is `<>` for a null reverse-path.
175    Envelope {
176        /// `mf=`: the `MAIL FROM` reverse-path.
177        mail_from: String,
178        /// `rt=`: the `RCPT TO` forward-paths.
179        rcpt_to: Vec<String>,
180    },
181    /// `nd=`: domain of the next `DKIM2-Signature`, used for an imaginary hop
182    /// where the message changes domain without an SMTP transaction.
183    NextDomain(String),
184}
185
186impl Default for ChainBinding {
187    fn default() -> Self {
188        ChainBinding::Envelope {
189            mail_from: String::new(),
190            rcpt_to: Vec::new(),
191        }
192    }
193}
194
195/// A value of the `f=` tag of a `DKIM2-Signature` (§10.10). Flag names are
196/// case-sensitive.
197#[derive(Debug, PartialEq, Eq, Clone)]
198pub enum Flag {
199    /// `feedback`: the signer requests feedback about how the message is
200    /// handled.
201    Feedback,
202    /// `feedhere`: feedback should be relayed through this hop instead of
203    /// being sent directly to the requestor.
204    FeedHere,
205    /// `exploded`: the signer is sending this message to more than one
206    /// recipient (§12.5). Not allowed on the first signature (`i=1`).
207    Exploded,
208    /// Any other flag, kept verbatim. Unknown flags have no effect.
209    Unknown(String),
210}
211
212impl Flag {
213    /// Returns the flag as it appears in the `f=` tag.
214    pub fn as_bytes(&self) -> &[u8] {
215        match self {
216            Flag::Feedback => b"feedback",
217            Flag::FeedHere => b"feedhere",
218            Flag::Exploded => b"exploded",
219            Flag::Unknown(value) => value.as_bytes(),
220        }
221    }
222}
223
224/// A parsed `Message-Instance` header field (§9).
225///
226/// Records the hashes of the message content at one revision and, for a
227/// revision made by a Reviser, the recipe that recreates the previous
228/// revision. Produced by [`MessageInstance::parse`] when a message is parsed,
229/// by [`MessageInstance::from_message`] and [`MessageInstance::from_recipe`],
230/// and by the signers in [`Dkim2Signed::message_instance`].
231#[derive(Debug, PartialEq, Eq, Clone, Default)]
232pub struct MessageInstance {
233    /// `m=`: revision number, starting at 1 for the originator (§9.1).
234    pub m: u32,
235    /// `h=`: header and body hashes of this revision, one entry per hash
236    /// algorithm (§9.3).
237    pub hashes: Vec<MessageHash>,
238    /// `r=`: recipe that recreates the previous revision from this one, or
239    /// `None` if the tag is absent (§9.2).
240    pub recipe: Option<Recipe>,
241}
242
243/// A hash algorithm defined for DKIM2 `Message-Instance` hashes (§3.1).
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub enum Dkim2Hash {
246    /// SHA-256 (`sha256`).
247    Sha256,
248    /// SHA-512 (`sha512`).
249    Sha512,
250}
251
252impl Dkim2Hash {
253    /// Parses a hash name. Names are case-sensitive (§2.14).
254    pub fn parse(name: &[u8]) -> Option<Self> {
255        hashify::map!(name, Dkim2Hash,
256            b"sha256" => Dkim2Hash::Sha256,
257            b"sha512" => Dkim2Hash::Sha512,
258        )
259        .copied()
260    }
261
262    /// Returns the hash name used in the `h=` tag.
263    pub fn name(&self) -> &'static str {
264        self.algorithm().name()
265    }
266
267    /// Returns the matching run-time hash algorithm.
268    pub fn algorithm(&self) -> HashAlgorithm {
269        match self {
270            Dkim2Hash::Sha256 => HashAlgorithm::Sha256,
271            Dkim2Hash::Sha512 => HashAlgorithm::Sha512,
272        }
273    }
274}
275
276/// The hash name of a [`MessageHash`]: an algorithm this crate implements, or
277/// the name of one it does not, which verifiers disregard (§3.5).
278#[derive(Debug, Clone, PartialEq, Eq)]
279pub enum HashName {
280    /// A supported algorithm.
281    Known(Dkim2Hash),
282    /// An unsupported algorithm, kept verbatim.
283    Unknown(Box<str>),
284}
285
286impl HashName {
287    /// Returns the hash name as it appears in the `h=` tag.
288    pub fn as_str(&self) -> &str {
289        match self {
290            HashName::Known(hash) => hash.name(),
291            HashName::Unknown(name) => name,
292        }
293    }
294
295    /// Returns the supported algorithm, or `None` for an unknown name.
296    pub fn known(&self) -> Option<Dkim2Hash> {
297        match self {
298            HashName::Known(hash) => Some(*hash),
299            HashName::Unknown(_) => None,
300        }
301    }
302}
303
304impl From<Dkim2Hash> for HashName {
305    fn from(hash: Dkim2Hash) -> Self {
306        HashName::Known(hash)
307    }
308}
309
310/// One entry of the `h=` tag of a `Message-Instance` (§9.3 and §6).
311#[derive(Debug, PartialEq, Eq, Clone)]
312pub struct MessageHash {
313    /// Hash algorithm name.
314    pub name: HashName,
315    /// Raw digest of the header fields hash (§6.2).
316    pub header_hash: Vec<u8>,
317    /// Raw digest of the body hash (§6.1).
318    pub body_hash: Vec<u8>,
319}
320
321impl Signature {
322    /// Returns the signing domain (`d=`).
323    pub fn domain(&self) -> &str {
324        &self.d
325    }
326}