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}