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}