1use std::time::{SystemTime, UNIX_EPOCH};
2
3use base64::engine::general_purpose::STANDARD as BASE64;
4use base64::Engine;
5use ring::rand::SystemRandom;
6use ring::signature as ring_sig;
7use ring::signature::KeyPair;
8
9use super::canon::{
10 canonicalize_body, canonicalize_header, normalize_line_endings, select_headers,
11};
12use super::types::{Algorithm, CanonicalizationMethod, HashAlgorithm};
13
14#[derive(Debug)]
16pub struct SigningError {
17 pub detail: String,
18}
19
20impl std::fmt::Display for SigningError {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 f.write_str(&self.detail)
23 }
24}
25
26impl std::error::Error for SigningError {}
27
28enum PrivateKey {
30 Rsa(ring_sig::RsaKeyPair),
31 Ed25519(ring_sig::Ed25519KeyPair),
32}
33
34pub struct DkimSigner {
39 key: PrivateKey,
40 algorithm: Algorithm,
41 domain: String,
42 selector: String,
43 header_canon: CanonicalizationMethod,
44 body_canon: CanonicalizationMethod,
45 headers_to_sign: Vec<String>,
46 over_sign: bool,
47 expiration_seconds: Option<u64>,
48}
49
50impl DkimSigner {
51 pub fn rsa_sha256(
53 domain: impl Into<String>,
54 selector: impl Into<String>,
55 pkcs8_pem: &[u8],
56 ) -> Result<Self, SigningError> {
57 let der = decode_pem(pkcs8_pem)?;
58 let key_pair = ring_sig::RsaKeyPair::from_pkcs8(&der)
59 .map_err(|e| SigningError { detail: format!("invalid RSA PKCS8 key: {}", e) })?;
60
61 Ok(Self {
62 key: PrivateKey::Rsa(key_pair),
63 algorithm: Algorithm::RsaSha256,
64 domain: domain.into(),
65 selector: selector.into(),
66 header_canon: CanonicalizationMethod::Relaxed,
67 body_canon: CanonicalizationMethod::Relaxed,
68 headers_to_sign: default_headers(),
69 over_sign: false,
70 expiration_seconds: None,
71 })
72 }
73
74 pub fn ed25519(
79 domain: impl Into<String>,
80 selector: impl Into<String>,
81 pkcs8: &[u8],
82 ) -> Result<Self, SigningError> {
83 let key_pair = ring_sig::Ed25519KeyPair::from_pkcs8(pkcs8)
84 .map_err(|e| SigningError { detail: format!("invalid Ed25519 PKCS8 key: {}", e) })?;
85
86 Ok(Self {
87 key: PrivateKey::Ed25519(key_pair),
88 algorithm: Algorithm::Ed25519Sha256,
89 domain: domain.into(),
90 selector: selector.into(),
91 header_canon: CanonicalizationMethod::Relaxed,
92 body_canon: CanonicalizationMethod::Relaxed,
93 headers_to_sign: default_headers(),
94 over_sign: false,
95 expiration_seconds: None,
96 })
97 }
98
99 pub fn header_canonicalization(mut self, method: CanonicalizationMethod) -> Self {
101 self.header_canon = method;
102 self
103 }
104
105 pub fn body_canonicalization(mut self, method: CanonicalizationMethod) -> Self {
107 self.body_canon = method;
108 self
109 }
110
111 pub fn headers(mut self, headers: Vec<String>) -> Self {
113 self.headers_to_sign = headers;
114 self
115 }
116
117 pub fn over_sign(mut self, enabled: bool) -> Self {
119 self.over_sign = enabled;
120 self
121 }
122
123 pub fn expiration(mut self, seconds: u64) -> Self {
125 self.expiration_seconds = Some(seconds);
126 self
127 }
128
129 pub fn sign_message(
136 &self,
137 headers: &[(&str, &str)],
138 body: &[u8],
139 ) -> Result<String, SigningError> {
140 if !self.headers_to_sign.iter().any(|h| h.eq_ignore_ascii_case("from")) {
142 return Err(SigningError {
143 detail: "h= headers must include 'from'".into(),
144 });
145 }
146
147 let normalized_body = normalize_line_endings(body);
149 let canon_body = canonicalize_body(self.body_canon, &normalized_body);
150 let body_hash = compute_hash(self.algorithm, &canon_body);
151 let bh_b64 = BASE64.encode(&body_hash);
152
153 let h_value = self.build_h_value();
155
156 let now = SystemTime::now()
158 .duration_since(UNIX_EPOCH)
159 .map(|d| d.as_secs())
160 .unwrap_or(0);
161
162 let algo_str = match self.algorithm {
164 Algorithm::RsaSha256 => "rsa-sha256",
165 Algorithm::Ed25519Sha256 => "ed25519-sha256",
166 Algorithm::RsaSha1 => unreachable!("RSA-SHA1 signing not supported"),
167 };
168 let canon_str = format!(
169 "{}/{}",
170 canon_method_str(self.header_canon),
171 canon_method_str(self.body_canon),
172 );
173
174 let mut sig_template = format!(
175 " v=1; a={}; c={}; d={}; s={}; t={}; h={}; bh={}; b=",
176 algo_str, canon_str, self.domain, self.selector, now, h_value, bh_b64,
177 );
178
179 if let Some(exp_secs) = self.expiration_seconds {
180 let x_val = now + exp_secs;
182 sig_template = format!(
183 " v=1; a={}; c={}; d={}; s={}; t={}; x={}; h={}; bh={}; b=",
184 algo_str, canon_str, self.domain, self.selector, now, x_val, h_value, bh_b64,
185 );
186 }
187
188 let h_names: Vec<String> = self.build_h_names();
191
192 let selected = select_headers(self.header_canon, &h_names, headers);
193
194 let mut hash_input = Vec::new();
195 for header_line in &selected {
196 hash_input.extend_from_slice(header_line.as_bytes());
197 }
198
199 let canon_sig = if self.header_canon == CanonicalizationMethod::Simple {
201 format!("DKIM-Signature:{}", sig_template)
202 } else {
203 canonicalize_header(self.header_canon, "dkim-signature", &sig_template)
204 };
205 hash_input.extend_from_slice(canon_sig.as_bytes());
206
207 let signature = self.sign_raw(&hash_input)?;
209 let sig_b64 = BASE64.encode(&signature);
210
211 let full_sig = format!("{}{}", sig_template, sig_b64);
213
214 Ok(full_sig)
215 }
216
217 fn build_h_value(&self) -> String {
219 let mut names = Vec::new();
220 for h in &self.headers_to_sign {
221 names.push(h.to_lowercase());
222 if self.over_sign {
223 names.push(h.to_lowercase());
224 }
225 }
226 names.join(":")
227 }
228
229 fn build_h_names(&self) -> Vec<String> {
231 let mut names = Vec::new();
232 for h in &self.headers_to_sign {
233 names.push(h.to_lowercase());
234 if self.over_sign {
235 names.push(h.to_lowercase());
236 }
237 }
238 names
239 }
240
241 fn sign_raw(&self, data: &[u8]) -> Result<Vec<u8>, SigningError> {
243 match &self.key {
244 PrivateKey::Rsa(key_pair) => {
245 let rng = SystemRandom::new();
246 let mut signature = vec![0u8; key_pair.public().modulus_len()];
247 key_pair
248 .sign(&ring_sig::RSA_PKCS1_SHA256, &rng, data, &mut signature)
249 .map_err(|e| SigningError { detail: format!("RSA signing failed: {}", e) })?;
250 Ok(signature)
251 }
252 PrivateKey::Ed25519(key_pair) => {
253 let sig = key_pair.sign(data);
254 Ok(sig.as_ref().to_vec())
255 }
256 }
257 }
258
259 pub fn public_key_bytes(&self) -> Vec<u8> {
263 match &self.key {
264 PrivateKey::Rsa(key_pair) => {
265 let pkcs1_der = key_pair.public().as_ref();
268 wrap_pkcs1_in_spki(pkcs1_der)
269 }
270 PrivateKey::Ed25519(key_pair) => {
271 key_pair.public_key().as_ref().to_vec()
272 }
273 }
274 }
275}
276
277fn wrap_pkcs1_in_spki(pkcs1_der: &[u8]) -> Vec<u8> {
281 let algo_id: &[u8] = &[
283 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, ];
287
288 let bit_string_content_len = 1 + pkcs1_der.len(); let mut bit_string = vec![0x03];
291 encode_asn1_length(&mut bit_string, bit_string_content_len);
292 bit_string.push(0x00); bit_string.extend_from_slice(pkcs1_der);
294
295 let inner_len = algo_id.len() + bit_string.len();
297 let mut spki = vec![0x30];
298 encode_asn1_length(&mut spki, inner_len);
299 spki.extend_from_slice(algo_id);
300 spki.extend_from_slice(&bit_string);
301
302 spki
303}
304
305fn encode_asn1_length(output: &mut Vec<u8>, len: usize) {
307 if len < 0x80 {
308 output.push(len as u8);
309 } else if len < 0x100 {
310 output.push(0x81);
311 output.push(len as u8);
312 } else {
313 output.push(0x82);
314 output.push((len >> 8) as u8);
315 output.push((len & 0xff) as u8);
316 }
317}
318
319fn default_headers() -> Vec<String> {
321 vec![
322 "from".into(),
323 "to".into(),
324 "subject".into(),
325 "date".into(),
326 "mime-version".into(),
327 "content-type".into(),
328 "message-id".into(),
329 ]
330}
331
332fn canon_method_str(m: CanonicalizationMethod) -> &'static str {
333 match m {
334 CanonicalizationMethod::Simple => "simple",
335 CanonicalizationMethod::Relaxed => "relaxed",
336 }
337}
338
339fn compute_hash(algorithm: Algorithm, data: &[u8]) -> Vec<u8> {
341 match algorithm.hash_algorithm() {
342 HashAlgorithm::Sha256 => {
343 let digest = ring::digest::digest(&ring::digest::SHA256, data);
344 digest.as_ref().to_vec()
345 }
346 HashAlgorithm::Sha1 => {
347 let digest = ring::digest::digest(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY, data);
348 digest.as_ref().to_vec()
349 }
350 }
351}
352
353fn decode_pem(pem: &[u8]) -> Result<Vec<u8>, SigningError> {
355 let pem_str = std::str::from_utf8(pem)
356 .map_err(|_| SigningError { detail: "PEM is not valid UTF-8".into() })?;
357
358 let lines: Vec<&str> = pem_str.lines().collect();
360 let mut b64 = String::new();
361 let mut in_block = false;
362
363 for line in &lines {
364 let trimmed = line.trim();
365 if trimmed.starts_with("-----BEGIN") {
366 in_block = true;
367 continue;
368 }
369 if trimmed.starts_with("-----END") {
370 break;
371 }
372 if in_block {
373 b64.push_str(trimmed);
374 }
375 }
376
377 if b64.is_empty() {
378 return Err(SigningError { detail: "no PEM content found".into() });
379 }
380
381 BASE64.decode(&b64)
382 .map_err(|e| SigningError { detail: format!("PEM base64 decode failed: {}", e) })
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use crate::common::dns::mock::MockResolver;
389 use crate::dkim::verify::DkimVerifier;
390 use crate::dkim::DkimResult;
391
392 fn setup_mock_key(resolver: &mut MockResolver, selector: &str, domain: &str, txt: &str) {
393 let qname = format!("{}._domainkey.{}", selector, domain);
394 resolver.add_txt(&qname, vec![txt.to_string()]);
395 }
396
397 #[test]
400 fn ed25519_key_loads_from_pkcs8() {
401 let rng = SystemRandom::new();
402 let pkcs8 = ring_sig::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
403 let signer = DkimSigner::ed25519("example.com", "sel", pkcs8.as_ref());
404 assert!(signer.is_ok());
405 }
406
407 #[test]
408 fn rsa_key_loads_from_pem() {
409 let pem = generate_rsa_pem();
410 let signer = DkimSigner::rsa_sha256("example.com", "sel", &pem);
411 assert!(signer.is_ok());
412 }
413
414 #[test]
415 fn invalid_key_fails_fast() {
416 let result = DkimSigner::ed25519("example.com", "sel", b"not-a-key");
417 assert!(result.is_err());
418 }
419
420 #[test]
421 fn invalid_pem_fails() {
422 let result = DkimSigner::rsa_sha256("example.com", "sel", b"not-pem");
423 assert!(result.is_err());
424 }
425
426 #[test]
429 fn rsa_sha1_signing_not_constructable() {
430 let pem = generate_rsa_pem();
434 let signer = DkimSigner::rsa_sha256("example.com", "sel", &pem).unwrap();
435 assert!(matches!(signer.algorithm, Algorithm::RsaSha256));
436 }
438
439 #[test]
442 fn sign_without_from_fails() {
443 let rng = SystemRandom::new();
444 let pkcs8 = ring_sig::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
445 let signer = DkimSigner::ed25519("example.com", "sel", pkcs8.as_ref())
446 .unwrap()
447 .headers(vec!["to".into(), "subject".into()]); let headers = vec![
450 ("From", " user@example.com"),
451 ("To", " other@example.com"),
452 ];
453 let result = signer.sign_message(&headers, b"body\r\n");
454 assert!(result.is_err());
455 assert!(result.unwrap_err().detail.contains("from"));
456 }
457
458 #[tokio::test]
461 async fn ed25519_sign_verify_roundtrip() {
462 let rng = SystemRandom::new();
463 let pkcs8 = ring_sig::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
464 let signer = DkimSigner::ed25519("example.com", "ed", pkcs8.as_ref()).unwrap();
465
466 let headers = vec![
467 ("From", " user@example.com"),
468 ("To", " other@example.com"),
469 ("Subject", " Test message"),
470 ];
471 let body = b"Hello world\r\n";
472
473 let sig_value = signer.sign_message(&headers, body).unwrap();
474
475 let pub_key = signer.public_key_bytes();
477 let pub_b64 = BASE64.encode(&pub_key);
478
479 let mut resolver = MockResolver::new();
480 setup_mock_key(
481 &mut resolver,
482 "ed",
483 "example.com",
484 &format!("v=DKIM1; k=ed25519; p={}", pub_b64),
485 );
486
487 let verifier = DkimVerifier::new(resolver);
488 let mut all_headers = headers.clone();
489 all_headers.push(("DKIM-Signature", &sig_value));
490
491 let results = verifier.verify_message(&all_headers, body).await;
492 match &results[0] {
493 DkimResult::Pass { domain, .. } => assert_eq!(domain, "example.com"),
494 other => panic!("Ed25519 roundtrip: expected Pass, got {:?}", other),
495 }
496 }
497
498 #[tokio::test]
501 async fn rsa_sha256_sign_verify_roundtrip() {
502 let pem = generate_rsa_pem();
503 let signer = DkimSigner::rsa_sha256("example.com", "rsa", &pem).unwrap();
504
505 let headers = vec![
506 ("From", " sender@example.com"),
507 ("To", " recipient@example.com"),
508 ("Subject", " RSA roundtrip test"),
509 ];
510 let body = b"RSA signed body\r\n";
511
512 let sig_value = signer.sign_message(&headers, body).unwrap();
513
514 let pub_key = signer.public_key_bytes();
516 let pub_b64 = BASE64.encode(&pub_key);
517
518 let mut resolver = MockResolver::new();
519 setup_mock_key(
520 &mut resolver,
521 "rsa",
522 "example.com",
523 &format!("v=DKIM1; k=rsa; p={}", pub_b64),
524 );
525
526 let verifier = DkimVerifier::new(resolver);
527 let mut all_headers = headers.clone();
528 all_headers.push(("DKIM-Signature", &sig_value));
529
530 let results = verifier.verify_message(&all_headers, body).await;
531 match &results[0] {
532 DkimResult::Pass { domain, .. } => assert_eq!(domain, "example.com"),
533 other => panic!("RSA-SHA256 roundtrip: expected Pass, got {:?}", other),
534 }
535 }
536
537 #[tokio::test]
540 async fn simple_simple_roundtrip() {
541 let rng = SystemRandom::new();
542 let pkcs8 = ring_sig::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
543 let signer = DkimSigner::ed25519("example.com", "ed", pkcs8.as_ref())
544 .unwrap()
545 .header_canonicalization(CanonicalizationMethod::Simple)
546 .body_canonicalization(CanonicalizationMethod::Simple);
547
548 let headers = vec![
549 ("From", " user@example.com"),
550 ("Subject", " Simple test"),
551 ];
552 let body = b"Simple body\r\n";
553
554 let sig_value = signer.sign_message(&headers, body).unwrap();
555 assert!(sig_value.contains("c=simple/simple"));
556
557 let pub_key = signer.public_key_bytes();
558 let pub_b64 = BASE64.encode(&pub_key);
559
560 let mut resolver = MockResolver::new();
561 setup_mock_key(
562 &mut resolver,
563 "ed",
564 "example.com",
565 &format!("v=DKIM1; k=ed25519; p={}", pub_b64),
566 );
567
568 let verifier = DkimVerifier::new(resolver);
569 let mut all_headers = headers.clone();
570 all_headers.push(("DKIM-Signature", &sig_value));
571
572 let results = verifier.verify_message(&all_headers, body).await;
573 match &results[0] {
574 DkimResult::Pass { .. } => {}
575 other => panic!("simple/simple roundtrip: expected Pass, got {:?}", other),
576 }
577 }
578
579 #[tokio::test]
582 async fn timestamp_and_expiration_set() {
583 let rng = SystemRandom::new();
584 let pkcs8 = ring_sig::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
585 let signer = DkimSigner::ed25519("example.com", "ed", pkcs8.as_ref())
586 .unwrap()
587 .expiration(3600);
588
589 let headers = vec![("From", " user@example.com")];
590 let body = b"body\r\n";
591
592 let sig_value = signer.sign_message(&headers, body).unwrap();
593 assert!(sig_value.contains("t="));
594 assert!(sig_value.contains("x="));
595
596 let t_pos = sig_value.find("t=").unwrap() + 2;
598 let t_end = sig_value[t_pos..].find(';').unwrap() + t_pos;
599 let t: u64 = sig_value[t_pos..t_end].trim().parse().unwrap();
600
601 let x_pos = sig_value.find("x=").unwrap() + 2;
602 let x_end = sig_value[x_pos..].find(';').unwrap() + x_pos;
603 let x: u64 = sig_value[x_pos..x_end].trim().parse().unwrap();
604
605 assert_eq!(x, t + 3600);
606 }
607
608 #[test]
611 fn pem_decode_rsa_key() {
612 let pem = generate_rsa_pem();
613 let result = decode_pem(&pem);
614 assert!(result.is_ok());
615 }
616
617 #[test]
618 fn pem_decode_invalid_base64() {
619 let bad_pem = b"-----BEGIN PRIVATE KEY-----\n!!!invalid!!!\n-----END PRIVATE KEY-----\n";
620 let result = decode_pem(bad_pem);
621 assert!(result.is_err());
622 }
623
624 #[test]
625 fn pem_decode_empty() {
626 let result = decode_pem(b"no markers here");
627 assert!(result.is_err());
628 }
629
630 #[tokio::test]
633 async fn over_sign_roundtrip() {
634 let rng = SystemRandom::new();
635 let pkcs8 = ring_sig::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
636 let signer = DkimSigner::ed25519("example.com", "ed", pkcs8.as_ref())
637 .unwrap()
638 .over_sign(true);
639
640 let headers = vec![
641 ("From", " user@example.com"),
642 ("To", " other@example.com"),
643 ("Subject", " Over-sign test"),
644 ];
645 let body = b"Over-sign body\r\n";
646
647 let sig_value = signer.sign_message(&headers, body).unwrap();
648 assert!(sig_value.contains("h=from:from:to:to:subject:subject"));
650
651 let pub_key = signer.public_key_bytes();
652 let pub_b64 = BASE64.encode(&pub_key);
653
654 let mut resolver = MockResolver::new();
655 setup_mock_key(
656 &mut resolver,
657 "ed",
658 "example.com",
659 &format!("v=DKIM1; k=ed25519; p={}", pub_b64),
660 );
661
662 let verifier = DkimVerifier::new(resolver);
663 let mut all_headers = headers.clone();
664 all_headers.push(("DKIM-Signature", &sig_value));
665
666 let results = verifier.verify_message(&all_headers, body).await;
667 match &results[0] {
668 DkimResult::Pass { .. } => {}
669 other => panic!("over-sign roundtrip: expected Pass, got {:?}", other),
670 }
671 }
672
673 #[test]
680 fn signed_header_contains_required_tags() {
681 let rng = SystemRandom::new();
682 let pkcs8 = ring_sig::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
683 let signer = DkimSigner::ed25519("example.com", "sel", pkcs8.as_ref()).unwrap();
684
685 let headers = vec![("From", " user@example.com")];
686 let sig = signer.sign_message(&headers, b"body\r\n").unwrap();
687
688 assert!(sig.contains("v=1"));
689 assert!(sig.contains("a=ed25519-sha256"));
690 assert!(sig.contains("d=example.com"));
691 assert!(sig.contains("s=sel"));
692 assert!(sig.contains("h="));
693 assert!(sig.contains("bh="));
694 assert!(sig.contains("b="));
695 assert!(sig.contains("t="));
696 }
697
698 #[test]
701 fn default_headers_include_recommended() {
702 let defaults = default_headers();
703 assert!(defaults.contains(&"from".to_string()));
704 assert!(defaults.contains(&"to".to_string()));
705 assert!(defaults.contains(&"subject".to_string()));
706 assert!(defaults.contains(&"date".to_string()));
707 assert!(defaults.contains(&"message-id".to_string()));
708 assert!(!defaults.contains(&"received".to_string()));
710 assert!(!defaults.contains(&"return-path".to_string()));
711 }
712
713 #[test]
716 fn signature_has_timestamp() {
717 let rng = SystemRandom::new();
718 let pkcs8 = ring_sig::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
719 let signer = DkimSigner::ed25519("example.com", "sel", pkcs8.as_ref()).unwrap();
720
721 let headers = vec![("From", " user@example.com")];
722 let sig = signer.sign_message(&headers, b"body\r\n").unwrap();
723 assert!(sig.contains("t="));
724
725 let t_pos = sig.find("t=").unwrap() + 2;
727 let t_end = sig[t_pos..].find(';').unwrap() + t_pos;
728 let t: u64 = sig[t_pos..t_end].trim().parse().unwrap();
729 let now = SystemTime::now()
730 .duration_since(UNIX_EPOCH)
731 .unwrap()
732 .as_secs();
733 assert!(t <= now);
734 assert!(t > now - 10); }
736
737 fn generate_rsa_pem() -> Vec<u8> {
740 include_bytes!("../../tests/fixtures/rsa2048.pem").to_vec()
746 }
747}