1use super::{
11 Algorithm, AtpsRecord, Canonicalization, DkimError, DkimReportRecord, Flag, HashAlgorithm,
12 RR_DNS, RR_OTHER, RR_POLICY, Service, Signature,
13};
14use crate::DnsError;
15use crate::{
16 Error,
17 crypto::VerifyingKeyType,
18 dkim::DomainKey,
19 dkim::{
20 RR_EXPIRATION, RR_SIGNATURE, RR_UNKNOWN_TAG, RR_VERIFICATION, record::R_MULTIPLE_RECORDS,
21 },
22 parse::*,
23};
24use std::slice::Iter;
25
26const ATPSH: u64 = (b'a' as u64)
27 | ((b't' as u64) << 8)
28 | ((b'p' as u64) << 16)
29 | ((b's' as u64) << 24)
30 | ((b'h' as u64) << 32);
31const ATPS: u64 =
32 (b'a' as u64) | ((b't' as u64) << 8) | ((b'p' as u64) << 16) | ((b's' as u64) << 24);
33const NONE: u64 =
34 (b'n' as u64) | ((b'o' as u64) << 8) | ((b'n' as u64) << 16) | ((b'e' as u64) << 24);
35const SHA256: u64 = (b's' as u64)
36 | ((b'h' as u64) << 8)
37 | ((b'a' as u64) << 16)
38 | ((b'2' as u64) << 24)
39 | ((b'5' as u64) << 32)
40 | ((b'6' as u64) << 40);
41const SHA1: u64 =
42 (b's' as u64) | ((b'h' as u64) << 8) | ((b'a' as u64) << 16) | ((b'1' as u64) << 24);
43const RA: u64 = (b'r' as u64) | ((b'a' as u64) << 8);
44const RP: u64 = (b'r' as u64) | ((b'p' as u64) << 8);
45const RR: u64 = (b'r' as u64) | ((b'r' as u64) << 8);
46const RS: u64 = (b'r' as u64) | ((b's' as u64) << 8);
47const ALL: u64 = (b'a' as u64) | ((b'l' as u64) << 8) | ((b'l' as u64) << 16);
48
49impl Signature {
50 #[allow(clippy::while_let_on_iterator)]
67 pub fn parse(header: &'_ [u8]) -> crate::Result<Self> {
68 let mut signature = Signature {
69 v: 0,
70 a: Algorithm::RsaSha256,
71 d: "".into(),
72 s: "".into(),
73 i: "".into(),
74 b: Vec::with_capacity(0),
75 bh: Vec::with_capacity(0),
76 h: Vec::with_capacity(0),
77 z: Vec::with_capacity(0),
78 l: 0,
79 x: 0,
80 t: 0,
81 ch: Canonicalization::Simple,
82 cb: Canonicalization::Simple,
83 r: false,
84 atps: None,
85 atpsh: None,
86 };
87 let mut header = header.iter();
88
89 while let Some(key) = header.key() {
90 match key {
91 V => {
92 signature.v = header.number().unwrap_or(0) as u32;
93 if signature.v != 1 {
94 return Err(Error::Dkim(DkimError::UnsupportedVersion));
95 }
96 }
97 A => {
98 signature.a = header.algorithm()?;
99 }
100 B => signature.b = header.base64().ok_or(Error::Base64)?,
101 BH => signature.bh = header.base64().ok_or(Error::Base64)?,
102 C => {
103 let (ch, cb) = header.canonicalization(Canonicalization::Simple)?;
104 signature.ch = ch;
105 signature.cb = cb;
106 }
107 D => signature.d = header.text(true),
108 H => signature.h = header.items(),
109 I => signature.i = header.text_qp(true).ok_or(Error::Parse)?,
110 L => signature.l = header.number().unwrap_or(0),
111 S => signature.s = header.text(true),
112 T => signature.t = header.number().unwrap_or(0),
113 X => signature.x = header.number().unwrap_or(0),
114 Z => signature.z = header.headers_qp(),
115 R => signature.r = header.value() == Y,
116 ATPS => {
117 if signature.atps.is_none() {
118 signature.atps = Some(header.text(true));
119 }
120 }
121 ATPSH => {
122 signature.atpsh = match header.value() {
123 SHA256 => HashAlgorithm::Sha256.into(),
124 SHA1 => HashAlgorithm::Sha1.into(),
125 NONE => None,
126 _ => {
127 signature.atps = Some("".into());
128 None
129 }
130 };
131 }
132 _ => header.ignore(),
133 }
134 }
135
136 if !signature.d.is_empty()
137 && !signature.s.is_empty()
138 && !signature.b.is_empty()
139 && !signature.bh.is_empty()
140 && !signature.h.is_empty()
141 {
142 Ok(signature)
143 } else {
144 Err(Error::MissingParameters)
145 }
146 }
147}
148
149pub(crate) trait SignatureParser: Sized {
150 fn canonicalization(
151 &mut self,
152 default: Canonicalization,
153 ) -> crate::Result<(Canonicalization, Canonicalization)>;
154 fn algorithm(&mut self) -> crate::Result<Algorithm>;
155}
156
157impl SignatureParser for Iter<'_, u8> {
158 fn canonicalization(
159 &mut self,
160 default: Canonicalization,
161 ) -> crate::Result<(Canonicalization, Canonicalization)> {
162 let mut cb = default;
163 let mut ch = default;
164
165 let mut has_header = false;
166 let mut c = None;
167
168 while let Some(char) = self.next() {
169 match (char, c) {
170 (b's' | b'S', None) => {
171 if self.match_bytes(b"imple") {
172 c = Canonicalization::Simple.into();
173 } else {
174 return Err(Error::Dkim(DkimError::UnsupportedCanonicalization));
175 }
176 }
177 (b'r' | b'R', None) => {
178 if self.match_bytes(b"elaxed") {
179 c = Canonicalization::Relaxed.into();
180 } else {
181 return Err(Error::Dkim(DkimError::UnsupportedCanonicalization));
182 }
183 }
184 (b'/', Some(c_)) => {
185 ch = c_;
186 c = None;
187 has_header = true;
188 }
189 (b';', _) => {
190 break;
191 }
192 (_, _) => {
193 if !char.is_ascii_whitespace() {
194 return Err(Error::Dkim(DkimError::UnsupportedCanonicalization));
195 }
196 }
197 }
198 }
199
200 if let Some(c) = c {
201 if has_header {
202 cb = c;
203 } else {
204 ch = c;
205 }
206 }
207
208 Ok((ch, cb))
209 }
210
211 fn algorithm(&mut self) -> crate::Result<Algorithm> {
212 match self.next_skip_whitespaces().unwrap_or(0) {
213 b'r' | b'R' => {
214 if self.match_bytes(b"sa-sha") {
215 let mut algo = 0;
216
217 for ch in self {
218 match ch {
219 b'1' if algo == 0 => algo = 1,
220 b'2' if algo == 0 => algo = 2,
221 b'5' if algo == 2 => algo = 25,
222 b'6' if algo == 25 => algo = 256,
223 b';' => {
224 break;
225 }
226 _ => {
227 if !ch.is_ascii_whitespace() {
228 return Err(Error::Dkim(DkimError::UnsupportedAlgorithm));
229 }
230 }
231 }
232 }
233
234 match algo {
235 256 => Ok(Algorithm::RsaSha256),
236 1 => Ok(Algorithm::RsaSha1),
237 _ => Err(Error::Dkim(DkimError::UnsupportedAlgorithm)),
238 }
239 } else {
240 Err(Error::Dkim(DkimError::UnsupportedAlgorithm))
241 }
242 }
243 b'e' | b'E' => {
244 if self.match_bytes(b"d25519-sha256") && self.seek_tag_end() {
245 Ok(Algorithm::Ed25519Sha256)
246 } else {
247 Err(Error::Dkim(DkimError::UnsupportedAlgorithm))
248 }
249 }
250 _ => Err(Error::Dkim(DkimError::UnsupportedAlgorithm)),
251 }
252 }
253}
254
255impl TxtRecordParser for DomainKey {
256 #[allow(clippy::while_let_on_iterator)]
257 fn parse(header: &[u8]) -> crate::Result<Self> {
258 let mut header = header.iter();
259 let mut flags = 0;
260 let mut key_type = VerifyingKeyType::Rsa;
261 let mut public_key = None;
262
263 while let Some(key) = header.key() {
264 match key {
265 V => {
266 if !header.match_bytes(b"DKIM1") || !header.seek_tag_end() {
267 return Err(Error::Dns(DnsError::InvalidRecordType));
268 }
269 }
270 H => flags |= header.flags::<HashAlgorithm>(),
271 P => {
272 if let Some(bytes) = header.base64() {
273 public_key = Some(bytes);
274 }
275 }
276 S => flags |= header.flags::<Service>(),
277 T => flags |= header.flags::<Flag>(),
278 K => {
279 if let Some(ch) = header.next_skip_whitespaces() {
280 match ch {
281 b'r' | b'R' if header.match_bytes(b"sa") && header.seek_tag_end() => {
282 key_type = VerifyingKeyType::Rsa;
283 }
284 b'e' | b'E'
285 if header.match_bytes(b"d25519") && header.seek_tag_end() =>
286 {
287 key_type = VerifyingKeyType::Ed25519;
288 }
289 b'm' | b'M'
290 if header.match_bytes(b"ldsa44") && header.seek_tag_end() =>
291 {
292 key_type = VerifyingKeyType::MlDsa44;
293 }
294 b';' => (),
295 _ => {
296 return Err(Error::Dkim(DkimError::UnsupportedKeyType));
297 }
298 }
299 }
300 }
301 _ => {
302 header.ignore();
303 }
304 }
305 }
306
307 match public_key {
308 Some(public_key) if public_key.is_empty() => {
309 Err(Error::Dkim(DkimError::PublicKeyRevoked))
310 }
311 Some(public_key) => Ok(DomainKey {
312 p: key_type.verifying_key(&public_key)?,
313 f: flags,
314 }),
315 _ => Err(Error::Dns(DnsError::InvalidRecordType)),
316 }
317 }
318
319 fn parse_records(records: &[Vec<u8>]) -> crate::Result<Self> {
320 let mut result = Err(Error::Dns(DnsError::InvalidRecordType));
321 for record in records {
322 result = Self::parse(record);
323 if let Ok(key) = &mut result {
324 if records.len() > 1 {
325 key.f |= R_MULTIPLE_RECORDS;
326 }
327 break;
328 }
329 }
330 result
331 }
332}
333
334impl TxtRecordParser for DkimReportRecord {
335 #[allow(clippy::while_let_on_iterator)]
336 fn parse(header: &[u8]) -> crate::Result<Self> {
337 let mut header = header.iter();
338 let mut record = DkimReportRecord {
339 ra: String::new(),
340 rp: 100,
341 rr: u8::MAX,
342 rs: None,
343 };
344
345 while let Some(key) = header.key() {
346 match key {
347 RA => {
348 record.ra = header.text_qp(true).unwrap_or_default();
349 }
350 RP => {
351 record.rp = std::cmp::min(header.number().unwrap_or(0), 100) as u8;
352 }
353 RS => {
354 record.rs = header.text_qp(false);
355 }
356 RR => {
357 record.rr = 0;
358 loop {
359 let (val, stop_char) = header.flag_value();
360 match val {
361 ALL => {
362 record.rr = u8::MAX;
363 }
364 D => {
365 record.rr |= RR_DNS;
366 }
367 O => {
368 record.rr |= RR_OTHER;
369 }
370 P => {
371 record.rr |= RR_POLICY;
372 }
373 S => {
374 record.rr |= RR_SIGNATURE;
375 }
376 U => {
377 record.rr |= RR_UNKNOWN_TAG;
378 }
379 V => {
380 record.rr |= RR_VERIFICATION;
381 }
382 X => {
383 record.rr |= RR_EXPIRATION;
384 }
385 _ => (),
386 }
387
388 if stop_char != b':' {
389 break;
390 }
391 }
392 }
393
394 _ => {
395 header.ignore();
396 }
397 }
398 }
399
400 if !record.ra.is_empty() {
401 Ok(record)
402 } else {
403 Err(Error::Dns(DnsError::InvalidRecordType))
404 }
405 }
406}
407
408impl TxtRecordParser for AtpsRecord {
409 #[allow(clippy::while_let_on_iterator)]
410 fn parse(header: &[u8]) -> crate::Result<Self> {
411 let mut header = header.iter();
412 let mut record = AtpsRecord { d: None };
413 let mut has_version = false;
414
415 while let Some(key) = header.key() {
416 match key {
417 V => {
418 if !header.match_bytes(b"ATPS1") || !header.seek_tag_end() {
419 return Err(Error::Dns(DnsError::InvalidRecordType));
420 }
421 has_version = true;
422 }
423 D => {
424 record.d = header.text(true).into();
425 }
426 _ => {
427 header.ignore();
428 }
429 }
430 }
431
432 if !has_version {
433 return Err(Error::Dns(DnsError::InvalidRecordType));
434 }
435
436 Ok(record)
437 }
438}
439
440impl ItemParser for HashAlgorithm {
441 fn parse(bytes: &[u8]) -> Option<Self> {
442 if bytes.eq_ignore_ascii_case(b"sha256") {
443 HashAlgorithm::Sha256.into()
444 } else if bytes.eq_ignore_ascii_case(b"sha1") {
445 HashAlgorithm::Sha1.into()
446 } else {
447 None
448 }
449 }
450}
451
452impl ItemParser for Flag {
453 fn parse(bytes: &[u8]) -> Option<Self> {
454 if bytes.eq_ignore_ascii_case(b"y") {
455 Flag::Testing.into()
456 } else if bytes.eq_ignore_ascii_case(b"s") {
457 Flag::MatchDomain.into()
458 } else {
459 None
460 }
461 }
462}
463
464impl ItemParser for Service {
465 fn parse(bytes: &[u8]) -> Option<Self> {
466 if bytes.eq(b"*") {
467 Service::All.into()
468 } else if bytes.eq_ignore_ascii_case(b"email") {
469 Service::Email.into()
470 } else {
471 None
472 }
473 }
474}
475
476#[cfg(test)]
477mod test {
478 use encodify::base64;
479
480 use crate::{
481 Error,
482 crypto::{Algorithm, R_HASH_SHA1, R_HASH_SHA256},
483 dkim::DomainKey,
484 dkim::{
485 Canonicalization, DkimReportRecord, RR_DNS, RR_EXPIRATION, RR_OTHER, RR_POLICY,
486 RR_SIGNATURE, RR_UNKNOWN_TAG, RR_VERIFICATION, Signature,
487 record::{R_FLAG_MATCH_DOMAIN, R_FLAG_TESTING, R_SVC_ALL, R_SVC_EMAIL},
488 },
489 headers::HeaderWriter,
490 parse::TxtRecordParser,
491 };
492
493 #[test]
494 fn dkim_signature_parse() {
495 for (signature, expected_result) in [
496 (
497 concat!(
498 "v=1; a=rsa-sha256; s=default; d=stalw.art; c=relaxed/relaxed; ",
499 "bh=QoiUNYyUV+1tZ/xUPRcE+gST2zAStvJx1OK078Ylm5s=; ",
500 "b=Du0rvdzNodI6b5bhlUaZZ+gpXJi0VwjY/3qL7lS0wzKutNVCbvdJuZObGdAcv\n",
501 " eVI/RNQh2gxW4H2ynMS3B+Unse1YLJQwdjuGxsCEKBqReKlsEKT8JlO/7b2AvxR\n",
502 "\t9Q+M2aHD5kn9dbNIKnN/PKouutaXmm18QwL5EPEN9DHXSqQ=;",
503 "h=Subject:To:From; t=311923920",
504 ),
505 Signature {
506 v: 1,
507 a: Algorithm::RsaSha256,
508 d: "stalw.art".into(),
509 s: "default".into(),
510 i: "".into(),
511 bh: base64::LENIENT
512 .decode(b"QoiUNYyUV+1tZ/xUPRcE+gST2zAStvJx1OK078Ylm5s=")
513 .unwrap(),
514 b: base64::LENIENT
515 .decode(
516 concat!(
517 "Du0rvdzNodI6b5bhlUaZZ+gpXJi0VwjY/3qL7lS0wzKutNVCbvdJuZObGdAcv",
518 "eVI/RNQh2gxW4H2ynMS3B+Unse1YLJQwdjuGxsCEKBqReKlsEKT8JlO/7b2AvxR",
519 "9Q+M2aHD5kn9dbNIKnN/PKouutaXmm18QwL5EPEN9DHXSqQ="
520 )
521 .as_bytes(),
522 )
523 .unwrap(),
524 h: vec!["Subject".into(), "To".into(), "From".into()],
525 z: vec![],
526 l: 0,
527 x: 0,
528 t: 311923920,
529 ch: Canonicalization::Relaxed,
530 cb: Canonicalization::Relaxed,
531 r: false,
532 atps: None,
533 atpsh: None,
534 },
535 ),
536 (
537 concat!(
538 "v=1; a=rsa-sha1; d=example.net; s=brisbane;\r\n",
539 " c=simple; q=dns/txt; i=@eng.example.net;\r\n",
540 " t=1117574938; x=1118006938;\r\n",
541 " h=from:to:subject:date;\r\n",
542 " z=From:foo@eng.example.net|To:joe@example.com|\r\n",
543 " Subject:demo=20run|Date:July=205,=202005=203:44:08=20PM=20-0700;\r\n",
544 " bh=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=;\r\n",
545 " b=dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD00lszZVoG4ZHRNiYzR",
546 ),
547 Signature {
548 v: 1,
549 a: Algorithm::RsaSha1,
550 d: "example.net".into(),
551 s: "brisbane".into(),
552 i: "@eng.example.net".into(),
553 bh: base64::LENIENT
554 .decode(b"MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=")
555 .unwrap(),
556 b: base64::LENIENT
557 .decode(
558 concat!(
559 "dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGe",
560 "eruD00lszZVoG4ZHRNiYzR"
561 )
562 .as_bytes(),
563 )
564 .unwrap(),
565 h: vec!["from".into(), "to".into(), "subject".into(), "date".into()],
566 z: vec![
567 "From:foo@eng.example.net".into(),
568 "To:joe@example.com".into(),
569 "Subject:demo run".into(),
570 "Date:July 5, 2005 3:44:08 PM -0700".into(),
571 ],
572 l: 0,
573 x: 1118006938,
574 t: 1117574938,
575 ch: Canonicalization::Simple,
576 cb: Canonicalization::Simple,
577 r: false,
578 atps: None,
579 atpsh: None,
580 },
581 ),
582 (
583 concat!(
584 "v=1; a = rsa - sha256; s = brisbane; d = example.com; \r\n",
585 "c = simple / relaxed; q=dns/txt; i = \r\n joe=20@\r\n",
586 " football.example.com; \r\n",
587 "h=Received : From : To :\r\n Subject : : Date : Message-ID::;;;; \r\n",
588 "bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=; \r\n",
589 "b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB \r\n",
590 "4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut \r\n",
591 "KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV \r\n",
592 "4bmp/YzhwvcubU4=; l = 123",
593 ),
594 Signature {
595 v: 1,
596 a: Algorithm::RsaSha256,
597 d: "example.com".into(),
598 s: "brisbane".into(),
599 i: "joe @football.example.com".into(),
600 bh: base64::LENIENT
601 .decode(b"2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=")
602 .unwrap(),
603 b: base64::LENIENT
604 .decode(
605 concat!(
606 "AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB",
607 "4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut",
608 "KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV",
609 "4bmp/YzhwvcubU4="
610 )
611 .as_bytes(),
612 )
613 .unwrap(),
614 h: vec![
615 "Received".into(),
616 "From".into(),
617 "To".into(),
618 "Subject".into(),
619 "Date".into(),
620 "Message-ID".into(),
621 ],
622 z: vec![],
623 l: 123,
624 x: 0,
625 t: 0,
626 ch: Canonicalization::Simple,
627 cb: Canonicalization::Relaxed,
628 r: false,
629 atps: None,
630 atpsh: None,
631 },
632 ),
633 ] {
634 let result = Signature::parse(signature.as_bytes()).unwrap();
635 assert_eq!(result.v, expected_result.v, "{signature:?}");
636 assert_eq!(result.a, expected_result.a, "{signature:?}");
637 assert_eq!(result.d, expected_result.d, "{signature:?}");
638 assert_eq!(result.s, expected_result.s, "{signature:?}");
639 assert_eq!(result.i, expected_result.i, "{signature:?}");
640 assert_eq!(result.b, expected_result.b, "{signature:?}");
641 assert_eq!(result.bh, expected_result.bh, "{signature:?}");
642 assert_eq!(result.h, expected_result.h, "{signature:?}");
643 assert_eq!(result.z, expected_result.z, "{signature:?}");
644 assert_eq!(result.l, expected_result.l, "{signature:?}");
645 assert_eq!(result.x, expected_result.x, "{signature:?}");
646 assert_eq!(result.t, expected_result.t, "{signature:?}");
647 assert_eq!(result.ch, expected_result.ch, "{signature:?}");
648 assert_eq!(result.cb, expected_result.cb, "{signature:?}");
649
650 let header = result.to_header();
651 let value = header.strip_prefix("DKIM-Signature: ").unwrap();
652 let mut round_trip = Signature::parse(value.as_bytes()).unwrap();
653 round_trip.z = result.z.clone();
654 assert_eq!(round_trip, result, "{header:?}");
655 }
656 }
657
658 #[test]
659 fn dkim_signature_auid_qp() {
660 for (auid, expected) in [
661 (
662 "joe=40football.example.com",
663 Some("joe@football.example.com"),
664 ),
665 ("joe=3db@example.com", Some("joe=b@example.com")),
666 ("jo\r\n\te=\r\n\t40example.com", Some("joe@example.com")),
667 ("x=zz@evil.example", None),
668 ("joe=4@example.com", None),
669 ("joe@example.com=", None),
670 ] {
671 let header = format!(
672 "v=1; a=rsa-sha256; d=example.com; s=sel; h=from; i={auid}; bh=MTIz; b=dzdV;"
673 );
674 match (Signature::parse(header.as_bytes()), expected) {
675 (Ok(signature), Some(expected)) => assert_eq!(signature.i, expected, "{auid:?}"),
676 (Err(Error::Parse), None) => {}
677 (result, _) => panic!("{auid:?}: unexpected {result:?}"),
678 }
679 }
680 }
681
682 #[test]
683 fn dkim_record_parse() {
684 for (record, expected_result) in [
685 (
686 concat!(
687 "v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ",
688 "KBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYt",
689 "IxN2SnFCjxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v",
690 "/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhi",
691 "tdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB",
692 ),
693 0,
694 ),
695 (
696 concat!(
697 "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOC",
698 "AQ8AMIIBCgKCAQEAvzwKQIIWzQXv0nihasFTT3+JO23hXCg",
699 "e+ESWNxCJdVLxKL5edxrumEU3DnrPeGD6q6E/vjoXwBabpm",
700 "8F5o96MEPm7v12O5IIK7wx7gIJiQWvexwh+GJvW4aFFa0g1",
701 "3Ai75UdZjGFNKHAEGeLmkQYybK/EHW5ymRlSg3g8zydJGEc",
702 "I/melLCiBoShHjfZFJEThxLmPHNSi+KOUMypxqYHd7hzg6W",
703 "7qnq6t9puZYXMWj6tEaf6ORWgb7DOXZSTJJjAJPBWa2+Urx",
704 "XX6Ro7L7Xy1zzeYFCk8W5vmn0wMgGpjkWw0ljJWNwIpxZAj9",
705 "p5wMedWasaPS74TZ1b7tI39ncp6QIDAQAB ; t= y : s :yy:x;",
706 "s=*:email;; h= sha1:sha 256:other;; n=ignore these notes "
707 ),
708 R_HASH_SHA1
709 | R_HASH_SHA256
710 | R_SVC_ALL
711 | R_SVC_EMAIL
712 | R_FLAG_MATCH_DOMAIN
713 | R_FLAG_TESTING,
714 ),
715 (
716 concat!(
717 "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYtb/9Sh8nGKV7exhUFS",
718 "+cBNXlHgO1CxD9zIfQd5ztlq1LO7g38dfmFpQafh9lKgqPBTolFhZxhF1yUNT",
719 "hpV673NdAtaCVGNyx/fTYtvyyFe9DH2tmm/ijLlygDRboSkIJ4NHZjK++48hk",
720 "NP8/htqWHS+CvwWT4Qgs0NtB7Re9bQIDAQAB"
721 ),
722 0,
723 ),
724 ] {
725 assert_eq!(
726 DomainKey::parse(record.as_bytes()).unwrap().f,
727 expected_result
728 );
729 }
730 }
731
732 #[test]
733 fn dkim_record_empty_p_is_revoked() {
734 use crate::{Error, dkim::DkimError};
735 for record in ["v=DKIM1; p=", "v=DKIM1; k=rsa; p=;", "p="] {
736 assert!(
737 matches!(
738 DomainKey::parse(record.as_bytes()),
739 Err(Error::Dkim(DkimError::PublicKeyRevoked))
740 ),
741 "{record:?}"
742 );
743 }
744 }
745
746 #[test]
747 fn dkim_report_record_parse() {
748 for (record, expected_result) in [
749 (
750 "ra=dkim-errors; rp=97; rr=v:x",
751 DkimReportRecord {
752 ra: "dkim-errors".to_string(),
753 rp: 97,
754 rr: RR_VERIFICATION | RR_EXPIRATION,
755 rs: None,
756 },
757 ),
758 (
759 "ra=postmaster; rp=1; rr=d:o:p:s:u:v:x; rs=Error=20Message;",
760 DkimReportRecord {
761 ra: "postmaster".to_string(),
762 rp: 1,
763 rr: RR_DNS
764 | RR_OTHER
765 | RR_POLICY
766 | RR_SIGNATURE
767 | RR_UNKNOWN_TAG
768 | RR_VERIFICATION
769 | RR_EXPIRATION,
770 rs: "Error Message".to_string().into(),
771 },
772 ),
773 (
774 "ra=dkim=2Derrors; rs=bad=zz;",
775 DkimReportRecord {
776 ra: "dkim-errors".to_string(),
777 rp: 100,
778 rr: u8::MAX,
779 rs: None,
780 },
781 ),
782 ] {
783 assert_eq!(
784 DkimReportRecord::parse(record.as_bytes()).unwrap(),
785 expected_result
786 );
787 }
788 }
789}