1use crate::errors::Status;
4use crate::hash::HeaderList;
5use crate::header::TaggedHeader;
6use dns_resolver::{HickoryResolver, Resolver};
7use ed25519_dalek::pkcs8::DecodePrivateKey;
8use ed25519_dalek::SigningKey;
9use mailparsing::AuthenticationResult;
10use openssl::md::Md;
11use openssl::pkey::PKey;
12use openssl::pkey_ctx::PkeyCtx;
13use openssl::rsa::{Padding, Rsa};
14use std::collections::BTreeMap;
15
16pub mod arc;
17pub mod canonicalization;
18mod errors;
19mod hash;
20mod header;
21mod parsed_email;
22mod parser;
23mod public_key;
24#[cfg(test)]
25mod roundtrip_test;
26mod sign;
27
28pub use errors::DKIMError;
29use header::{DKIMHeader, DKIM_SIGNATURE_HEADER_NAME};
30pub use parsed_email::ParsedEmail;
31pub use parser::{tag_list as parse_tag_list, Tag};
32pub use sign::{Signer, SignerBuilder};
33
34const DNS_NAMESPACE: &str = "_domainkey";
35
36#[derive(Debug)]
37pub(crate) enum DkimPublicKey {
38 Rsa(PKey<openssl::pkey::Public>),
39 Ed25519(ed25519_dalek::VerifyingKey),
40}
41
42#[allow(clippy::large_enum_variant)]
43#[derive(Debug)]
44pub enum DkimPrivateKey {
45 Ed25519(SigningKey),
46 OpenSSLRsa(Rsa<openssl::pkey::Private>),
47}
48
49impl DkimPrivateKey {
50 pub fn rsa_key(data: &[u8]) -> Result<Self, DKIMError> {
52 let mut errors = vec![];
53
54 match Rsa::private_key_from_pem(data) {
55 Ok(key) => return Ok(Self::OpenSSLRsa(key)),
56 Err(err) => errors.push(format!("openssl private_key_from_pem: {err:#}")),
57 };
58 match Rsa::private_key_from_der(data) {
59 Ok(key) => return Ok(Self::OpenSSLRsa(key)),
60 Err(err) => errors.push(format!("openssl private_key_from_der: {err:#}")),
61 };
62
63 Err(DKIMError::PrivateKeyLoadError(errors.join(". ")))
64 }
65
66 pub fn rsa_key_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, DKIMError> {
68 let path = path.as_ref();
69 let data = std::fs::read(path).map_err(|err| {
70 DKIMError::PrivateKeyLoadError(format!(
71 "rsa_key_file: failed to read file {path:?}: {err:#}"
72 ))
73 })?;
74 Self::rsa_key(&data)
75 }
76
77 pub fn ed25519_key(data: &[u8]) -> Result<Self, DKIMError> {
80 let mut errors = vec![];
81
82 match SigningKey::from_pkcs8_der(data) {
83 Ok(key) => return Ok(Self::Ed25519(key)),
84 Err(err) => errors.push(format!("Ed25519 SigningKey::from_pkcs8_der: {err:#}")),
85 }
86
87 match std::str::from_utf8(data) {
88 Ok(s) => match SigningKey::from_pkcs8_pem(s) {
89 Ok(key) => return Ok(Self::Ed25519(key)),
90 Err(err) => errors.push(format!("Ed25519 SigningKey::from_pkcs8_pem: {err:#}")),
91 },
92 Err(err) => errors.push(format!("ed25519_key: data is not UTF-8: {err:#}")),
93 }
94
95 Err(DKIMError::PrivateKeyLoadError(errors.join(". ")))
96 }
97
98 pub fn key(data: &[u8]) -> Result<Self, DKIMError> {
101 let mut errors = vec![];
102
103 match Rsa::private_key_from_pem(data) {
104 Ok(key) => return Ok(Self::OpenSSLRsa(key)),
105 Err(err) => errors.push(format!("openssl private_key_from_pem: {err:#}")),
106 }
107 match Rsa::private_key_from_der(data) {
108 Ok(key) => return Ok(Self::OpenSSLRsa(key)),
109 Err(err) => errors.push(format!("openssl private_key_from_der: {err:#}")),
110 }
111 match SigningKey::from_pkcs8_der(data) {
112 Ok(key) => return Ok(Self::Ed25519(key)),
113 Err(err) => errors.push(format!("Ed25519 SigningKey::from_pkcs8_der: {err:#}")),
114 }
115 match std::str::from_utf8(data) {
116 Ok(s) => match SigningKey::from_pkcs8_pem(s) {
117 Ok(key) => return Ok(Self::Ed25519(key)),
118 Err(err) => errors.push(format!("Ed25519 SigningKey::from_pkcs8_pem: {err:#}")),
119 },
120 Err(err) => errors.push(format!("ed25519_key: data is not UTF-8: {err:#}")),
121 }
122
123 Err(DKIMError::PrivateKeyLoadError(errors.join(". ")))
124 }
125}
126
127fn verify_signature(
129 hash_algo: hash::HashAlgo,
130 header_hash: &[u8],
131 signature: &[u8],
132 public_key: DkimPublicKey,
133) -> Result<bool, DKIMError> {
134 Ok(match public_key {
135 DkimPublicKey::Rsa(public_key) => {
136 let md = match hash_algo {
137 hash::HashAlgo::RsaSha1 => Md::sha1(),
138 hash::HashAlgo::RsaSha256 => Md::sha256(),
139 hash @ hash::HashAlgo::Ed25519Sha256 => {
140 return Err(DKIMError::UnsupportedHashAlgorithm(format!("{hash:?}")));
145 }
146 };
147
148 let mut ctx = PkeyCtx::new(&public_key).map_err(|err| {
149 DKIMError::SignatureSyntaxError(format!("Error loading RSA public key: {err}"))
150 })?;
151
152 ctx.verify_init().map_err(|err| {
153 DKIMError::UnknownInternalError(format!("ctx.verify_init failed: {err}"))
154 })?;
155 ctx.set_rsa_padding(Padding::PKCS1).map_err(|err| {
156 DKIMError::UnknownInternalError(format!("ctx.set_rsa_padding failed: {err}"))
157 })?;
158 ctx.set_signature_md(md).map_err(|err| {
159 DKIMError::UnknownInternalError(format!("ctx.set_signature_md failed: {err}"))
160 })?;
161 ctx.verify(header_hash, signature).unwrap_or_default()
162 }
163 DkimPublicKey::Ed25519(public_key) => {
164 let mut sig_bytes = [0u8; ed25519_dalek::Signature::BYTE_SIZE];
165 if signature.len() != sig_bytes.len() {
166 return Err(DKIMError::SignatureSyntaxError(format!(
167 "ed25519 signatures should be {} bytes in length, have: {}",
168 ed25519_dalek::Signature::BYTE_SIZE,
169 signature.len()
170 )));
171 }
172 sig_bytes.copy_from_slice(signature);
173
174 public_key
175 .verify_strict(
176 header_hash,
177 &ed25519_dalek::Signature::from_bytes(&sig_bytes),
178 )
179 .is_ok()
180 }
181 })
182}
183
184async fn verify_email_header<'a>(
185 resolver: &dyn Resolver,
186 signature_header_name: &str,
187 dkim_header: &'a TaggedHeader,
188 email: &'a ParsedEmail<'a>,
189) -> Result<(), DKIMError> {
190 let public_keys = public_key::retrieve_public_keys(
191 resolver,
192 dkim_header.get_required_tag("d"),
193 dkim_header.get_required_tag("s"),
194 )
195 .await?;
196
197 let (header_canonicalization_type, body_canonicalization_type) =
198 parser::parse_canonicalization(dkim_header.get_tag("c"))?;
199 let hash_algo = parser::parse_hash_algo(dkim_header.get_required_tag("a"))?;
200
201 let header_list: Vec<String> = dkim_header
202 .get_required_tag("h")
203 .split(':')
204 .map(|s| s.trim().to_ascii_lowercase())
205 .collect();
206 let header_list = HeaderList::new(header_list).compute_concrete_header_list(email);
207
208 let computed_headers_hash = hash::compute_headers_hash(
209 header_canonicalization_type,
210 &header_list,
211 hash_algo,
212 dkim_header,
213 signature_header_name,
214 )?;
215
216 if let Some(header_body_hash) = dkim_header.get_tag("bh") {
217 let computed_body_hash = hash::compute_body_hash(
218 body_canonicalization_type,
219 dkim_header.parse_tag("l")?,
220 hash_algo,
221 email,
222 )?;
223 tracing::debug!("body_hash {:?}", computed_body_hash);
224 if header_body_hash != computed_body_hash {
225 return Err(DKIMError::BodyHashDidNotVerify);
226 }
227 }
228
229 let signature = data_encoding::BASE64
230 .decode(dkim_header.get_required_tag("b").as_bytes())
231 .map_err(|err| {
232 DKIMError::SignatureSyntaxError(format!("failed to decode signature: {}", err))
233 })?;
234
235 let mut errors = vec![];
236 for public_key in public_keys {
237 match verify_signature(hash_algo, &computed_headers_hash, &signature, public_key) {
238 Ok(true) => return Ok(()),
239 Ok(false) => {}
240 Err(err) => {
241 errors.push(err);
242 }
243 }
244 }
245
246 if let Some(err) = errors.pop() {
247 return Err(err);
249 }
250
251 Err(DKIMError::SignatureDidNotVerify)
254}
255
256pub async fn verify_email_with_resolver<'a>(
258 email: &'a ParsedEmail<'a>,
259 resolver: &dyn Resolver,
260) -> Result<Vec<AuthenticationResult>, DKIMError> {
261 let mut results = vec![];
262
263 let mut dkim_headers = vec![];
264
265 for h in email.get_headers().iter_named(DKIM_SIGNATURE_HEADER_NAME) {
266 if results.len() > 10 {
267 break;
270 }
271
272 match h
273 .get_raw_value_string()
274 .map_err(Into::into)
275 .and_then(DKIMHeader::parse)
276 {
277 Ok(v) => {
278 dkim_headers.push(v);
279 }
280 Err(err) => {
281 results.push(AuthenticationResult {
282 method: "dkim".into(),
283 method_version: None,
284 result: "permerror".into(),
285 reason: Some(format!("{err}").into()),
286 props: BTreeMap::new(),
287 });
288 }
289 }
290 }
291
292 fn compute_header_b(b_tag: &str, headers: &[DKIMHeader]) -> String {
299 let mut len = 8;
300
301 'bigger: while len < b_tag.len() {
302 for h in headers {
303 let candidate = h.get_required_tag("b");
304 if candidate == b_tag {
305 continue;
306 }
307 if b_tag[0..len] == candidate[0..len] {
308 len += 2;
309 continue 'bigger;
310 }
311 }
312 return b_tag[0..len].to_string();
313 }
314 b_tag.to_string()
315 }
316
317 for dkim_header in &dkim_headers {
318 let signing_domain = dkim_header.get_required_tag("d");
319 let mut props = BTreeMap::new();
320
321 props.insert("header.d".into(), signing_domain.into());
322 props.insert("header.i".into(), format!("@{signing_domain}").into());
323 props.insert("header.a".into(), dkim_header.get_required_tag("a").into());
324 props.insert("header.s".into(), dkim_header.get_required_tag("s").into());
325
326 let b_tag = compute_header_b(dkim_header.get_required_tag("b"), &dkim_headers);
327 props.insert("header.b".into(), b_tag.into());
328
329 let mut reason = None;
330 let result =
331 match verify_email_header(resolver, DKIM_SIGNATURE_HEADER_NAME, dkim_header, email)
332 .await
333 {
334 Ok(()) => "pass",
335 Err(err) => {
336 reason.replace(format!("{err}"));
337 match err.status() {
338 Status::Tempfail => "temperror",
339 Status::Permfail => "permerror",
340 }
341 }
342 };
343
344 results.push(AuthenticationResult {
345 method: "dkim".into(),
346 method_version: None,
347 result: result.into(),
348 reason: reason.map(Into::into),
349 props,
350 });
351 }
352
353 Ok(results)
354}
355
356pub async fn verify_email<'a>(
358 email: &'a ParsedEmail<'a>,
359) -> Result<Vec<AuthenticationResult>, DKIMError> {
360 let resolver = HickoryResolver::new().map_err(|err| {
361 DKIMError::UnknownInternalError(format!("failed to create DNS resolver: {}", err))
362 })?;
363
364 verify_email_with_resolver(email, &resolver).await
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use dns_resolver::TestResolver;
371
372 const NEW_ENGLAND_DKIM: (&str, &str) = (
373 "newengland._domainkey.example.com",
374 "v=DKIM1; p=MIGJAoGBALVI635dLK4cJJAH3Lx6upo3X/Lm1tQz3mezcWTA3BUBnyIsdnRf57aD5BtNmhPrYYDlWlzw3UgnKisIxktkk5+iMQMlFtAS10JB8L3YadXNJY+JBcbeSi5TgJe4WFzNgW95FWDAuSTRXSWZfA/8xjflbTLDx0euFZOM7C4T0GwLAgMBAAE=",
375 );
376
377 #[test]
378 fn test_validate_header() {
379 let header = r#"v=1; a=rsa-sha256; d=example.net; s=brisbane;
380c=relaxed/simple; q=dns/txt; i=foo@eng.example.net;
381t=1117574938; x=9118006938; l=200;
382h=from:to:subject:date:keywords:keywords;
383z=From:foo@eng.example.net|To:joe@example.com|
384Subject:demo=20run|Date:July=205,=202005=203:44:08=20PM=20-0700;
385bh=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=;
386b=dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD00lszZ
387 VoG4ZHRNiYzR
388 "#;
389 DKIMHeader::parse(header).unwrap();
390 }
391
392 #[test]
393 fn test_validate_header_missing_tag() {
394 let header = "v=1; a=rsa-sha256; bh=a; b=b";
395 assert_eq!(
396 DKIMHeader::parse(header).unwrap_err(),
397 DKIMError::SignatureMissingRequiredTag("d")
398 );
399 }
400
401 #[test]
402 fn test_validate_header_domain_mismatch() {
403 let header = r#"v=1; a=rsa-sha256; d=example.net; s=brisbane; i=foo@hein.com; h=headers; bh=hash; b=hash
404 "#;
405 assert_eq!(
406 DKIMHeader::parse(header).unwrap_err(),
407 DKIMError::DomainMismatch
408 );
409
410 let header = r#"v=1; a=rsa-sha256; d=example.net; s=brisbane.net; i=foo@fexample.net; h=headers; bh=hash; b=hash
411 "#;
412 assert_eq!(
413 DKIMHeader::parse(header).unwrap_err(),
414 DKIMError::DomainMismatch
415 );
416 }
417
418 #[test]
419 fn test_validate_header_incompatible_version() {
420 let header = r#"v=3; a=rsa-sha256; d=example.net; s=brisbane; i=foo@example.net; h=headers; bh=hash; b=hash
421 "#;
422 assert_eq!(
423 DKIMHeader::parse(header).unwrap_err(),
424 DKIMError::IncompatibleVersion
425 );
426 }
427
428 #[test]
429 fn test_validate_header_missing_from_in_headers_signature() {
430 let header = r#"v=1; a=rsa-sha256; d=example.net; s=brisbane; i=foo@example.net; h=Subject:A:B; bh=hash; b=hash
431 "#;
432 assert_eq!(
433 DKIMHeader::parse(header).unwrap_err(),
434 DKIMError::FromFieldNotSigned
435 );
436 }
437
438 #[test]
439 fn test_validate_header_expired_in_drift() {
440 let mut now = chrono::Utc::now().naive_utc();
441 now -= chrono::Duration::try_seconds(1).expect("1 second to be valid");
442
443 let header = format!("v=1; a=rsa-sha256; d=example.net; s=brisbane; i=foo@example.net; h=From:B; bh=hash; b=hash; x={}", now.and_utc().timestamp());
444
445 assert!(DKIMHeader::parse(&header).is_ok());
446 }
447
448 #[test]
449 fn test_validate_header_expired() {
450 let mut now = chrono::Utc::now().naive_utc();
451 now -= chrono::Duration::try_hours(3).expect("3 hours to be legit");
452
453 let header = format!("v=1; a=rsa-sha256; d=example.net; s=brisbane; i=foo@example.net; h=From:B; bh=hash; b=hash; x={}", now.and_utc().timestamp());
454
455 assert_eq!(
456 DKIMHeader::parse(&header).unwrap_err(),
457 DKIMError::SignatureExpired
458 );
459 }
460
461 #[tokio::test]
462 async fn test_validate_email_header_ed25519() {
463 let raw_email = r#"DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed;
464 d=football.example.com; i=@football.example.com;
465 q=dns/txt; s=brisbane; t=1528637909; h=from : to :
466 subject : date : message-id : from : subject : date;
467 bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
468 b=/gCrinpcQOoIfuHNQIbq4pgh9kyIK3AQUdt9OdqQehSwhEIug4D11Bus
469 Fa3bT3FY5OsU7ZbnKELq+eXdp1Q1Dw==
470DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
471 d=football.example.com; i=@football.example.com;
472 q=dns/txt; s=test; t=1528637909; h=from : to : subject :
473 date : message-id : from : subject : date;
474 bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
475 b=F45dVWDfMbQDGHJFlXUNB2HKfbCeLRyhDXgFpEL8GwpsRe0IeIixNTe3
476 DhCVlUrSjV4BwcVcOF6+FF3Zo9Rpo1tFOeS9mPYQTnGdaSGsgeefOsk2Jz
477 dA+L10TeYt9BgDfQNZtKdN1WO//KgIqXP7OdEFE4LjFYNcUxZQ4FADY+8=
478From: Joe SixPack <joe@football.example.com>
479To: Suzie Q <suzie@shopping.example.net>
480Subject: Is dinner ready?
481Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
482Message-ID: <20030712040037.46341.5F8J@football.example.com>
483
484Hi.
485
486We lost the game. Are you hungry yet?
487
488Joe."#
489 .replace('\n', "\r\n");
490
491 let email = ParsedEmail::parse(raw_email).unwrap();
492 let raw_header_dkim = email
493 .get_headers()
494 .iter_named(DKIM_SIGNATURE_HEADER_NAME)
495 .next()
496 .unwrap()
497 .get_raw_value_string()
498 .unwrap();
499
500 const DKIM_BRISBANE: &str = r#"
501$ORIGIN brisbane._domainkey.football.example.com
502@ 300 TXT "v=DKIM1; p=MIGJAoGBALVI635dLK4cJJAH3Lx6upo3X/Lm1tQz3mezcWTA3BUBnyIsdnRf57aD5BtNmhPrYYDlWlzw3UgnKisIxktkk5+iMQMlFtAS10JB8L3YadXNJY+JBcbeSi5TgJe4WFzNgW95FWDAuSTRXSWZfA/8xjflbTLDx0euFZOM7C4T0GwLAgMBAAE="
503 TXT "v=DKIM1; k=ed25519; p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo="
504"#;
505
506 let resolver = TestResolver::default().with_zone(DKIM_BRISBANE).unwrap();
507
508 verify_email_header(
509 &resolver,
510 DKIM_SIGNATURE_HEADER_NAME,
511 &DKIMHeader::parse(raw_header_dkim).unwrap(),
512 &email,
513 )
514 .await
515 .unwrap();
516 }
517
518 #[tokio::test]
519 async fn test_validate_email_header_rsa() {
520 let raw_email =
524 r#"DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
525 c=simple/simple; d=example.com;
526 h=Received:From:To:Subject:Date:Message-ID; i=joe@football.example.com;
527 s=newengland; t=1615825284; v=1;
528 b=Xh4Ujb2wv5x54gXtulCiy4C0e+plRm6pZ4owF+kICpYzs/8WkTVIDBrzhJP0DAYCpnL62T0G
529 k+0OH8pi/yqETVjKtKk+peMnNvKkut0GeWZMTze0bfq3/JUK3Ln3jTzzpXxrgVnvBxeY9EZIL4g
530 s4wwFRRKz/1bksZGSjD8uuSU=
531Received: from client1.football.example.com [192.0.2.1]
532 by submitserver.example.com with SUBMISSION;
533 Fri, 11 Jul 2003 21:01:54 -0700 (PDT)
534From: Joe SixPack <joe@football.example.com>
535To: Suzie Q <suzie@shopping.example.net>
536Subject: Is dinner ready?
537Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
538Message-ID: <20030712040037.46341.5F8J@football.example.com>
539
540Hi.
541
542We lost the game. Are you hungry yet?
543
544Joe.
545"#
546 .replace('\n', "\r\n");
547 let email = ParsedEmail::parse(raw_email).unwrap();
548 let raw_header_rsa = email
549 .get_headers()
550 .iter_named(DKIM_SIGNATURE_HEADER_NAME)
551 .next()
552 .unwrap()
553 .get_raw_value_string()
554 .unwrap();
555
556 let resolver =
557 TestResolver::default().with_txt(NEW_ENGLAND_DKIM.0, NEW_ENGLAND_DKIM.1.to_owned());
558
559 verify_email_header(
560 &resolver,
561 DKIM_SIGNATURE_HEADER_NAME,
562 &DKIMHeader::parse(raw_header_rsa).unwrap(),
563 &email,
564 )
565 .await
566 .unwrap();
567 }
568}