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-ed25519-sha512")
291 && header.seek_tag_end() =>
292 {
293 key_type = VerifyingKeyType::MlDsa44Ed25519;
294 }
295 b';' => (),
296 _ => {
297 return Err(Error::Dkim(DkimError::UnsupportedKeyType));
298 }
299 }
300 }
301 }
302 _ => {
303 header.ignore();
304 }
305 }
306 }
307
308 match public_key {
309 Some(public_key) if public_key.is_empty() => {
310 Err(Error::Dkim(DkimError::PublicKeyRevoked))
311 }
312 Some(public_key) => Ok(DomainKey {
313 p: key_type.verifying_key(&public_key)?,
314 f: flags,
315 }),
316 _ => Err(Error::Dns(DnsError::InvalidRecordType)),
317 }
318 }
319
320 fn parse_records(records: &[Vec<u8>]) -> crate::Result<Self> {
321 let mut result = Err(Error::Dns(DnsError::InvalidRecordType));
322 for record in records {
323 result = Self::parse(record);
324 if let Ok(key) = &mut result {
325 if records.len() > 1 {
326 key.f |= R_MULTIPLE_RECORDS;
327 }
328 break;
329 }
330 }
331 result
332 }
333}
334
335impl TxtRecordParser for DkimReportRecord {
336 #[allow(clippy::while_let_on_iterator)]
337 fn parse(header: &[u8]) -> crate::Result<Self> {
338 let mut header = header.iter();
339 let mut record = DkimReportRecord {
340 ra: String::new(),
341 rp: 100,
342 rr: u8::MAX,
343 rs: None,
344 };
345
346 while let Some(key) = header.key() {
347 match key {
348 RA => {
349 record.ra = header.text_qp(true).unwrap_or_default();
350 }
351 RP => {
352 record.rp = std::cmp::min(header.number().unwrap_or(0), 100) as u8;
353 }
354 RS => {
355 record.rs = header.text_qp(false);
356 }
357 RR => {
358 record.rr = 0;
359 loop {
360 let (val, stop_char) = header.flag_value();
361 match val {
362 ALL => {
363 record.rr = u8::MAX;
364 }
365 D => {
366 record.rr |= RR_DNS;
367 }
368 O => {
369 record.rr |= RR_OTHER;
370 }
371 P => {
372 record.rr |= RR_POLICY;
373 }
374 S => {
375 record.rr |= RR_SIGNATURE;
376 }
377 U => {
378 record.rr |= RR_UNKNOWN_TAG;
379 }
380 V => {
381 record.rr |= RR_VERIFICATION;
382 }
383 X => {
384 record.rr |= RR_EXPIRATION;
385 }
386 _ => (),
387 }
388
389 if stop_char != b':' {
390 break;
391 }
392 }
393 }
394
395 _ => {
396 header.ignore();
397 }
398 }
399 }
400
401 if !record.ra.is_empty() {
402 Ok(record)
403 } else {
404 Err(Error::Dns(DnsError::InvalidRecordType))
405 }
406 }
407}
408
409impl TxtRecordParser for AtpsRecord {
410 #[allow(clippy::while_let_on_iterator)]
411 fn parse(header: &[u8]) -> crate::Result<Self> {
412 let mut header = header.iter();
413 let mut record = AtpsRecord { d: None };
414 let mut has_version = false;
415
416 while let Some(key) = header.key() {
417 match key {
418 V => {
419 if !header.match_bytes(b"ATPS1") || !header.seek_tag_end() {
420 return Err(Error::Dns(DnsError::InvalidRecordType));
421 }
422 has_version = true;
423 }
424 D => {
425 record.d = header.text(true).into();
426 }
427 _ => {
428 header.ignore();
429 }
430 }
431 }
432
433 if !has_version {
434 return Err(Error::Dns(DnsError::InvalidRecordType));
435 }
436
437 Ok(record)
438 }
439}
440
441impl ItemParser for HashAlgorithm {
442 fn parse(bytes: &[u8]) -> Option<Self> {
443 if bytes.eq_ignore_ascii_case(b"sha256") {
444 HashAlgorithm::Sha256.into()
445 } else if bytes.eq_ignore_ascii_case(b"sha1") {
446 HashAlgorithm::Sha1.into()
447 } else {
448 None
449 }
450 }
451}
452
453impl ItemParser for Flag {
454 fn parse(bytes: &[u8]) -> Option<Self> {
455 if bytes.eq_ignore_ascii_case(b"y") {
456 Flag::Testing.into()
457 } else if bytes.eq_ignore_ascii_case(b"s") {
458 Flag::MatchDomain.into()
459 } else {
460 None
461 }
462 }
463}
464
465impl ItemParser for Service {
466 fn parse(bytes: &[u8]) -> Option<Self> {
467 if bytes.eq(b"*") {
468 Service::All.into()
469 } else if bytes.eq_ignore_ascii_case(b"email") {
470 Service::Email.into()
471 } else {
472 None
473 }
474 }
475}
476
477#[cfg(test)]
478mod test {
479 use encodify::base64;
480
481 use crate::{
482 Error,
483 crypto::{Algorithm, R_HASH_SHA1, R_HASH_SHA256},
484 dkim::DomainKey,
485 dkim::{
486 Canonicalization, DkimReportRecord, RR_DNS, RR_EXPIRATION, RR_OTHER, RR_POLICY,
487 RR_SIGNATURE, RR_UNKNOWN_TAG, RR_VERIFICATION, Signature,
488 record::{R_FLAG_MATCH_DOMAIN, R_FLAG_TESTING, R_SVC_ALL, R_SVC_EMAIL},
489 },
490 headers::HeaderWriter,
491 parse::TxtRecordParser,
492 };
493
494 #[test]
495 fn dkim_signature_parse() {
496 for (signature, expected_result) in [
497 (
498 concat!(
499 "v=1; a=rsa-sha256; s=default; d=stalw.art; c=relaxed/relaxed; ",
500 "bh=QoiUNYyUV+1tZ/xUPRcE+gST2zAStvJx1OK078Ylm5s=; ",
501 "b=Du0rvdzNodI6b5bhlUaZZ+gpXJi0VwjY/3qL7lS0wzKutNVCbvdJuZObGdAcv\n",
502 " eVI/RNQh2gxW4H2ynMS3B+Unse1YLJQwdjuGxsCEKBqReKlsEKT8JlO/7b2AvxR\n",
503 "\t9Q+M2aHD5kn9dbNIKnN/PKouutaXmm18QwL5EPEN9DHXSqQ=;",
504 "h=Subject:To:From; t=311923920",
505 ),
506 Signature {
507 v: 1,
508 a: Algorithm::RsaSha256,
509 d: "stalw.art".into(),
510 s: "default".into(),
511 i: "".into(),
512 bh: base64::LENIENT
513 .decode(b"QoiUNYyUV+1tZ/xUPRcE+gST2zAStvJx1OK078Ylm5s=")
514 .unwrap(),
515 b: base64::LENIENT
516 .decode(
517 concat!(
518 "Du0rvdzNodI6b5bhlUaZZ+gpXJi0VwjY/3qL7lS0wzKutNVCbvdJuZObGdAcv",
519 "eVI/RNQh2gxW4H2ynMS3B+Unse1YLJQwdjuGxsCEKBqReKlsEKT8JlO/7b2AvxR",
520 "9Q+M2aHD5kn9dbNIKnN/PKouutaXmm18QwL5EPEN9DHXSqQ="
521 )
522 .as_bytes(),
523 )
524 .unwrap(),
525 h: vec!["Subject".into(), "To".into(), "From".into()],
526 z: vec![],
527 l: 0,
528 x: 0,
529 t: 311923920,
530 ch: Canonicalization::Relaxed,
531 cb: Canonicalization::Relaxed,
532 r: false,
533 atps: None,
534 atpsh: None,
535 },
536 ),
537 (
538 concat!(
539 "v=1; a=rsa-sha1; d=example.net; s=brisbane;\r\n",
540 " c=simple; q=dns/txt; i=@eng.example.net;\r\n",
541 " t=1117574938; x=1118006938;\r\n",
542 " h=from:to:subject:date;\r\n",
543 " z=From:foo@eng.example.net|To:joe@example.com|\r\n",
544 " Subject:demo=20run|Date:July=205,=202005=203:44:08=20PM=20-0700;\r\n",
545 " bh=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=;\r\n",
546 " b=dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD00lszZVoG4ZHRNiYzR",
547 ),
548 Signature {
549 v: 1,
550 a: Algorithm::RsaSha1,
551 d: "example.net".into(),
552 s: "brisbane".into(),
553 i: "@eng.example.net".into(),
554 bh: base64::LENIENT
555 .decode(b"MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=")
556 .unwrap(),
557 b: base64::LENIENT
558 .decode(
559 concat!(
560 "dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGe",
561 "eruD00lszZVoG4ZHRNiYzR"
562 )
563 .as_bytes(),
564 )
565 .unwrap(),
566 h: vec!["from".into(), "to".into(), "subject".into(), "date".into()],
567 z: vec![
568 "From:foo@eng.example.net".into(),
569 "To:joe@example.com".into(),
570 "Subject:demo run".into(),
571 "Date:July 5, 2005 3:44:08 PM -0700".into(),
572 ],
573 l: 0,
574 x: 1118006938,
575 t: 1117574938,
576 ch: Canonicalization::Simple,
577 cb: Canonicalization::Simple,
578 r: false,
579 atps: None,
580 atpsh: None,
581 },
582 ),
583 (
584 concat!(
585 "v=1; a = rsa - sha256; s = brisbane; d = example.com; \r\n",
586 "c = simple / relaxed; q=dns/txt; i = \r\n joe=20@\r\n",
587 " football.example.com; \r\n",
588 "h=Received : From : To :\r\n Subject : : Date : Message-ID::;;;; \r\n",
589 "bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=; \r\n",
590 "b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB \r\n",
591 "4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut \r\n",
592 "KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV \r\n",
593 "4bmp/YzhwvcubU4=; l = 123",
594 ),
595 Signature {
596 v: 1,
597 a: Algorithm::RsaSha256,
598 d: "example.com".into(),
599 s: "brisbane".into(),
600 i: "joe @football.example.com".into(),
601 bh: base64::LENIENT
602 .decode(b"2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=")
603 .unwrap(),
604 b: base64::LENIENT
605 .decode(
606 concat!(
607 "AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB",
608 "4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut",
609 "KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV",
610 "4bmp/YzhwvcubU4="
611 )
612 .as_bytes(),
613 )
614 .unwrap(),
615 h: vec![
616 "Received".into(),
617 "From".into(),
618 "To".into(),
619 "Subject".into(),
620 "Date".into(),
621 "Message-ID".into(),
622 ],
623 z: vec![],
624 l: 123,
625 x: 0,
626 t: 0,
627 ch: Canonicalization::Simple,
628 cb: Canonicalization::Relaxed,
629 r: false,
630 atps: None,
631 atpsh: None,
632 },
633 ),
634 ] {
635 let result = Signature::parse(signature.as_bytes()).unwrap();
636 assert_eq!(result.v, expected_result.v, "{signature:?}");
637 assert_eq!(result.a, expected_result.a, "{signature:?}");
638 assert_eq!(result.d, expected_result.d, "{signature:?}");
639 assert_eq!(result.s, expected_result.s, "{signature:?}");
640 assert_eq!(result.i, expected_result.i, "{signature:?}");
641 assert_eq!(result.b, expected_result.b, "{signature:?}");
642 assert_eq!(result.bh, expected_result.bh, "{signature:?}");
643 assert_eq!(result.h, expected_result.h, "{signature:?}");
644 assert_eq!(result.z, expected_result.z, "{signature:?}");
645 assert_eq!(result.l, expected_result.l, "{signature:?}");
646 assert_eq!(result.x, expected_result.x, "{signature:?}");
647 assert_eq!(result.t, expected_result.t, "{signature:?}");
648 assert_eq!(result.ch, expected_result.ch, "{signature:?}");
649 assert_eq!(result.cb, expected_result.cb, "{signature:?}");
650
651 let header = result.to_header();
652 let value = header.strip_prefix("DKIM-Signature: ").unwrap();
653 let mut round_trip = Signature::parse(value.as_bytes()).unwrap();
654 round_trip.z = result.z.clone();
655 assert_eq!(round_trip, result, "{header:?}");
656 }
657 }
658
659 #[test]
660 fn dkim_signature_auid_qp() {
661 for (auid, expected) in [
662 (
663 "joe=40football.example.com",
664 Some("joe@football.example.com"),
665 ),
666 ("joe=3db@example.com", Some("joe=b@example.com")),
667 ("jo\r\n\te=\r\n\t40example.com", Some("joe@example.com")),
668 ("x=zz@evil.example", None),
669 ("joe=4@example.com", None),
670 ("joe@example.com=", None),
671 ] {
672 let header = format!(
673 "v=1; a=rsa-sha256; d=example.com; s=sel; h=from; i={auid}; bh=MTIz; b=dzdV;"
674 );
675 match (Signature::parse(header.as_bytes()), expected) {
676 (Ok(signature), Some(expected)) => assert_eq!(signature.i, expected, "{auid:?}"),
677 (Err(Error::Parse), None) => {}
678 (result, _) => panic!("{auid:?}: unexpected {result:?}"),
679 }
680 }
681 }
682
683 #[test]
684 fn dkim_record_parse() {
685 for (record, expected_result) in [
686 (
687 concat!(
688 "v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ",
689 "KBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYt",
690 "IxN2SnFCjxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v",
691 "/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhi",
692 "tdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB",
693 ),
694 0,
695 ),
696 (
697 concat!(
698 "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOC",
699 "AQ8AMIIBCgKCAQEAvzwKQIIWzQXv0nihasFTT3+JO23hXCg",
700 "e+ESWNxCJdVLxKL5edxrumEU3DnrPeGD6q6E/vjoXwBabpm",
701 "8F5o96MEPm7v12O5IIK7wx7gIJiQWvexwh+GJvW4aFFa0g1",
702 "3Ai75UdZjGFNKHAEGeLmkQYybK/EHW5ymRlSg3g8zydJGEc",
703 "I/melLCiBoShHjfZFJEThxLmPHNSi+KOUMypxqYHd7hzg6W",
704 "7qnq6t9puZYXMWj6tEaf6ORWgb7DOXZSTJJjAJPBWa2+Urx",
705 "XX6Ro7L7Xy1zzeYFCk8W5vmn0wMgGpjkWw0ljJWNwIpxZAj9",
706 "p5wMedWasaPS74TZ1b7tI39ncp6QIDAQAB ; t= y : s :yy:x;",
707 "s=*:email;; h= sha1:sha 256:other;; n=ignore these notes "
708 ),
709 R_HASH_SHA1
710 | R_HASH_SHA256
711 | R_SVC_ALL
712 | R_SVC_EMAIL
713 | R_FLAG_MATCH_DOMAIN
714 | R_FLAG_TESTING,
715 ),
716 (
717 concat!(
718 "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYtb/9Sh8nGKV7exhUFS",
719 "+cBNXlHgO1CxD9zIfQd5ztlq1LO7g38dfmFpQafh9lKgqPBTolFhZxhF1yUNT",
720 "hpV673NdAtaCVGNyx/fTYtvyyFe9DH2tmm/ijLlygDRboSkIJ4NHZjK++48hk",
721 "NP8/htqWHS+CvwWT4Qgs0NtB7Re9bQIDAQAB"
722 ),
723 0,
724 ),
725 ] {
726 assert_eq!(
727 DomainKey::parse(record.as_bytes()).unwrap().f,
728 expected_result
729 );
730 }
731 }
732
733 #[test]
734 fn dkim_record_empty_p_is_revoked() {
735 use crate::{Error, dkim::DkimError};
736 for record in ["v=DKIM1; p=", "v=DKIM1; k=rsa; p=;", "p="] {
737 assert!(
738 matches!(
739 DomainKey::parse(record.as_bytes()),
740 Err(Error::Dkim(DkimError::PublicKeyRevoked))
741 ),
742 "{record:?}"
743 );
744 }
745 }
746
747 #[test]
748 fn dkim_report_record_parse() {
749 for (record, expected_result) in [
750 (
751 "ra=dkim-errors; rp=97; rr=v:x",
752 DkimReportRecord {
753 ra: "dkim-errors".to_string(),
754 rp: 97,
755 rr: RR_VERIFICATION | RR_EXPIRATION,
756 rs: None,
757 },
758 ),
759 (
760 "ra=postmaster; rp=1; rr=d:o:p:s:u:v:x; rs=Error=20Message;",
761 DkimReportRecord {
762 ra: "postmaster".to_string(),
763 rp: 1,
764 rr: RR_DNS
765 | RR_OTHER
766 | RR_POLICY
767 | RR_SIGNATURE
768 | RR_UNKNOWN_TAG
769 | RR_VERIFICATION
770 | RR_EXPIRATION,
771 rs: "Error Message".to_string().into(),
772 },
773 ),
774 (
775 "ra=dkim=2Derrors; rs=bad=zz;",
776 DkimReportRecord {
777 ra: "dkim-errors".to_string(),
778 rp: 100,
779 rr: u8::MAX,
780 rs: None,
781 },
782 ),
783 ] {
784 assert_eq!(
785 DkimReportRecord::parse(record.as_bytes()).unwrap(),
786 expected_result
787 );
788 }
789 }
790}