Skip to main content

stalwart_lib/http/src/management/
dkim.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
5 */
6
7use std::str::FromStr;
8
9use crate::common::{Server, auth::AccessToken, config::smtp::auth::simple_pem_parse};
10use crate::directory::{Permission, backend::internal::manage};
11use crate::store::write::now;
12use hyper::Method;
13use mail_auth::{
14    common::crypto::{Ed25519Key, RsaKey, Sha256},
15    dkim::generate::DkimKeyPair,
16};
17use mail_builder::encoders::base64::base64_encode;
18use mail_parser::DateTime;
19use pkcs8::Document;
20use rsa::pkcs1::DecodeRsaPublicKey;
21use rustls_pki_types::{PrivateKeyDer, PrivatePkcs1KeyDer};
22use serde::{Deserialize, Serialize};
23use serde_json::json;
24
25use crate::http_proto::{request::decode_path_element, *};
26use std::future::Future;
27
28#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)]
29pub enum Algorithm {
30    Rsa,
31    Ed25519,
32}
33
34#[derive(Debug, Serialize, Deserialize)]
35struct DkimSignature {
36    id: Option<String>,
37    algorithm: Algorithm,
38    domain: String,
39    selector: Option<String>,
40}
41
42pub trait DkimManagement: Sync + Send {
43    fn handle_manage_dkim(
44        &self,
45        req: &HttpRequest,
46        path: Vec<&str>,
47        body: Option<Vec<u8>>,
48        access_token: &AccessToken,
49    ) -> impl Future<Output = crate::trc::Result<HttpResponse>> + Send;
50
51    fn handle_get_public_key(
52        &self,
53        path: Vec<&str>,
54    ) -> impl Future<Output = crate::trc::Result<HttpResponse>> + Send;
55
56    fn handle_create_signature(
57        &self,
58        body: Option<Vec<u8>>,
59    ) -> impl Future<Output = crate::trc::Result<HttpResponse>> + Send;
60
61    fn create_dkim_key(
62        &self,
63        algo: Algorithm,
64        id: impl AsRef<str> + Send,
65        domain: impl Into<String> + Send,
66        selector: impl Into<String> + Send,
67    ) -> impl Future<Output = crate::trc::Result<()>> + Send;
68}
69
70impl DkimManagement for Server {
71    async fn handle_manage_dkim(
72        &self,
73        req: &HttpRequest,
74        path: Vec<&str>,
75        body: Option<Vec<u8>>,
76        access_token: &AccessToken,
77    ) -> crate::trc::Result<HttpResponse> {
78        match *req.method() {
79            Method::GET => {
80                // Validate the access token
81                access_token.assert_has_permission(Permission::DkimSignatureGet)?;
82
83                self.handle_get_public_key(path).await
84            }
85            Method::POST => {
86                // Validate the access token
87                access_token.assert_has_permission(Permission::DkimSignatureCreate)?;
88
89                self.handle_create_signature(body).await
90            }
91            _ => Err(crate::trc::ResourceEvent::NotFound.into_err()),
92        }
93    }
94
95    async fn handle_get_public_key(&self, path: Vec<&str>) -> crate::trc::Result<HttpResponse> {
96        let signature_id = match path.get(1) {
97            Some(signature_id) => decode_path_element(signature_id),
98            None => {
99                return Err(crate::trc::ResourceEvent::NotFound.into_err());
100            }
101        };
102
103        let (pk, algo) = match (
104            self.core
105                .storage
106                .config
107                .get(&format!("signature.{signature_id}.private-key"))
108                .await,
109            self.core
110                .storage
111                .config
112                .get(&format!("signature.{signature_id}.algorithm"))
113                .await
114                .map(|algo| algo.and_then(|algo| algo.parse::<Algorithm>().ok())),
115        ) {
116            (Ok(Some(pk)), Ok(Some(algorithm))) => (pk, algorithm),
117            (Err(err), _) | (_, Err(err)) => return Err(err.caused_by(crate::trc::location!())),
118            _ => return Err(crate::trc::ResourceEvent::NotFound.into_err()),
119        };
120
121        Ok(JsonResponse::new(json!({
122            "data": obtain_dkim_public_key(algo, &pk)?,
123        }))
124        .into_http_response())
125    }
126
127    async fn handle_create_signature(
128        &self,
129        body: Option<Vec<u8>>,
130    ) -> crate::trc::Result<HttpResponse> {
131        let request =
132            match serde_json::from_slice::<DkimSignature>(body.as_deref().unwrap_or_default()) {
133                Ok(request) => request,
134                Err(err) => {
135                    return Err(crate::trc::EventType::Resource(
136                        crate::trc::ResourceEvent::BadParameters,
137                    )
138                    .reason(err));
139                }
140            };
141
142        let algo_str = match request.algorithm {
143            Algorithm::Rsa => "rsa",
144            Algorithm::Ed25519 => "ed25519",
145        };
146        let id = request
147            .id
148            .unwrap_or_else(|| format!("{algo_str}-{}", request.domain));
149        let selector = request.selector.unwrap_or_else(|| {
150            let dt = DateTime::from_timestamp(now() as i64);
151            format!(
152                "{:04}{:02}{}",
153                dt.year,
154                dt.month,
155                if Algorithm::Rsa == request.algorithm {
156                    "r"
157                } else {
158                    "e"
159                }
160            )
161        });
162
163        // Make sure the signature does not exist already
164        if let Some(value) = self
165            .core
166            .storage
167            .config
168            .get(&format!("signature.{id}.private-key"))
169            .await?
170        {
171            return Err(manage::err_exists(
172                format!("signature.{id}.private-key"),
173                value,
174            ));
175        }
176
177        // Create signature
178        self.create_dkim_key(request.algorithm, id, request.domain, selector)
179            .await?;
180
181        Ok(JsonResponse::new(json!({
182            "data": (),
183        }))
184        .into_http_response())
185    }
186
187    async fn create_dkim_key(
188        &self,
189        algo: Algorithm,
190        id: impl AsRef<str>,
191        domain: impl Into<String>,
192        selector: impl Into<String>,
193    ) -> crate::trc::Result<()> {
194        let id = id.as_ref();
195        let (algorithm, pk_type) = match algo {
196            Algorithm::Rsa => ("rsa-sha256", "RSA PRIVATE KEY"),
197            Algorithm::Ed25519 => ("ed25519-sha256", "PRIVATE KEY"),
198        };
199        let mut pk = format!("-----BEGIN {pk_type}-----\n").into_bytes();
200        let mut lf_count = 65;
201        for ch in base64_encode(
202            match algo {
203                Algorithm::Rsa => DkimKeyPair::generate_rsa(2048),
204                Algorithm::Ed25519 => DkimKeyPair::generate_ed25519(),
205            }
206            .map_err(|err| {
207                manage::error("Failed to generate key", err.to_string().into())
208                    .caused_by(crate::trc::location!())
209            })?
210            .private_key(),
211        )
212        .unwrap_or_default()
213        {
214            pk.push(ch);
215            lf_count -= 1;
216            if lf_count == 0 {
217                pk.push(b'\n');
218                lf_count = 65;
219            }
220        }
221        if lf_count != 65 {
222            pk.push(b'\n');
223        }
224        pk.extend_from_slice(format!("-----END {pk_type}-----\n").as_bytes());
225
226        self.core
227            .storage
228            .config
229            .set(
230                [
231                    (
232                        format!("signature.{id}.private-key"),
233                        String::from_utf8(pk).unwrap(),
234                    ),
235                    (format!("signature.{id}.domain"), domain.into()),
236                    (format!("signature.{id}.selector"), selector.into()),
237                    (format!("signature.{id}.algorithm"), algorithm.to_string()),
238                    (
239                        format!("signature.{id}.canonicalization"),
240                        "relaxed/relaxed".to_string(),
241                    ),
242                    (format!("signature.{id}.headers.0"), "From".to_string()),
243                    (format!("signature.{id}.headers.1"), "To".to_string()),
244                    (format!("signature.{id}.headers.2"), "Date".to_string()),
245                    (format!("signature.{id}.headers.3"), "Subject".to_string()),
246                    (
247                        format!("signature.{id}.headers.4"),
248                        "Message-ID".to_string(),
249                    ),
250                    (format!("signature.{id}.report"), "false".to_string()),
251                ],
252                true,
253            )
254            .await
255    }
256}
257
258pub fn obtain_dkim_public_key(algo: Algorithm, pk: &str) -> crate::trc::Result<String> {
259    match simple_pem_parse(pk) {
260        Some(der) => match algo {
261            Algorithm::Rsa => match RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
262                PrivatePkcs1KeyDer::from(der),
263            ))
264            .and_then(|key| {
265                Document::from_pkcs1_der(&key.public_key())
266                    .map_err(|err| mail_auth::Error::CryptoError(err.to_string()))
267            }) {
268                Ok(pk) => Ok(
269                    String::from_utf8(base64_encode(pk.as_bytes()).unwrap_or_default())
270                        .unwrap_or_default(),
271                ),
272                Err(err) => Err(manage::error(
273                    "Failed to read RSA DER",
274                    err.to_string().into(),
275                )),
276            },
277            Algorithm::Ed25519 => {
278                match Ed25519Key::from_pkcs8_maybe_unchecked_der(&der)
279                    .map_err(|err| mail_auth::Error::CryptoError(err.to_string()))
280                {
281                    Ok(pk) => Ok(String::from_utf8(
282                        base64_encode(&pk.public_key()).unwrap_or_default(),
283                    )
284                    .unwrap_or_default()),
285                    Err(err) => Err(manage::error("Crypto error", err.to_string().into())),
286                }
287            }
288        },
289        None => Err(manage::error("Failed to decode private key", None::<u32>)),
290    }
291}
292
293impl FromStr for Algorithm {
294    type Err = ();
295
296    fn from_str(s: &str) -> Result<Self, Self::Err> {
297        match s.split_once('-').map(|(algo, _)| algo) {
298            Some("rsa") => Ok(Algorithm::Rsa),
299            Some("ed25519") => Ok(Algorithm::Ed25519),
300            _ => Err(()),
301        }
302    }
303}