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