Skip to main content

mail_auth/common/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
7use super::headers::{Writable, Writer};
8use crate::{Result, dkim::Canonicalization};
9
10#[derive(Debug, PartialEq, Eq, Clone)]
11pub enum CryptoError {
12    Library(String),
13    FailedVerification,
14    IncompatibleAlgorithms,
15}
16
17impl std::fmt::Display for CryptoError {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        match self {
20            CryptoError::Library(err) => write!(f, "Cryptography layer error: {err}"),
21            CryptoError::FailedVerification => write!(f, "Signature verification failed"),
22            CryptoError::IncompatibleAlgorithms => write!(
23                f,
24                "Incompatible algorithms used in signature and DKIM DNS record"
25            ),
26        }
27    }
28}
29
30#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
31mod ring_impls;
32#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
33pub use ring_impls::{Ed25519Key, RsaKey};
34#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
35pub(crate) use ring_impls::{Ed25519PublicKey, RsaPublicKey};
36
37#[cfg(all(
38    feature = "rust-crypto",
39    not(any(feature = "ring", feature = "aws-lc-rs"))
40))]
41mod rust_crypto;
42#[cfg(all(
43    feature = "rust-crypto",
44    not(any(feature = "ring", feature = "aws-lc-rs"))
45))]
46pub use rust_crypto::{Ed25519Key, RsaKey};
47#[cfg(all(
48    feature = "rust-crypto",
49    not(any(feature = "ring", feature = "aws-lc-rs"))
50))]
51pub(crate) use rust_crypto::{Ed25519PublicKey, RsaPublicKey};
52
53pub trait SigningKey {
54    type Hasher: HashImpl;
55
56    fn sign(&self, input: impl Writable) -> Result<Vec<u8>>;
57
58    fn hash(&self, data: impl Writable) -> HashOutput {
59        let mut hasher = <Self::Hasher as HashImpl>::hasher();
60        data.write(&mut hasher);
61        hasher.complete()
62    }
63
64    fn algorithm(&self) -> Algorithm;
65}
66
67/// A concrete DKIM signing key, holding either an RSA or an Ed25519 key.
68#[cfg(any(feature = "ring", feature = "aws-lc-rs", feature = "rust-crypto"))]
69pub enum DkimKey {
70    Rsa(RsaKey<Sha256>),
71    Ed25519(Ed25519Key),
72}
73
74#[cfg(any(feature = "ring", feature = "aws-lc-rs", feature = "rust-crypto"))]
75impl SigningKey for DkimKey {
76    type Hasher = Sha256;
77
78    fn sign(&self, input: impl Writable) -> Result<Vec<u8>> {
79        match self {
80            DkimKey::Rsa(key) => key.sign(input),
81            DkimKey::Ed25519(key) => key.sign(input),
82        }
83    }
84
85    fn algorithm(&self) -> Algorithm {
86        match self {
87            DkimKey::Rsa(key) => key.algorithm(),
88            DkimKey::Ed25519(key) => key.algorithm(),
89        }
90    }
91}
92
93#[cfg(any(feature = "ring", feature = "aws-lc-rs", feature = "rust-crypto"))]
94impl From<RsaKey<Sha256>> for DkimKey {
95    fn from(key: RsaKey<Sha256>) -> Self {
96        DkimKey::Rsa(key)
97    }
98}
99
100#[cfg(any(feature = "ring", feature = "aws-lc-rs", feature = "rust-crypto"))]
101impl From<Ed25519Key> for DkimKey {
102    fn from(key: Ed25519Key) -> Self {
103        DkimKey::Ed25519(key)
104    }
105}
106
107pub trait VerifyingKey {
108    fn verify<'a>(
109        &self,
110        headers: &mut dyn Iterator<Item = (&'a [u8], &'a [u8])>,
111        signature: &[u8],
112        canonicalization: Canonicalization,
113        algorithm: Algorithm,
114    ) -> Result<()>;
115
116    /// Verifies a signature computed over an already-canonicalized byte string
117    fn verify_bytes(&self, input: &[u8], signature: &[u8], algorithm: Algorithm) -> Result<()>;
118
119    /// Size in bits of the public key.
120    fn public_key_bits(&self) -> usize {
121        usize::MAX
122    }
123}
124
125pub(crate) enum VerifyingKeyType {
126    Rsa,
127    Ed25519,
128}
129
130impl VerifyingKeyType {
131    pub(crate) fn verifying_key(
132        &self,
133        bytes: &[u8],
134    ) -> Result<Box<dyn VerifyingKey + Send + Sync>> {
135        match self {
136            #[cfg(any(feature = "ring", feature = "aws-lc-rs", feature = "rust-crypto"))]
137            Self::Rsa => RsaPublicKey::verifying_key_from_bytes(bytes),
138            #[cfg(any(feature = "ring", feature = "aws-lc-rs", feature = "rust-crypto"))]
139            Self::Ed25519 => Ed25519PublicKey::verifying_key_from_bytes(bytes),
140        }
141    }
142}
143
144pub trait HashContext: Writer + Sized {
145    fn complete(self) -> HashOutput;
146}
147
148pub trait HashImpl {
149    type Context: HashContext;
150
151    fn hasher() -> Self::Context;
152}
153
154#[derive(Clone, Copy)]
155pub struct Sha1;
156
157#[derive(Clone, Copy)]
158pub struct Sha256;
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161#[repr(u64)]
162pub enum HashAlgorithm {
163    Sha1 = R_HASH_SHA1,
164    Sha256 = R_HASH_SHA256,
165}
166
167#[cfg(feature = "aws-lc-rs")]
168use aws_lc_rs as crypto_backend;
169#[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
170use ring as crypto_backend;
171
172#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
173impl HashAlgorithm {
174    pub fn hash(&self, data: impl Writable) -> HashOutput {
175        match self {
176            Self::Sha1 => {
177                let mut hasher = crypto_backend::digest::Context::new(
178                    &crypto_backend::digest::SHA1_FOR_LEGACY_USE_ONLY,
179                );
180                data.write(&mut hasher);
181                HashOutput::Digest(hasher.finish())
182            }
183            Self::Sha256 => {
184                let mut hasher =
185                    crypto_backend::digest::Context::new(&crypto_backend::digest::SHA256);
186                data.write(&mut hasher);
187                HashOutput::Digest(hasher.finish())
188            }
189        }
190    }
191}
192
193#[cfg(all(
194    feature = "rust-crypto",
195    not(any(feature = "ring", feature = "aws-lc-rs"))
196))]
197impl HashAlgorithm {
198    pub fn hash(&self, data: impl Writable) -> HashOutput {
199        use sha2::Digest as _;
200        match self {
201            Self::Sha1 => {
202                let mut hasher = sha1::Sha1::new();
203                data.write(&mut hasher);
204                HashOutput::RustCryptoSha1(hasher.finalize())
205            }
206            Self::Sha256 => {
207                let mut hasher = sha2::Sha256::new();
208                data.write(&mut hasher);
209                HashOutput::RustCryptoSha256(hasher.finalize())
210            }
211        }
212    }
213}
214
215impl HashAlgorithm {
216    pub fn parse(name: &str) -> Option<Self> {
217        if name.eq_ignore_ascii_case("sha256") {
218            Some(HashAlgorithm::Sha256)
219        } else if name.eq_ignore_ascii_case("sha1") {
220            Some(HashAlgorithm::Sha1)
221        } else {
222            None
223        }
224    }
225
226    pub fn name(&self) -> &'static str {
227        match self {
228            HashAlgorithm::Sha1 => "sha1",
229            HashAlgorithm::Sha256 => "sha256",
230        }
231    }
232}
233
234#[derive(Clone)]
235#[non_exhaustive]
236pub enum HashOutput {
237    #[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
238    Digest(crypto_backend::digest::Digest),
239    #[cfg(all(
240        feature = "rust-crypto",
241        not(any(feature = "ring", feature = "aws-lc-rs"))
242    ))]
243    RustCryptoSha1(sha1::digest::Output<sha1::Sha1>),
244    #[cfg(all(
245        feature = "rust-crypto",
246        not(any(feature = "ring", feature = "aws-lc-rs"))
247    ))]
248    RustCryptoSha256(sha2::digest::Output<sha2::Sha256>),
249}
250
251impl AsRef<[u8]> for HashOutput {
252    fn as_ref(&self) -> &[u8] {
253        match self {
254            #[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
255            Self::Digest(output) => output.as_ref(),
256            #[cfg(all(
257                feature = "rust-crypto",
258                not(any(feature = "ring", feature = "aws-lc-rs"))
259            ))]
260            Self::RustCryptoSha1(output) => output.as_ref(),
261            #[cfg(all(
262                feature = "rust-crypto",
263                not(any(feature = "ring", feature = "aws-lc-rs"))
264            ))]
265            Self::RustCryptoSha256(output) => output.as_ref(),
266        }
267    }
268}
269
270impl PartialEq for HashOutput {
271    fn eq(&self, other: &Self) -> bool {
272        self.as_ref() == other.as_ref()
273    }
274}
275
276impl Eq for HashOutput {}
277
278impl std::fmt::Debug for HashOutput {
279    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280        self.as_ref().fmt(f)
281    }
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
285#[repr(u8)]
286pub enum Algorithm {
287    RsaSha1,
288    #[default]
289    RsaSha256,
290    Ed25519Sha256,
291}
292
293pub(crate) const R_HASH_SHA1: u64 = 0x01;
294pub(crate) const R_HASH_SHA256: u64 = 0x02;
295
296impl Algorithm {
297    pub fn parse(name: &[u8]) -> Option<Self> {
298        hashify::tiny_map_ignore_case!(name,
299            b"rsa-sha1" => Algorithm::RsaSha1,
300            b"rsa-sha256" => Algorithm::RsaSha256,
301            b"ed25519-sha256" => Algorithm::Ed25519Sha256,
302        )
303    }
304
305    pub fn name(&self) -> &'static str {
306        match self {
307            Algorithm::RsaSha1 => "rsa-sha1",
308            Algorithm::RsaSha256 => "rsa-sha256",
309            Algorithm::Ed25519Sha256 => "ed25519-sha256",
310        }
311    }
312}