1use crate::{
20 crypto::{HashAlgorithm, KeyType},
21 header::FieldName,
22 parse, quoted_printable,
23 tag_list::{TagList, TagSpec},
24 util::{Base64Debug, BytesDebug, CanonicalStr},
25};
26use std::{
27 error::Error,
28 fmt::{self, Display, Formatter},
29 hash::{Hash, Hasher},
30 str::{self, FromStr},
31};
32
33#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
45pub struct ParseDomainError;
46
47impl Display for ParseDomainError {
48 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
49 write!(f, "could not parse domain name")
50 }
51}
52
53impl Error for ParseDomainError {}
54
55#[derive(Clone, Eq)]
59pub struct DomainName(Box<str>);
60
61impl DomainName {
62 pub fn new(s: impl Into<Box<str>>) -> Result<Self, ParseDomainError> {
73 let s = s.into();
74 if is_valid_domain_name(&s) {
75 Ok(Self(s))
76 } else {
77 Err(ParseDomainError)
78 }
79 }
80
81 pub fn eq_or_subdomain_of(&self, other: &Self) -> bool {
84 if self == other {
85 return true;
86 }
87
88 let name = self.to_ascii();
89 let other = other.to_ascii();
90
91 if name.len() >= other.len() {
92 let (left, right) = name.split_at(name.len() - other.len());
93 right.eq_ignore_ascii_case(&other) && (left.is_empty() || left.ends_with('.'))
94 } else {
95 false
96 }
97 }
98
99 pub fn to_ascii(&self) -> String {
101 idna::domain_to_ascii(&self.0).unwrap()
103 }
104
105 pub fn to_unicode(&self) -> String {
107 idna::domain_to_unicode(&self.0).0
109 }
110}
111
112impl Display for DomainName {
113 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
114 self.0.fmt(f)
115 }
116}
117
118impl fmt::Debug for DomainName {
119 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
120 write!(f, "{self}")
121 }
122}
123
124impl AsRef<str> for DomainName {
125 fn as_ref(&self) -> &str {
126 &self.0
127 }
128}
129
130impl PartialEq for DomainName {
131 fn eq(&self, other: &Self) -> bool {
132 self.0.eq_ignore_ascii_case(&other.0)
133 }
134}
135
136impl Hash for DomainName {
137 fn hash<H: Hasher>(&self, state: &mut H) {
138 self.0.to_ascii_lowercase().hash(state);
139 }
140}
141
142impl FromStr for DomainName {
143 type Err = ParseDomainError;
144
145 fn from_str(s: &str) -> Result<Self, Self::Err> {
146 if is_valid_domain_name(s) {
147 Ok(Self(s.into()))
148 } else {
149 Err(ParseDomainError)
150 }
151 }
152}
153
154fn is_valid_domain_name(s: &str) -> bool {
155 is_valid_domain_string(s, true)
156}
157
158#[derive(Clone, Eq)]
162pub struct Selector(Box<str>);
163
164impl Selector {
165 pub fn new(s: impl Into<Box<str>>) -> Result<Self, ParseDomainError> {
176 let s = s.into();
177 if is_valid_selector(&s) {
178 Ok(Self(s))
179 } else {
180 Err(ParseDomainError)
181 }
182 }
183
184 pub fn to_ascii(&self) -> String {
186 idna::domain_to_ascii(&self.0).unwrap()
188 }
189
190 pub fn to_unicode(&self) -> String {
192 idna::domain_to_unicode(&self.0).0
194 }
195}
196
197impl Display for Selector {
198 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
199 self.0.fmt(f)
200 }
201}
202
203impl fmt::Debug for Selector {
204 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
205 write!(f, "{self}")
206 }
207}
208
209impl AsRef<str> for Selector {
210 fn as_ref(&self) -> &str {
211 &self.0
212 }
213}
214
215impl PartialEq for Selector {
216 fn eq(&self, other: &Self) -> bool {
217 self.0.eq_ignore_ascii_case(&other.0)
218 }
219}
220
221impl Hash for Selector {
222 fn hash<H: Hasher>(&self, state: &mut H) {
223 self.0.to_ascii_lowercase().hash(state);
224 }
225}
226
227impl FromStr for Selector {
228 type Err = ParseDomainError;
229
230 fn from_str(s: &str) -> Result<Self, Self::Err> {
231 if is_valid_selector(s) {
232 Ok(Self(s.into()))
233 } else {
234 Err(ParseDomainError)
235 }
236 }
237}
238
239fn is_valid_selector(s: &str) -> bool {
240 is_valid_domain_string(s, false)
241}
242
243fn is_valid_domain_string(s: &str, check_tld: bool) -> bool {
244 match idna::domain_to_ascii(s) {
247 Ok(ascii_s) => {
248 is_valid_dns_name(&ascii_s, check_tld) && idna::domain_to_unicode(s).1.is_ok()
249 }
250 Err(_) => false,
251 }
252}
253
254fn is_valid_dns_name(s: &str, check_tld: bool) -> bool {
255 if !has_valid_domain_len(s) {
256 return false;
257 }
258
259 let mut labels = s.split('.').rev();
260
261 let final_label = labels.next().expect("failed to split string");
262
263 if !is_label(final_label) || (check_tld && final_label.chars().all(|c| c.is_ascii_digit())) {
266 return false;
267 }
268
269 labels.all(is_label)
270}
271
272fn is_label(s: &str) -> bool {
275 debug_assert!(!s.contains('.'));
276 has_valid_label_len(s)
277 && !s.starts_with('-')
278 && !s.ends_with('-')
279 && s.chars()
280 .all(|c: char| c.is_ascii_alphanumeric() || matches!(c, '-' | '_'))
281}
282
283fn has_valid_domain_len(s: &str) -> bool {
287 matches!(s.len(), 1..=253)
288}
289
290fn has_valid_label_len(s: &str) -> bool {
291 matches!(s.len(), 1..=63)
292}
293
294#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
296pub struct ParseIdentityError;
297
298impl Display for ParseIdentityError {
299 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
300 write!(f, "could not parse identity")
301 }
302}
303
304impl Error for ParseIdentityError {}
305
306#[derive(Clone, Eq, Hash, PartialEq)]
310pub struct Identity {
311 pub local_part: Option<Box<str>>,
316
317 pub domain: DomainName,
319}
320
321impl Identity {
322 pub fn new(s: &str) -> Result<Self, ParseIdentityError> {
328 let (local_part, domain) = s.rsplit_once('@').ok_or(ParseIdentityError)?;
329
330 let local_part = if local_part.is_empty() {
331 None
332 } else {
333 if !is_local_part(local_part) {
334 return Err(ParseIdentityError);
335 }
336 Some(local_part)
337 };
338
339 let domain = domain.parse().map_err(|_| ParseIdentityError)?;
340
341 Ok(Self {
342 local_part: local_part.map(Into::into),
343 domain,
344 })
345 }
346
347 pub fn from_domain(domain: DomainName) -> Self {
350 Self {
351 local_part: None,
352 domain,
353 }
354 }
355}
356
357impl Display for Identity {
361 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
362 if let Some(local_part) = &self.local_part {
363 write!(f, "{local_part}")?;
364 }
365 write!(f, "@{}", self.domain)
366 }
367}
368
369impl fmt::Debug for Identity {
370 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
371 write!(f, "{self}")
372 }
373}
374
375impl FromStr for Identity {
376 type Err = ParseIdentityError;
377
378 fn from_str(s: &str) -> Result<Self, Self::Err> {
379 Self::new(s)
380 }
381}
382
383fn is_local_part(s: &str) -> bool {
386 if s.len() > 64 {
388 return false;
389 }
390
391 if s.starts_with('"') {
392 is_quoted_string(s)
393 } else {
394 is_dot_string(s)
395 }
396}
397
398fn is_quoted_string(s: &str) -> bool {
399 fn is_qtext_smtp(c: char) -> bool {
400 c == ' ' || (c.is_ascii_graphic() && !matches!(c, '"' | '\\')) || !c.is_ascii()
401 }
402
403 if let Some(s) = s.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
404 let mut quoted = false;
405 for c in s.chars() {
406 if quoted {
407 if c == ' ' || c.is_ascii_graphic() {
408 quoted = false;
409 } else {
410 return false;
411 }
412 } else if c == '\\' {
413 quoted = true;
414 } else if !is_qtext_smtp(c) {
415 return false;
416 }
417 }
418 !quoted
419 } else {
420 false
421 }
422}
423
424fn is_dot_string(s: &str) -> bool {
425 fn is_atext(c: char) -> bool {
427 c.is_ascii_alphanumeric()
428 || matches!(
429 c,
430 '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '/' | '=' | '?' | '^' | '_'
431 | '`' | '{' | '|' | '}' | '~'
432 )
433 || !c.is_ascii()
434 }
435
436 let mut dot = true;
437 for c in s.chars() {
438 if dot {
439 if is_atext(c) {
440 dot = false;
441 } else {
442 return false;
443 }
444 } else if c == '.' {
445 dot = true;
446 } else if !is_atext(c) {
447 return false;
448 }
449 }
450 !dot
451}
452
453#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
455pub struct ParseAlgorithmError;
456
457impl Display for ParseAlgorithmError {
458 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
459 write!(f, "could not parse algorithm name")
460 }
461}
462
463impl Error for ParseAlgorithmError {}
464
465#[derive(Clone, Copy, Eq, Hash, PartialEq)]
470pub enum SigningAlgorithm {
471 RsaSha256,
473 Ed25519Sha256,
475 #[cfg(feature = "pre-rfc8301")]
476 RsaSha1,
478}
479
480impl SigningAlgorithm {
481 pub fn from_parts(key_type: KeyType, hash_alg: HashAlgorithm) -> Option<Self> {
484 match (key_type, hash_alg) {
485 (KeyType::Rsa, HashAlgorithm::Sha256) => Some(Self::RsaSha256),
486 (KeyType::Ed25519, HashAlgorithm::Sha256) => Some(Self::Ed25519Sha256),
487 #[cfg(feature = "pre-rfc8301")]
488 (KeyType::Rsa, HashAlgorithm::Sha1) => Some(Self::RsaSha1),
489 #[cfg(feature = "pre-rfc8301")]
490 _ => None,
491 }
492 }
493
494 pub fn key_type(self) -> KeyType {
496 match self {
497 Self::RsaSha256 => KeyType::Rsa,
498 Self::Ed25519Sha256 => KeyType::Ed25519,
499 #[cfg(feature = "pre-rfc8301")]
500 Self::RsaSha1 => KeyType::Rsa,
501 }
502 }
503
504 pub fn hash_algorithm(self) -> HashAlgorithm {
506 match self {
507 Self::RsaSha256 | Self::Ed25519Sha256 => HashAlgorithm::Sha256,
508 #[cfg(feature = "pre-rfc8301")]
509 Self::RsaSha1 => HashAlgorithm::Sha1,
510 }
511 }
512}
513
514impl CanonicalStr for SigningAlgorithm {
515 fn canonical_str(&self) -> &'static str {
516 match self {
517 Self::RsaSha256 => "rsa-sha256",
518 Self::Ed25519Sha256 => "ed25519-sha256",
519 #[cfg(feature = "pre-rfc8301")]
520 Self::RsaSha1 => "rsa-sha1",
521 }
522 }
523}
524
525impl Display for SigningAlgorithm {
526 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
527 f.write_str(self.canonical_str())
528 }
529}
530
531impl fmt::Debug for SigningAlgorithm {
532 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
533 write!(f, "{self}")
534 }
535}
536
537impl FromStr for SigningAlgorithm {
538 type Err = ParseAlgorithmError;
539
540 fn from_str(s: &str) -> Result<Self, Self::Err> {
541 if s.eq_ignore_ascii_case("rsa-sha256") {
542 Ok(Self::RsaSha256)
543 } else if s.eq_ignore_ascii_case("ed25519-sha256") {
544 Ok(Self::Ed25519Sha256)
545 } else {
546 #[cfg(feature = "pre-rfc8301")]
547 if s.eq_ignore_ascii_case("rsa-sha1") {
548 return Ok(Self::RsaSha1);
549 }
550 Err(ParseAlgorithmError)
551 }
552 }
553}
554
555impl From<SigningAlgorithm> for (KeyType, HashAlgorithm) {
556 fn from(alg: SigningAlgorithm) -> Self {
557 (alg.key_type(), alg.hash_algorithm())
558 }
559}
560
561#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
563pub enum CanonicalizationAlgorithm {
564 #[default]
566 Simple,
567 Relaxed,
569}
570
571impl CanonicalStr for CanonicalizationAlgorithm {
572 fn canonical_str(&self) -> &'static str {
573 match self {
574 Self::Simple => "simple",
575 Self::Relaxed => "relaxed",
576 }
577 }
578}
579
580impl Display for CanonicalizationAlgorithm {
581 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
582 f.write_str(self.canonical_str())
583 }
584}
585
586impl fmt::Debug for CanonicalizationAlgorithm {
587 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
588 write!(f, "{self}")
589 }
590}
591
592impl FromStr for CanonicalizationAlgorithm {
593 type Err = ParseAlgorithmError;
594
595 fn from_str(s: &str) -> Result<Self, Self::Err> {
596 if s.eq_ignore_ascii_case("simple") {
597 Ok(Self::Simple)
598 } else if s.eq_ignore_ascii_case("relaxed") {
599 Ok(Self::Relaxed)
600 } else {
601 Err(ParseAlgorithmError)
602 }
603 }
604}
605
606#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
618pub struct Canonicalization {
619 pub header: CanonicalizationAlgorithm,
621 pub body: CanonicalizationAlgorithm,
623}
624
625impl CanonicalStr for Canonicalization {
626 fn canonical_str(&self) -> &'static str {
627 use CanonicalizationAlgorithm::*;
628
629 match (self.header, self.body) {
630 (Simple, Simple) => "simple/simple",
631 (Simple, Relaxed) => "simple/relaxed",
632 (Relaxed, Simple) => "relaxed/simple",
633 (Relaxed, Relaxed) => "relaxed/relaxed",
634 }
635 }
636}
637
638impl Display for Canonicalization {
639 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
640 f.write_str(self.canonical_str())
641 }
642}
643
644impl fmt::Debug for Canonicalization {
645 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
646 write!(f, "{self}")
647 }
648}
649
650impl From<(CanonicalizationAlgorithm, CanonicalizationAlgorithm)> for Canonicalization {
651 fn from((header, body): (CanonicalizationAlgorithm, CanonicalizationAlgorithm)) -> Self {
652 Self { header, body }
653 }
654}
655
656impl FromStr for Canonicalization {
657 type Err = ParseAlgorithmError;
658
659 fn from_str(s: &str) -> Result<Self, Self::Err> {
660 let (header, body) = if let Some((header, body)) = s.split_once('/') {
661 (header.parse()?, body.parse()?)
662 } else {
663 (s.parse()?, Default::default())
664 };
665 Ok(Self { header, body })
666 }
667}
668
669pub const DKIM_SIGNATURE_NAME: &str = "DKIM-Signature";
671
672#[derive(Clone, Debug, Eq, Hash, PartialEq)]
678pub struct DkimSignatureError {
679 pub kind: DkimSignatureErrorKind,
681
682 pub algorithm_str: Option<Box<str>>,
684 pub signature_data_str: Option<Box<str>>,
686 pub domain_str: Option<Box<str>>,
688 pub identity_str: Option<Box<str>>,
690 pub selector_str: Option<Box<str>>,
692}
693
694impl DkimSignatureError {
695 pub fn new(kind: DkimSignatureErrorKind) -> Self {
698 Self {
699 kind,
700 algorithm_str: None,
701 signature_data_str: None,
702 domain_str: None,
703 identity_str: None,
704 selector_str: None,
705 }
706 }
707}
708
709impl Display for DkimSignatureError {
710 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
711 self.kind.fmt(f)
712 }
713}
714
715impl Error for DkimSignatureError {}
716
717#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
719pub enum DkimSignatureErrorKind {
720 Utf8Encoding,
722 TagListFormat,
724 IncompatibleVersion,
726 HistoricAlgorithm,
729 UnsupportedAlgorithm,
731 InvalidBase64,
733 EmptySignatureTag,
735 EmptyBodyHashTag,
737 UnsupportedCanonicalization,
739 InvalidDomain,
741 InvalidSignedHeaderName,
743 EmptySignedHeadersTag,
745 FromHeaderNotSigned,
747 InvalidIdentity,
749 InvalidBodyLength,
751 InvalidQueryMethod,
753 NoSupportedQueryMethods,
755 InvalidSelector,
757 InvalidTimestamp,
759 InvalidExpiration,
761 InvalidCopiedHeaderField,
763 MissingVersionTag,
765 MissingAlgorithmTag,
767 MissingSignatureTag,
769 MissingBodyHashTag,
771 MissingDomainTag,
773 MissingSignedHeadersTag,
775 MissingSelectorTag,
777 DomainMismatch,
779 ExpirationNotAfterTimestamp,
781}
782
783impl Display for DkimSignatureErrorKind {
784 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
785 match self {
786 Self::Utf8Encoding => write!(f, "signature not UTF-8 encoded"),
787 Self::TagListFormat => write!(f, "ill-formed tag list"),
788 Self::IncompatibleVersion => write!(f, "incompatible version"),
789 Self::HistoricAlgorithm => write!(f, "historic signature algorithm"),
790 Self::UnsupportedAlgorithm => write!(f, "unsupported signature algorithm"),
791 Self::InvalidBase64 => write!(f, "invalid Base64 string"),
792 Self::EmptySignatureTag => write!(f, "b= tag empty"),
793 Self::EmptyBodyHashTag => write!(f, "bh= tag empty"),
794 Self::UnsupportedCanonicalization => write!(f, "unsupported canonicalization"),
795 Self::InvalidDomain => write!(f, "invalid signing domain"),
796 Self::InvalidSignedHeaderName => write!(f, "invalid signed header name"),
797 Self::EmptySignedHeadersTag => write!(f, "h= tag empty"),
798 Self::FromHeaderNotSigned => write!(f, "From header not signed"),
799 Self::InvalidIdentity => write!(f, "invalid signing identity"),
800 Self::InvalidBodyLength => write!(f, "invalid body length"),
801 Self::InvalidQueryMethod => write!(f, "invalid query method"),
802 Self::NoSupportedQueryMethods => write!(f, "no supported query methods"),
803 Self::InvalidSelector => write!(f, "invalid selector"),
804 Self::InvalidTimestamp => write!(f, "invalid timestamp"),
805 Self::InvalidExpiration => write!(f, "invalid expiration"),
806 Self::InvalidCopiedHeaderField => write!(f, "invalid header field in z= tag"),
807 Self::MissingVersionTag => write!(f, "v= tag missing"),
808 Self::MissingAlgorithmTag => write!(f, "a= tag missing"),
809 Self::MissingSignatureTag => write!(f, "b= tag missing"),
810 Self::MissingBodyHashTag => write!(f, "bh= tag missing"),
811 Self::MissingDomainTag => write!(f, "d= tag missing"),
812 Self::MissingSignedHeadersTag => write!(f, "h= tag missing"),
813 Self::MissingSelectorTag => write!(f, "s= tag missing"),
814 Self::DomainMismatch => write!(f, "domain mismatch"),
815 Self::ExpirationNotAfterTimestamp => write!(f, "expiration not after timestamp"),
816 }
817 }
818}
819
820#[derive(Clone, Eq, Hash, PartialEq)]
825pub struct DkimSignature {
826 pub algorithm: SigningAlgorithm,
839 pub signature_data: Box<[u8]>,
841 pub body_hash: Box<[u8]>,
843 pub canonicalization: Canonicalization,
845 pub domain: DomainName,
847 pub signed_headers: Box<[FieldName]>, pub identity: Option<Identity>,
851 pub body_length: Option<u64>,
853 pub selector: Selector,
855 pub timestamp: Option<u64>,
857 pub expiration: Option<u64>,
859 pub copied_headers: Box<[(FieldName, Box<[u8]>)]>, pub ext_tags: Box<[(Box<str>, Box<str>)]>,
864}
865
866impl DkimSignature {
867 pub fn from_tag_list(tag_list: &TagList<'_>) -> Result<Self, DkimSignatureError> {
874 Self::from_tag_list_internal(tag_list).map_err(|kind| {
875 let mut algorithm_str = None;
878 let mut signature_data_str = None;
879 let mut domain_str = None;
880 let mut identity_str = None;
881 let mut selector_str = None;
882
883 for &TagSpec { name, value } in tag_list.as_ref() {
884 match name {
885 "a" => algorithm_str = Some(value.into()),
886 "b" => signature_data_str = Some(value.into()),
887 "d" => domain_str = Some(value.into()),
888 "i" => identity_str = Some(value.into()),
889 "s" => selector_str = Some(value.into()),
890 _ => {}
891 }
892 }
893
894 DkimSignatureError {
895 kind,
896 algorithm_str,
897 signature_data_str,
898 domain_str,
899 identity_str,
900 selector_str,
901 }
902 })
903 }
904
905 fn from_tag_list_internal(tag_list: &TagList<'_>) -> Result<Self, DkimSignatureErrorKind> {
906 let mut version_seen = false;
907 let mut algorithm = None;
908 let mut signature_data = None;
909 let mut body_hash = None;
910 let mut canonicalization = None;
911 let mut domain = None;
912 let mut signed_headers = None;
913 let mut identity = None;
914 let mut body_length = None;
915 let mut selector = None;
916 let mut timestamp = None;
917 let mut expiration = None;
918 let mut copied_headers = None;
919 let mut ext_tags = vec![];
920
921 for &TagSpec { name, value } in tag_list.as_ref() {
922 match name {
923 "v" => {
924 if value != "1" {
925 return Err(DkimSignatureErrorKind::IncompatibleVersion);
926 }
927
928 version_seen = true;
929 }
930 "a" => {
931 let value = value.parse().map_err(|_| {
932 #[cfg(not(feature = "pre-rfc8301"))]
933 if value.eq_ignore_ascii_case("rsa-sha1") {
934 return DkimSignatureErrorKind::HistoricAlgorithm;
937 }
938 DkimSignatureErrorKind::UnsupportedAlgorithm
939 })?;
940
941 algorithm = Some(value);
942 }
943 "b" => {
944 let value = parse::parse_base64_tvalue(value)
945 .map_err(|_| DkimSignatureErrorKind::InvalidBase64)?;
946
947 if value.is_empty() {
948 return Err(DkimSignatureErrorKind::EmptySignatureTag);
949 }
950
951 signature_data = Some(value.into());
952 }
953 "bh" => {
954 let value = parse::parse_base64_tvalue(value)
955 .map_err(|_| DkimSignatureErrorKind::InvalidBase64)?;
956
957 if value.is_empty() {
958 return Err(DkimSignatureErrorKind::EmptyBodyHashTag);
959 }
960
961 body_hash = Some(value.into());
962 }
963 "c" => {
964 let value = value.parse()
965 .map_err(|_| DkimSignatureErrorKind::UnsupportedCanonicalization)?;
966
967 canonicalization = Some(value);
968 }
969 "d" => {
970 let value = value.parse()
971 .map_err(|_| DkimSignatureErrorKind::InvalidDomain)?;
972
973 domain = Some(value);
974 }
975 "h" => {
976 if value.is_empty() {
977 return Err(DkimSignatureErrorKind::EmptySignedHeadersTag);
978 }
979
980 let mut sh = vec![];
981
982 for s in parse::parse_colon_separated_tvalue(value) {
983 let name = FieldName::new(s)
984 .map_err(|_| DkimSignatureErrorKind::InvalidSignedHeaderName)?;
985 sh.push(name);
986 }
987
988 if !sh.iter().any(|h| *h == "From") {
989 return Err(DkimSignatureErrorKind::FromHeaderNotSigned);
990 }
991
992 signed_headers = Some(sh.into());
993 }
994 "i" => {
995 let value = quoted_printable::decode(value)
996 .map_err(|_| DkimSignatureErrorKind::InvalidIdentity)?;
997
998 let value = String::from_utf8(value)
1000 .map_err(|_| DkimSignatureErrorKind::InvalidIdentity)?;
1001
1002 let value = Identity::new(&value)
1003 .map_err(|_| DkimSignatureErrorKind::InvalidIdentity)?;
1004
1005 identity = Some(value);
1006 }
1007 "l" => {
1008 let value = value.parse()
1009 .map_err(|_| DkimSignatureErrorKind::InvalidBodyLength)?;
1010
1011 body_length = Some(value);
1012 }
1013 "q" => {
1014 let mut dns_txt_seen = false;
1021
1022 for s in parse::parse_colon_separated_tvalue(value) {
1023 if s.is_empty() {
1024 return Err(DkimSignatureErrorKind::InvalidQueryMethod);
1025 }
1026 if s.eq_ignore_ascii_case("dns/txt") {
1027 dns_txt_seen = true;
1028 }
1029 }
1030
1031 if !dns_txt_seen {
1032 return Err(DkimSignatureErrorKind::NoSupportedQueryMethods);
1033 }
1034 }
1035 "s" => {
1036 let value = value.parse()
1037 .map_err(|_| DkimSignatureErrorKind::InvalidSelector)?;
1038
1039 selector = Some(value);
1040 }
1041 "t" => {
1042 let value = value.parse()
1043 .map_err(|_| DkimSignatureErrorKind::InvalidTimestamp)?;
1044
1045 timestamp = Some(value);
1046 }
1047 "x" => {
1048 let value = value.parse()
1049 .map_err(|_| DkimSignatureErrorKind::InvalidExpiration)?;
1050
1051 expiration = Some(value);
1052 }
1053 "z" => {
1054 let mut headers = vec![];
1055
1056 for piece in value.split('|') {
1057 let header = parse_copied_header_field(piece)?;
1058 headers.push(header);
1059 }
1060
1061 copied_headers = Some(headers.into());
1062 }
1063 _ => {
1064 ext_tags.push((name.into(), value.into()));
1065 }
1066 }
1067 }
1068
1069 if !version_seen {
1070 return Err(DkimSignatureErrorKind::MissingVersionTag);
1071 }
1072
1073 let algorithm = algorithm.ok_or(DkimSignatureErrorKind::MissingAlgorithmTag)?;
1074 let signature_data = signature_data.ok_or(DkimSignatureErrorKind::MissingSignatureTag)?;
1075 let body_hash = body_hash.ok_or(DkimSignatureErrorKind::MissingBodyHashTag)?;
1076 let domain = domain.ok_or(DkimSignatureErrorKind::MissingDomainTag)?;
1077 let signed_headers = signed_headers.ok_or(DkimSignatureErrorKind::MissingSignedHeadersTag)?;
1078 let selector = selector.ok_or(DkimSignatureErrorKind::MissingSelectorTag)?;
1079
1080 if let Some(id) = &identity {
1081 if !id.domain.eq_or_subdomain_of(&domain) {
1082 return Err(DkimSignatureErrorKind::DomainMismatch);
1083 }
1084 }
1085
1086 if let (Some(timestamp), Some(expiration)) = (timestamp, expiration) {
1087 if expiration <= timestamp {
1088 return Err(DkimSignatureErrorKind::ExpirationNotAfterTimestamp);
1089 }
1090 }
1091
1092 let canonicalization = canonicalization.unwrap_or_default();
1093 let copied_headers = copied_headers.unwrap_or_default();
1094 let ext_tags = ext_tags.into();
1095
1096 Ok(Self {
1097 algorithm,
1098 signature_data,
1099 body_hash,
1100 canonicalization,
1101 domain,
1102 signed_headers,
1103 identity,
1104 body_length,
1105 selector,
1106 timestamp,
1107 expiration,
1108 copied_headers,
1109 ext_tags,
1110 })
1111 }
1112}
1113
1114fn parse_copied_header_field(
1115 value: &str,
1116) -> Result<(FieldName, Box<[u8]>), DkimSignatureErrorKind> {
1117 use DkimSignatureErrorKind::InvalidCopiedHeaderField;
1118
1119 let val = quoted_printable::decode(value).map_err(|_| InvalidCopiedHeaderField)?;
1124
1125 let mut iter = val.splitn(2, |&c| c == b':');
1126
1127 match (iter.next(), iter.next()) {
1128 (Some(name), Some(value)) => {
1129 let name = str::from_utf8(name).map_err(|_| InvalidCopiedHeaderField)?;
1130 let name = FieldName::new(name).map_err(|_| InvalidCopiedHeaderField)?;
1131 let value = value.into();
1132 Ok((name, value))
1133 }
1134 _ => Err(InvalidCopiedHeaderField),
1135 }
1136}
1137
1138impl FromStr for DkimSignature {
1139 type Err = DkimSignatureError;
1140
1141 fn from_str(s: &str) -> Result<Self, Self::Err> {
1142 let tag_list = TagList::from_str(s)
1143 .map_err(|_| DkimSignatureError::new(DkimSignatureErrorKind::TagListFormat))?;
1144
1145 Self::from_tag_list(&tag_list)
1146 }
1147}
1148
1149struct CopiedHeadersDebug<'a>(&'a [(FieldName, Box<[u8]>)]);
1150
1151impl fmt::Debug for CopiedHeadersDebug<'_> {
1152 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1153 let mut d = f.debug_list();
1154 for (name, value) in self.0 {
1155 d.entry(&(name, BytesDebug(value)));
1156 }
1157 d.finish()
1158 }
1159}
1160
1161impl fmt::Debug for DkimSignature {
1162 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1163 f.debug_struct("DkimSignature")
1164 .field("algorithm", &self.algorithm)
1165 .field("signature_data", &Base64Debug(&self.signature_data))
1166 .field("body_hash", &Base64Debug(&self.body_hash))
1167 .field("canonicalization", &self.canonicalization)
1168 .field("domain", &self.domain)
1169 .field("signed_headers", &self.signed_headers)
1170 .field("identity", &self.identity)
1171 .field("body_length", &self.body_length)
1172 .field("selector", &self.selector)
1173 .field("timestamp", &self.timestamp)
1174 .field("expiration", &self.expiration)
1175 .field("copied_headers", &CopiedHeadersDebug(&self.copied_headers))
1176 .field("ext_tags", &self.ext_tags)
1177 .finish()
1178 }
1179}
1180
1181#[cfg(test)]
1182mod tests {
1183 use super::*;
1184 use crate::{tag_list::TagList, util};
1185 use CanonicalizationAlgorithm::*;
1186
1187 #[test]
1188 fn domain_name_ok() {
1189 assert!(DomainName::new("com").is_ok());
1190 assert!(DomainName::new("com123").is_ok());
1191 assert!(DomainName::new("example.com").is_ok());
1192 assert!(DomainName::new("_abc.example.com").is_ok());
1193 assert!(DomainName::new("中国").is_ok());
1194 assert!(DomainName::new("example.中国").is_ok());
1195 assert!(DomainName::new("☕.example.中国").is_ok());
1196 assert!(DomainName::new("xn--53h.example.xn--fiqs8s").is_ok());
1197
1198 assert!(DomainName::new("").is_err());
1199 assert!(DomainName::new("-com").is_err());
1200 assert!(DomainName::new("c,m").is_err());
1201 assert!(DomainName::new("c;m").is_err());
1202 assert!(DomainName::new("123").is_err());
1203 assert!(DomainName::new("com.").is_err());
1204 assert!(DomainName::new("example..com").is_err());
1205 assert!(DomainName::new("example-.com").is_err());
1206 assert!(DomainName::new("example.123").is_err());
1207 assert!(DomainName::new("_$@.example.com").is_err());
1208 assert!(DomainName::new("example.com.").is_err());
1209 assert!(DomainName::new("ex mple.com").is_err());
1210 assert!(DomainName::new("xn---y.example.com").is_err());
1211 }
1212
1213 #[test]
1214 fn domain_name_eq_or_subdomain() {
1215 fn domain(s: &str) -> DomainName {
1216 DomainName::new(s).unwrap()
1217 }
1218
1219 assert!(domain("eXaMpLe.CoM").eq_or_subdomain_of(&domain("example.com")));
1220 assert!(domain("mAiL.eXaMpLe.CoM").eq_or_subdomain_of(&domain("example.com")));
1221 assert!(!domain("XaMpLe.CoM").eq_or_subdomain_of(&domain("example.com")));
1222 assert!(!domain("meXaMpLe.CoM").eq_or_subdomain_of(&domain("example.com")));
1223
1224 assert!(domain("例子.xn--fiqs8s").eq_or_subdomain_of(&domain("xn--fsqu00a.中国")));
1225 assert!(domain("☕.例子.xn--fiqs8s").eq_or_subdomain_of(&domain("xn--fsqu00a.中国")));
1226 assert!(!domain("子.xn--fiqs8s").eq_or_subdomain_of(&domain("xn--fsqu00a.中国")));
1227 assert!(!domain("假例子.xn--fiqs8s").eq_or_subdomain_of(&domain("xn--fsqu00a.中国")));
1228 }
1229
1230 #[test]
1231 fn selector_ok() {
1232 assert!(Selector::new("example").is_ok());
1233 assert!(Selector::new("x☕y").is_ok());
1234 assert!(Selector::new("_x☕y").is_ok());
1235 assert!(Selector::new("123").is_ok());
1236 assert!(Selector::new("☕.example").is_ok());
1237 assert!(Selector::new("_☕.example").is_ok());
1238 assert!(Selector::new("xn--53h.example").is_ok());
1239 assert!(Selector::new("xn--_-2yp.example").is_ok());
1240
1241 assert!(Selector::new("").is_err());
1242 assert!(Selector::new(".").is_err());
1243 assert!(Selector::new("example.").is_err());
1244 assert!(Selector::new("xn---x.example").is_err());
1245 }
1246
1247 #[test]
1248 fn identity_ok() {
1249 assert!(Identity::new("我@☕.example.中国").is_ok());
1250 assert!(Identity::new("\"我\"@☕.example.中国").is_ok());
1251
1252 assert!(Identity::new("me@@☕.example.中国").is_err());
1253 }
1254
1255 #[test]
1256 fn identity_repr_ok() {
1257 let id1 = Identity::new("@example.org").unwrap();
1258 let id2 = Identity::new("Me@Example.Org").unwrap();
1259 let id3 = Identity::new("我.x#!@example.中国").unwrap();
1260 let id4 = Identity::new("\"x #$我\\\"\"@example.org").unwrap();
1261
1262 assert_eq!(id1.to_string(), "@example.org");
1263 assert_eq!(id2.to_string(), "Me@Example.Org");
1264 assert_eq!(id3.to_string(), "我.x#!@example.中国");
1265 assert_eq!(id4.to_string(), "\"x #$我\\\"\"@example.org");
1266
1267 assert_eq!(format!("{:?}", id1), "@example.org");
1268 assert_eq!(format!("{:?}", id2), "Me@Example.Org");
1269 assert_eq!(format!("{:?}", id3), "我.x#!@example.中国");
1270 assert_eq!(format!("{:?}", id4), "\"x #$我\\\"\"@example.org");
1271 }
1272
1273 #[test]
1274 fn rfc6376_example_signature() {
1275 let example = " v=1; a=rsa-sha256; d=example.net; s=brisbane;
1277 c=simple; q=dns/txt; i=@eng.example.net;
1278 t=1117574938; x=1118006938;
1279 h=from:to:subject:date;
1280 z=From:foo@eng.example.net|To:joe@example.com|
1281 Subject:demo=20run|Date:July=205,=202005=203:44:08=20PM=20-0700;
1282 bh=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=;
1283 b=dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD00lszZVoG4ZHRNiYzR";
1284 let example = example.replace('\n', "\r\n");
1285
1286 let q = TagList::from_str(&example).unwrap();
1287
1288 let hdr = DkimSignature::from_tag_list(&q).unwrap();
1289
1290 assert_eq!(
1291 hdr,
1292 DkimSignature {
1293 algorithm: SigningAlgorithm::RsaSha256,
1294 signature_data: util::decode_base64(
1295 "dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD00lszZVoG4ZHRNiYzR"
1296 )
1297 .unwrap()
1298 .into(),
1299 body_hash: util::decode_base64("MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=")
1300 .unwrap()
1301 .into(),
1302 canonicalization: (Simple, Simple).into(),
1303 domain: DomainName::new("example.net").unwrap(),
1304 signed_headers: [
1305 FieldName::new("from").unwrap(),
1306 FieldName::new("to").unwrap(),
1307 FieldName::new("subject").unwrap(),
1308 FieldName::new("date").unwrap(),
1309 ]
1310 .into(),
1311 identity: Some(Identity::new("@eng.example.net").unwrap()),
1312 selector: Selector::new("brisbane").unwrap(),
1313 body_length: None,
1314 timestamp: Some(1117574938),
1315 expiration: Some(1118006938),
1316 copied_headers: [
1317 (
1318 FieldName::new("From").unwrap(),
1319 Box::from(*b"foo@eng.example.net")
1320 ),
1321 (
1322 FieldName::new("To").unwrap(),
1323 Box::from(*b"joe@example.com")
1324 ),
1325 (
1326 FieldName::new("Subject").unwrap(),
1327 Box::from(*b"demo run")
1328 ),
1329 (
1330 FieldName::new("Date").unwrap(),
1331 Box::from(*b"July 5, 2005 3:44:08 PM -0700")
1332 ),
1333 ]
1334 .into(),
1335 ext_tags: [].into(),
1336 }
1337 );
1338 }
1339
1340 #[test]
1341 fn complicated_i18n_example_signature() {
1342 let example = " v = 1 ; a=rsa-sha256;d=example.net; s=brisbane;
1343 c=simple; q=dns/txt; i=中文=40en
1344 g.example =2E net;
1345 t=1117574938; x=1118006938; y= curious
1346 value; zz=;
1347 h=from:to:subject:date;
1348 bh=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=;
1349 b=dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD00lszZVoG4ZHRNiYzR";
1350 let example = example.replace('\n', "\r\n");
1351
1352 let q = TagList::from_str(&example).unwrap();
1353
1354 let hdr = DkimSignature::from_tag_list(&q).unwrap();
1355
1356 assert_eq!(
1357 hdr,
1358 DkimSignature {
1359 algorithm: SigningAlgorithm::RsaSha256,
1360 signature_data: util::decode_base64(
1361 "dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD00lszZVoG4ZHRNiYzR"
1362 )
1363 .unwrap()
1364 .into(),
1365 body_hash: util::decode_base64("MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=")
1366 .unwrap()
1367 .into(),
1368 canonicalization: (Simple, Simple).into(),
1369 domain: DomainName::new("example.net").unwrap(),
1370 signed_headers: [
1371 FieldName::new("from").unwrap(),
1372 FieldName::new("to").unwrap(),
1373 FieldName::new("subject").unwrap(),
1374 FieldName::new("date").unwrap(),
1375 ]
1376 .into(),
1377 identity: Some(Identity::new("中文@eng.example.net").unwrap()),
1378 selector: Selector::new("brisbane").unwrap(),
1379 body_length: None,
1380 timestamp: Some(1117574938),
1381 expiration: Some(1118006938),
1382 copied_headers: [].into(),
1383 ext_tags: [
1384 (
1385 "y".into(),
1386 "curious\r\n value".into(),
1387 ),
1388 (
1389 "zz".into(),
1390 "".into(),
1391 ),
1392 ]
1393 .into(),
1394 }
1395 );
1396 }
1397
1398 #[test]
1399 fn parse_copied_header_field_ok() {
1400 let example = " Date:=20July=205,=0D=0A=092005=20\r\n\t3:44:08=20PM=20-0700 ";
1401
1402 let result = parse_copied_header_field(example);
1403
1404 assert_eq!(
1405 result,
1406 Ok((
1407 FieldName::new("Date").unwrap(),
1408 Box::from(*b" July 5,\r\n\t2005 3:44:08 PM -0700"),
1409 ))
1410 );
1411 }
1412}