Skip to main content

mail_auth/crypto/
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//! Signing keys, verification keys and hash functions.
8//!
9//! Implements the signature algorithms used by DKIM (RFC 6376 Section 3.3),
10//! ARC (RFC 8617) and DKIM2 (draft-ietf-dkim-dkim2-spec): `rsa-sha256`,
11//! `rsa-sha1` (verification only; deprecated by RFC 8301), `ed25519-sha256`
12//! (RFC 8463, built on the EdDSA scheme of RFC 8032) and, for DKIM2 only,
13//! the post-quantum composite `mldsa44-ed25519-sha512`
14//! (draft-ietf-lamps-pq-composite-sigs).
15//!
16//! The backend is chosen at compile time: `aws-lc-rs`, or `rust-crypto` (the
17//! pure Rust `rsa`, `ed25519-dalek`, `ml-dsa`, `sha1` and `sha2` crates) when
18//! `aws-lc-rs` is not enabled. [`RsaKey`], [`Ed25519Key`] and
19//! [`MlDsa44Ed25519Key`] have the same interface on every backend.
20
21use super::headers::{Writable, Writer};
22use crate::{Result, dkim::Canonicalization};
23
24#[cfg(any(feature = "aws-lc-rs", feature = "rust-crypto"))]
25pub(crate) mod composite;
26
27/// A failure reported by the cryptography layer, carried by
28/// [`Error::Crypto`](crate::Error::Crypto).
29#[derive(Debug, PartialEq, Eq, Clone)]
30pub enum CryptoError {
31    /// The backend rejected an operation (malformed or unsupported key,
32    /// signing failure). The string holds the backend's error message.
33    Library(String),
34    /// The signature does not match the signed data.
35    FailedVerification,
36    /// The signature algorithm does not match the public key type (for
37    /// example, an `ed25519-sha256` signature checked against an RSA key
38    /// published with `k=rsa`).
39    IncompatibleAlgorithms,
40}
41
42impl std::fmt::Display for CryptoError {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            CryptoError::Library(err) => write!(f, "Cryptography layer error: {err}"),
46            CryptoError::FailedVerification => write!(f, "Signature verification failed"),
47            CryptoError::IncompatibleAlgorithms => write!(
48                f,
49                "Incompatible algorithms used in signature and DKIM DNS record"
50            ),
51        }
52    }
53}
54
55#[cfg(feature = "aws-lc-rs")]
56mod aws_lc;
57#[cfg(feature = "aws-lc-rs")]
58mod aws_lc_mldsa;
59#[cfg(feature = "aws-lc-rs")]
60pub use aws_lc::{Ed25519Key, MlDsa44Ed25519Key, RsaKey};
61#[cfg(feature = "aws-lc-rs")]
62pub(crate) use aws_lc::{Ed25519PublicKey, MlDsa44Ed25519PublicKey, RsaPublicKey};
63
64#[cfg(all(feature = "rust-crypto", not(feature = "aws-lc-rs")))]
65mod rust_crypto;
66#[cfg(all(feature = "rust-crypto", not(feature = "aws-lc-rs")))]
67pub use rust_crypto::{Ed25519Key, MlDsa44Ed25519Key, RsaKey};
68#[cfg(all(feature = "rust-crypto", not(feature = "aws-lc-rs")))]
69pub(crate) use rust_crypto::{Ed25519PublicKey, MlDsa44Ed25519PublicKey, RsaPublicKey};
70
71/// A private key that can produce DKIM, ARC and DKIM2 signatures.
72///
73/// Implemented by [`RsaKey<Sha256>`](RsaKey), [`Ed25519Key`],
74/// [`MlDsa44Ed25519Key`] and [`DkimKey`]. Signers such as
75/// [`DkimSigner`](crate::dkim::DkimSigner) are generic over this trait.
76pub trait SigningKey {
77    /// The hash function used by the signature algorithm.
78    type Hasher: HashImpl;
79
80    /// Signs the bytes written by `input` and returns the raw signature.
81    ///
82    /// # Errors
83    ///
84    /// Returns [`Error::Crypto`](crate::Error::Crypto) with
85    /// [`CryptoError::Library`] if the backend fails to sign.
86    fn sign(&self, input: impl Writable) -> Result<Vec<u8>>;
87
88    /// Hashes the bytes written by `data` with [`Self::Hasher`].
89    fn hash(&self, data: impl Writable) -> HashOutput {
90        let mut hasher = <Self::Hasher as HashImpl>::hasher();
91        data.write(&mut hasher);
92        hasher.complete()
93    }
94
95    /// Returns the signature algorithm written to the `a=` tag.
96    fn algorithm(&self) -> Algorithm;
97
98    /// Approximate heap memory owned by the key, in bytes, for weighing
99    /// cache entries that hold it.
100    fn heap_size(&self) -> usize {
101        0
102    }
103}
104
105/// A concrete DKIM signing key, holding an RSA, an Ed25519 or an
106/// ML-DSA-44/Ed25519 composite key.
107///
108/// Lets a single signer type hold keys of any algorithm.
109/// [`Dkim2Signer`](crate::dkim2::Dkim2Signer) requires it. Every key type
110/// converts into it with `From`. The composite variant is defined for DKIM2
111/// only; the DKIM and ARC signers reject it.
112#[cfg(any(feature = "aws-lc-rs", feature = "rust-crypto"))]
113pub enum DkimKey {
114    /// An RSA key producing `rsa-sha256` signatures.
115    Rsa(RsaKey<Sha256>),
116    /// An Ed25519 key producing `ed25519-sha256` signatures (RFC 8463).
117    Ed25519(Ed25519Key),
118    /// An ML-DSA-44/Ed25519 composite key producing `mldsa44-ed25519-sha512`
119    /// signatures (DKIM2 only).
120    MlDsa44Ed25519(Box<MlDsa44Ed25519Key>),
121}
122
123#[cfg(any(feature = "aws-lc-rs", feature = "rust-crypto"))]
124impl SigningKey for DkimKey {
125    type Hasher = Sha256;
126
127    fn sign(&self, input: impl Writable) -> Result<Vec<u8>> {
128        match self {
129            DkimKey::Rsa(key) => key.sign(input),
130            DkimKey::Ed25519(key) => key.sign(input),
131            DkimKey::MlDsa44Ed25519(key) => key.sign(input),
132        }
133    }
134
135    fn algorithm(&self) -> Algorithm {
136        match self {
137            DkimKey::Rsa(key) => key.algorithm(),
138            DkimKey::Ed25519(key) => key.algorithm(),
139            DkimKey::MlDsa44Ed25519(key) => key.algorithm(),
140        }
141    }
142
143    fn heap_size(&self) -> usize {
144        match self {
145            DkimKey::Rsa(key) => key.heap_size(),
146            DkimKey::Ed25519(key) => key.heap_size(),
147            DkimKey::MlDsa44Ed25519(key) => size_of::<MlDsa44Ed25519Key>() + key.heap_size(),
148        }
149    }
150}
151
152#[cfg(any(feature = "aws-lc-rs", feature = "rust-crypto"))]
153pub(crate) const fn rsa_private_key_heap_size(modulus_len: usize) -> usize {
154    const BASE: usize = 768;
155    const PER_MODULUS_BYTE: usize = 8;
156    BASE + PER_MODULUS_BYTE * modulus_len
157}
158
159#[cfg(any(feature = "aws-lc-rs", feature = "rust-crypto"))]
160impl From<RsaKey<Sha256>> for DkimKey {
161    fn from(key: RsaKey<Sha256>) -> Self {
162        DkimKey::Rsa(key)
163    }
164}
165
166#[cfg(any(feature = "aws-lc-rs", feature = "rust-crypto"))]
167impl From<Ed25519Key> for DkimKey {
168    fn from(key: Ed25519Key) -> Self {
169        DkimKey::Ed25519(key)
170    }
171}
172
173#[cfg(any(feature = "aws-lc-rs", feature = "rust-crypto"))]
174impl From<MlDsa44Ed25519Key> for DkimKey {
175    fn from(key: MlDsa44Ed25519Key) -> Self {
176        DkimKey::MlDsa44Ed25519(Box::new(key))
177    }
178}
179
180/// A public key that can verify signatures, parsed from the `p=` tag of a
181/// DKIM key record (RFC 6376 Section 3.6.1).
182pub trait VerifyingKey {
183    /// Canonicalizes `headers` with `canonicalization` and verifies
184    /// `signature` over the result, using `algorithm`.
185    ///
186    /// # Errors
187    ///
188    /// Returns [`Error::Crypto`](crate::Error::Crypto) with
189    /// [`CryptoError::IncompatibleAlgorithms`] if `algorithm` does not match
190    /// the key type, or with [`CryptoError::FailedVerification`] or
191    /// [`CryptoError::Library`] if the signature does not verify.
192    fn verify<'a>(
193        &self,
194        headers: &mut dyn Iterator<Item = (&'a [u8], &'a [u8])>,
195        signature: &[u8],
196        canonicalization: Canonicalization,
197        algorithm: Algorithm,
198    ) -> Result<()>;
199
200    /// Verifies `signature` over `input`, an already canonicalized byte
201    /// string, using `algorithm`.
202    ///
203    /// # Errors
204    ///
205    /// Returns [`Error::Crypto`](crate::Error::Crypto) with
206    /// [`CryptoError::IncompatibleAlgorithms`] if `algorithm` does not match
207    /// the key type, or with [`CryptoError::FailedVerification`] if the
208    /// signature does not verify.
209    fn verify_bytes(&self, input: &[u8], signature: &[u8], algorithm: Algorithm) -> Result<()>;
210
211    /// Size in bits of the public key (the RSA modulus length). The default
212    /// implementation returns `usize::MAX`, which Ed25519 and composite keys
213    /// keep, so that minimum key size checks apply to RSA keys only.
214    fn public_key_bits(&self) -> usize {
215        usize::MAX
216    }
217
218    /// Approximate heap memory owned by the key, in bytes, for weighing
219    /// cache entries that hold it.
220    fn heap_size(&self) -> usize {
221        0
222    }
223}
224
225pub(crate) enum VerifyingKeyType {
226    Rsa,
227    Ed25519,
228    MlDsa44Ed25519,
229}
230
231impl VerifyingKeyType {
232    pub(crate) fn verifying_key(
233        &self,
234        bytes: &[u8],
235    ) -> Result<Box<dyn VerifyingKey + Send + Sync>> {
236        match self {
237            #[cfg(any(feature = "aws-lc-rs", feature = "rust-crypto"))]
238            Self::Rsa => RsaPublicKey::verifying_key_from_bytes(bytes),
239            #[cfg(any(feature = "aws-lc-rs", feature = "rust-crypto"))]
240            Self::Ed25519 => Ed25519PublicKey::verifying_key_from_bytes(bytes),
241            #[cfg(any(feature = "aws-lc-rs", feature = "rust-crypto"))]
242            Self::MlDsa44Ed25519 => MlDsa44Ed25519PublicKey::verifying_key_from_bytes(bytes),
243        }
244    }
245}
246
247/// An incremental hash computation. Data is fed through [`Writer`].
248pub trait HashContext: Writer + Sized {
249    /// Finishes the computation and returns the digest.
250    fn complete(self) -> HashOutput;
251}
252
253/// A hash function, selected at the type level.
254pub trait HashImpl {
255    /// The incremental hashing state for this function.
256    type Context: HashContext;
257
258    /// Starts a new hash computation.
259    fn hasher() -> Self::Context;
260
261    /// Hashes the bytes written by `data`.
262    fn digest(data: impl Writable) -> HashOutput {
263        let mut hasher = Self::hasher();
264        data.write(&mut hasher);
265        hasher.complete()
266    }
267}
268
269/// The SHA-1 hash function, used by the `rsa-sha1` algorithm (RFC 6376
270/// Section 3.3.1). Deprecated by RFC 8301; kept for verifying legacy
271/// signatures.
272#[derive(Clone, Copy)]
273pub struct Sha1;
274
275/// The SHA-256 hash function, used by `rsa-sha256` and `ed25519-sha256`.
276#[derive(Clone, Copy)]
277pub struct Sha256;
278
279/// The SHA-512 hash function, used by `mldsa44-ed25519-sha512` and by DKIM2
280/// `sha512` message hashes.
281#[derive(Clone, Copy)]
282pub struct Sha512;
283
284/// A hash algorithm selected at run time, as named in DKIM tags (the hash
285/// part of `a=`, and the `h=` tag of DKIM key records) and in the `h=` tag
286/// of DKIM2 `Message-Instance` header fields.
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288#[repr(u64)]
289pub enum HashAlgorithm {
290    /// SHA-1 (`sha1`).
291    Sha1 = R_HASH_SHA1,
292    /// SHA-256 (`sha256`).
293    Sha256 = R_HASH_SHA256,
294    /// SHA-512 (`sha512`). Used by DKIM2 only.
295    Sha512 = R_HASH_SHA512,
296}
297
298#[cfg(feature = "aws-lc-rs")]
299impl HashAlgorithm {
300    /// Hashes the bytes written by `data` with this algorithm.
301    pub fn hash(&self, data: impl Writable) -> HashOutput {
302        let mut hasher = aws_lc_rs::digest::Context::new(match self {
303            Self::Sha1 => &aws_lc_rs::digest::SHA1_FOR_LEGACY_USE_ONLY,
304            Self::Sha256 => &aws_lc_rs::digest::SHA256,
305            Self::Sha512 => &aws_lc_rs::digest::SHA512,
306        });
307        data.write(&mut hasher);
308        HashOutput::Digest(hasher.finish())
309    }
310}
311
312#[cfg(all(feature = "rust-crypto", not(feature = "aws-lc-rs")))]
313impl HashAlgorithm {
314    /// Hashes the bytes written by `data` with this algorithm.
315    pub fn hash(&self, data: impl Writable) -> HashOutput {
316        match self {
317            Self::Sha1 => Sha1::digest(data),
318            Self::Sha256 => Sha256::digest(data),
319            Self::Sha512 => Sha512::digest(data),
320        }
321    }
322}
323
324impl HashAlgorithm {
325    /// Parses a DKIM hash algorithm name (`sha1` or `sha256`), ignoring
326    /// ASCII case. Returns `None` for any other name, including `sha512`,
327    /// which DKIM does not define.
328    pub fn parse(name: &str) -> Option<Self> {
329        if name.eq_ignore_ascii_case("sha256") {
330            Some(HashAlgorithm::Sha256)
331        } else if name.eq_ignore_ascii_case("sha1") {
332            Some(HashAlgorithm::Sha1)
333        } else {
334            None
335        }
336    }
337
338    /// Returns the lowercase algorithm name (`sha1`, `sha256` or `sha512`).
339    pub fn name(&self) -> &'static str {
340        match self {
341            HashAlgorithm::Sha1 => "sha1",
342            HashAlgorithm::Sha256 => "sha256",
343            HashAlgorithm::Sha512 => "sha512",
344        }
345    }
346}
347
348/// A hash digest produced by the active cryptography backend.
349///
350/// Read the digest bytes through [`AsRef<[u8]>`](AsRef). Two outputs compare
351/// equal when their bytes are equal, whatever the variant.
352#[derive(Clone)]
353#[non_exhaustive]
354pub enum HashOutput {
355    /// A digest computed by `aws-lc-rs`.
356    #[cfg(feature = "aws-lc-rs")]
357    Digest(aws_lc_rs::digest::Digest),
358    /// A SHA-1 digest computed by the `sha1` crate.
359    #[cfg(all(feature = "rust-crypto", not(feature = "aws-lc-rs")))]
360    RustCryptoSha1(sha1::digest::Output<sha1::Sha1>),
361    /// A SHA-256 digest computed by the `sha2` crate.
362    #[cfg(all(feature = "rust-crypto", not(feature = "aws-lc-rs")))]
363    RustCryptoSha256(sha2::digest::Output<sha2::Sha256>),
364    /// A SHA-512 digest computed by the `sha2` crate.
365    #[cfg(all(feature = "rust-crypto", not(feature = "aws-lc-rs")))]
366    RustCryptoSha512(sha2::digest::Output<sha2::Sha512>),
367}
368
369impl AsRef<[u8]> for HashOutput {
370    fn as_ref(&self) -> &[u8] {
371        match self {
372            #[cfg(feature = "aws-lc-rs")]
373            Self::Digest(output) => output.as_ref(),
374            #[cfg(all(feature = "rust-crypto", not(feature = "aws-lc-rs")))]
375            Self::RustCryptoSha1(output) => output.as_ref(),
376            #[cfg(all(feature = "rust-crypto", not(feature = "aws-lc-rs")))]
377            Self::RustCryptoSha256(output) => output.as_ref(),
378            #[cfg(all(feature = "rust-crypto", not(feature = "aws-lc-rs")))]
379            Self::RustCryptoSha512(output) => output.as_ref(),
380        }
381    }
382}
383
384impl PartialEq for HashOutput {
385    fn eq(&self, other: &Self) -> bool {
386        self.as_ref() == other.as_ref()
387    }
388}
389
390impl Eq for HashOutput {}
391
392impl std::fmt::Debug for HashOutput {
393    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394        self.as_ref().fmt(f)
395    }
396}
397
398/// A signature algorithm, as named in the `a=` tag of DKIM (RFC 6376
399/// Section 3.5) and ARC signatures and in the `s=` tag of DKIM2 signatures.
400#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
401#[repr(u8)]
402pub enum Algorithm {
403    /// `rsa-sha1` (RFC 6376 Section 3.3.1). This crate only verifies it;
404    /// RFC 8301 deprecates it for both signing and verifying.
405    RsaSha1,
406    /// `rsa-sha256` (RFC 6376 Section 3.3.2). This is the default.
407    #[default]
408    RsaSha256,
409    /// `ed25519-sha256` (RFC 8463).
410    Ed25519Sha256,
411    /// `mldsa44-ed25519-sha512`: the composite of ML-DSA-44 (FIPS 204) and
412    /// Ed25519 (RFC 8032) defined as `id-MLDSA44-Ed25519-SHA512` by
413    /// draft-ietf-lamps-pq-composite-sigs, with an empty application
414    /// context. The composite message `M` is the DKIM2 signature input
415    /// (draft-ietf-dkim-dkim2-spec Section 12.8), so its SHA-512 pre-hash is
416    /// the only SHA-512 computation. DKIM2 only.
417    MlDsa44Ed25519Sha512,
418}
419
420pub(crate) const R_HASH_SHA1: u64 = 0x01;
421pub(crate) const R_HASH_SHA256: u64 = 0x02;
422pub(crate) const R_HASH_SHA512: u64 = 0x40;
423
424impl Algorithm {
425    /// Parses a DKIM or ARC algorithm name (`rsa-sha1`, `rsa-sha256` or
426    /// `ed25519-sha256`), ignoring ASCII case. Returns `None` for any other
427    /// name, including the DKIM2-only `mldsa44-ed25519-sha512`.
428    pub fn parse(name: &[u8]) -> Option<Self> {
429        hashify::map_ignore_case!(name, Algorithm,
430            b"rsa-sha1" => Algorithm::RsaSha1,
431            b"rsa-sha256" => Algorithm::RsaSha256,
432            b"ed25519-sha256" => Algorithm::Ed25519Sha256,
433        )
434        .copied()
435    }
436
437    /// Parses a DKIM2 signature algorithm name (`rsa-sha256`,
438    /// `ed25519-sha256` or `mldsa44-ed25519-sha512`). Names are
439    /// case-sensitive (draft-ietf-dkim-dkim2-spec Section 2.14), and
440    /// `rsa-sha1` is not a DKIM2 algorithm.
441    pub fn parse_dkim2(name: &[u8]) -> Option<Self> {
442        hashify::map!(name, Algorithm,
443            b"rsa-sha256" => Algorithm::RsaSha256,
444            b"ed25519-sha256" => Algorithm::Ed25519Sha256,
445            b"mldsa44-ed25519-sha512" => Algorithm::MlDsa44Ed25519Sha512,
446        )
447        .copied()
448    }
449
450    /// Returns the lowercase algorithm name used in the `a=` or `s=` tag.
451    pub fn name(&self) -> &'static str {
452        match self {
453            Algorithm::RsaSha1 => "rsa-sha1",
454            Algorithm::RsaSha256 => "rsa-sha256",
455            Algorithm::Ed25519Sha256 => "ed25519-sha256",
456            Algorithm::MlDsa44Ed25519Sha512 => "mldsa44-ed25519-sha512",
457        }
458    }
459
460    pub(crate) fn is_dkim1(&self) -> bool {
461        !matches!(self, Algorithm::MlDsa44Ed25519Sha512)
462    }
463}