viadkim/
signature.rs

1// viadkim – implementation of the DKIM specification
2// Copyright © 2022–2024 David Bürgin <dbuergin@gluet.ch>
3//
4// This program is free software: you can redistribute it and/or modify it under
5// the terms of the GNU General Public License as published by the Free Software
6// Foundation, either version 3 of the License, or (at your option) any later
7// version.
8//
9// This program is distributed in the hope that it will be useful, but WITHOUT
10// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12// details.
13//
14// You should have received a copy of the GNU General Public License along with
15// this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! DKIM signature.
18
19use 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// Design note: According to RFC 6376, bare TLDs are not allowed (see ABNF for
34// d= tag). But elsewhere it does seem to assume possibility of such domains,
35// see §6.1.1: ‘signatures with "d=" values such as "com" and "co.uk" could be
36// ignored.’ Therefore, we allow such values. (See also RFC 5321, section 2.3.5:
37// ‘A domain name […] consists of one or more components, separated by dots if
38// more than one appears. In the case of a top-level domain used by itself in an
39// email address, a single string is used without any dots.’)
40
41// Note: some of this was copied from viaspf.
42
43/// An error indicating that a domain name could not be parsed.
44#[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/// A domain name.
56///
57/// This type is used to wrap domain names as used in the *d=* and *i=* tags.
58#[derive(Clone, Eq)]
59pub struct DomainName(Box<str>);
60
61impl DomainName {
62    /// Creates a new domain name from the given string.
63    ///
64    /// Note that the string is validated and then encapsulated as-is.
65    /// Equivalence comparison is case-insensitive; IDNA-equivalence comparisons
66    /// are done in viadkim where necessary but are not part of the type’s own
67    /// equivalence relations.
68    ///
69    /// # Errors
70    ///
71    /// If the given string is not a valid domain name, an error is returned.
72    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    /// Compares this and the given domain for equivalence/subdomain
82    /// relationship, in case-insensitive and IDNA-aware manner.
83    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    /// Produces the IDNA A-label (ASCII) form of this domain name.
100    pub fn to_ascii(&self) -> String {
101        // Wrapped name is guaranteed to be convertible to A-label form.
102        idna::domain_to_ascii(&self.0).unwrap()
103    }
104
105    /// Produces the IDNA U-label (Unicode) form of this domain name.
106    pub fn to_unicode(&self) -> String {
107        // Wrapped name is guaranteed to be convertible to U-label form.
108        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/// A selector.
159///
160/// This type is used to wrap a sequence of labels as used in the *s=* tag.
161#[derive(Clone, Eq)]
162pub struct Selector(Box<str>);
163
164impl Selector {
165    /// Creates a new selector from the given string.
166    ///
167    /// Note that the string is validated and then encapsulated as-is.
168    /// Equivalence comparison is case-insensitive; IDNA-equivalence comparisons
169    /// are done in viadkim where necessary but are not part of the type’s own
170    /// equivalence relations.
171    ///
172    /// # Errors
173    ///
174    /// If the given string is not a valid selector, an error is returned.
175    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    /// Produces the IDNA A-label (ASCII) form of this selector.
185    pub fn to_ascii(&self) -> String {
186        // Wrapped name is guaranteed to be convertible to A-label form.
187        idna::domain_to_ascii(&self.0).unwrap()
188    }
189
190    /// Produces the IDNA U-label (Unicode) form of this selector.
191    pub fn to_unicode(&self) -> String {
192        // Wrapped name is guaranteed to be convertible to U-label form.
193        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    // For later IDNA processing, require that inputs can be converted without
245    // error in both directions.
246    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    // RFC 3696, section 2: ‘There is an additional rule that essentially
264    // requires that top-level domain names not be all-numeric.’
265    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
272// Use a somewhat relaxed definition of DNS labels that also allows underscores,
273// as seen in the wild.
274fn 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
283// Note that these length checks are not definitive, as a later concatenation of
284// selector, "_domainkey", and domain may still produce an invalid domain name.
285
286fn 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/// An error indicating that an identity could not be parsed.
295#[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/// An agent or user identifier (AUID).
307///
308/// This type is used to wrap addresses as used in the *i=* tag.
309#[derive(Clone, Eq, Hash, PartialEq)]
310pub struct Identity {
311    // Note: because PartialEq and Hash are derived, the local-part will be
312    // compared/hashed literally and in case-sensitive fashion.
313
314    /// The identity’s local-part, if any.
315    pub local_part: Option<Box<str>>,
316
317    /// The identity’s domain part.
318    pub domain: DomainName,
319}
320
321impl Identity {
322    /// Creates a new agent or user identifier (AUID) from the given string.
323    ///
324    /// # Errors
325    ///
326    /// If the given string is not a valid AUID, an error is returned.
327    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    /// Creates a new agent or user identifier from the given domain name,
348    /// without local-part.
349    pub fn from_domain(domain: DomainName) -> Self {
350        Self {
351            local_part: None,
352            domain,
353        }
354    }
355}
356
357// Note that local-part may include semicolon and space, which are here printed
358// as-is. However, they cannot appear in a tag-list value and so must be encoded
359// when formatted into a DKIM-Signature.
360impl 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
383// ‘local-part’ is defined in RFC 5321, §4.1.2. Modifications for
384// internationalisation are in RFC 6531, §3.3.
385fn is_local_part(s: &str) -> bool {
386    // See RFC 5321, §4.5.3.1.1.
387    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    // See RFC 5322, §3.2.3, with the modifications in RFC 6531, §3.3.
426    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/// An error indicating that an algorithm name could not be parsed.
454#[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/// A signing algorithm.
466///
467/// When **feature `pre-rfc8301`**  is enabled, this enum has a third variant
468/// `RsaSha1` representing the *rsa-sha1* signing algorithm.
469#[derive(Clone, Copy, Eq, Hash, PartialEq)]
470pub enum SigningAlgorithm {
471    /// The *rsa-sha256* signing algorithm.
472    RsaSha256,
473    /// The *ed25519-sha256* signing algorithm.
474    Ed25519Sha256,
475    #[cfg(feature = "pre-rfc8301")]
476    /// The *rsa-sha1* signing algorithm.
477    RsaSha1,
478}
479
480impl SigningAlgorithm {
481    /// Assembles a signing algorithm from the constituent key type and hash
482    /// algorithm, if possible.
483    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    /// Returns this signing algorithm’s key type component.
495    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    /// Returns this signing algorithm’s hash algorithm component.
505    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/// A canonicalization algorithm.
562#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
563pub enum CanonicalizationAlgorithm {
564    /// The *simple* canonicalization algorithm.
565    #[default]
566    Simple,
567    /// The *relaxed* canonicalization algorithm.
568    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/// A pair of header/body canonicalization algorithms.
607///
608/// # Examples
609///
610/// ```
611/// use viadkim::signature::{Canonicalization, CanonicalizationAlgorithm::*};
612///
613/// let c = Canonicalization::from((Relaxed, Simple));
614///
615/// assert_eq!(c.to_string(), "relaxed/simple");
616/// ```
617#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
618pub struct Canonicalization {
619    /// The header canonicalization.
620    pub header: CanonicalizationAlgorithm,
621    /// The body canonicalization.
622    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
669/// The *DKIM-Signature* header name.
670pub const DKIM_SIGNATURE_NAME: &str = "DKIM-Signature";
671
672/// An error that occurs when parsing a DKIM signature for further processing.
673///
674/// The error comes with salvaged data from the failed parsing attempt that
675/// could be reported in an *Authentication-Results* header. This data is in raw
676/// (string) form because it might fail to parse into a concrete type.
677#[derive(Clone, Debug, Eq, Hash, PartialEq)]
678pub struct DkimSignatureError {
679    /// The error kind that caused this error.
680    pub kind: DkimSignatureErrorKind,
681
682    /// The string value of the *a=* tag, if available.
683    pub algorithm_str: Option<Box<str>>,
684    /// The string value of the *b=* tag, if available.
685    pub signature_data_str: Option<Box<str>>,
686    /// The string value of the *d=* tag, if available.
687    pub domain_str: Option<Box<str>>,
688    /// The string value of the *i=* tag, if available.
689    pub identity_str: Option<Box<str>>,
690    /// The string value of the *s=* tag, if available.
691    pub selector_str: Option<Box<str>>,
692}
693
694impl DkimSignatureError {
695    /// Creates a new DKIM signature error of the given kind, with no additional
696    /// data attached.
697    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/// The type of the error caused by an invalid DKIM signature.
718#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
719pub enum DkimSignatureErrorKind {
720    /// Invalid UTF-8 in *DKIM-Signature* header.
721    Utf8Encoding,
722    /// The tag list is syntactically invalid.
723    TagListFormat,
724    /// Incompatible DKIM version.
725    IncompatibleVersion,
726    /// Use of a historic signature algorithm. (Note: only used when feature
727    /// `pre-rfc8301` is not enabled.)
728    HistoricAlgorithm,
729    /// The algorithm in *a=* is not supported.
730    UnsupportedAlgorithm,
731    /// Invalid Base64 in DKIM signature.
732    InvalidBase64,
733    /// The *b=* tag is empty.
734    EmptySignatureTag,
735    /// The *bh=* tag is empty.
736    EmptyBodyHashTag,
737    /// The algorithm in *c=* is not supported.
738    UnsupportedCanonicalization,
739    /// The signing domain in *d=* is syntactically invalid.
740    InvalidDomain,
741    /// Invalid value in *h=*.
742    InvalidSignedHeaderName,
743    /// The *h=* tag is empty.
744    EmptySignedHeadersTag,
745    /// *From* is not included in *h=*.
746    FromHeaderNotSigned,
747    /// The AUID in *i=* is syntactically invalid.
748    InvalidIdentity,
749    /// Invalid value in *l=*.
750    InvalidBodyLength,
751    /// Invalid value in *q=*.
752    InvalidQueryMethod,
753    /// No supported query methods in *q=*.
754    NoSupportedQueryMethods,
755    /// The selector in *s=* is syntactically invalid.
756    InvalidSelector,
757    /// Invalid value in *t=*.
758    InvalidTimestamp,
759    /// Invalid value in *x=*.
760    InvalidExpiration,
761    /// A header field in *z=* is syntactically invalid.
762    InvalidCopiedHeaderField,
763    /// The *v=* tag is missing.
764    MissingVersionTag,
765    /// The *a=* tag is missing.
766    MissingAlgorithmTag,
767    /// The *b=* tag is missing.
768    MissingSignatureTag,
769    /// The *bh=* tag is missing.
770    MissingBodyHashTag,
771    /// The *d=* tag is missing.
772    MissingDomainTag,
773    /// The *h=* tag is missing.
774    MissingSignedHeadersTag,
775    /// The *s=* tag is missing.
776    MissingSelectorTag,
777    /// The *i=* domain is not equal to or a subdomain of the *d=* domain.
778    DomainMismatch,
779    /// The *x=* timestamp is not after the *i=* timestamp.
780    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/// DKIM signature data as encoded in a *DKIM-Signature* header.
821///
822/// The *v=* tag (always 1) and the *q=* tag (always includes dns/txt) are not
823/// included.
824#[derive(Clone, Eq, Hash, PartialEq)]
825pub struct DkimSignature {
826    // The fields are strongly typed and have public visibility. This does allow
827    // constructing an invalid `DkimSignature` (eg with empty signature, or
828    // empty signed headers) but we consider this acceptable, because this is
829    // mainly an ‘output’ data container.
830    //
831    // i= is `Option` because of §3.5: ‘the Signer might wish to assert that
832    // although it is willing to go as far as signing for the domain, it is
833    // unable or unwilling to commit to an individual user name within the
834    // domain. It can do so by including the domain part but not the local-part
835    // of the identity.’
836
837    /// The *a=* tag.
838    pub algorithm: SigningAlgorithm,
839    /// The *b=* tag.
840    pub signature_data: Box<[u8]>,
841    /// The *bh=* tag.
842    pub body_hash: Box<[u8]>,
843    /// The *c=* tag.
844    pub canonicalization: Canonicalization,
845    /// The *d=* tag.
846    pub domain: DomainName,
847    /// The *h=* tag.
848    pub signed_headers: Box<[FieldName]>,  // not empty, no names containing `;`
849    /// The *i=* tag.
850    pub identity: Option<Identity>,
851    /// The *l=* tag.
852    pub body_length: Option<u64>,
853    /// The *s=* tag.
854    pub selector: Selector,
855    /// The *t=* tag.
856    pub timestamp: Option<u64>,
857    /// The *x=* tag.
858    pub expiration: Option<u64>,
859    /// The *z=* tag.
860    pub copied_headers: Box<[(FieldName, Box<[u8]>)]>,  // names may contain `;`
861    /// Additional, unrecognised tag name and value pairs. (For example, the RFC
862    /// 6651 extension tag *r=y*.)
863    pub ext_tags: Box<[(Box<str>, Box<str>)]>,
864}
865
866impl DkimSignature {
867    /// Parses a DKIM signature from a tag list.
868    ///
869    /// # Errors
870    ///
871    /// If the tag list does not represent a valid DKIM signature, an error is
872    /// returned.
873    pub fn from_tag_list(tag_list: &TagList<'_>) -> Result<Self, DkimSignatureError> {
874        Self::from_tag_list_internal(tag_list).map_err(|kind| {
875            // The error path. Extract some data in raw form.
876
877            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                            // Note: special-case "rsa-sha1" as recognised but
935                            // no longer supported (RFC 8301).
936                            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                    // This identifier is expected to contain UTF-8 only.
999                    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                    // Note that even though *q=* is specified as being plain
1015                    // text, the ABNF then allows qp-hdr-value (or rather
1016                    // ‘dkim-quoted-printable with colon encoded’, see erratum
1017                    // 4810). We skip this by simply checking for presence of
1018                    // `dns/txt`, the only generally supported value.
1019
1020                    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    // This enforces the well-formedness requirement for header field names, but
1120    // not for the qp-encoded value, which can be anything (it should of course
1121    // conform to `FieldBody`, but since it is foreign data we cannot assume).
1122
1123    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        // See §3.5:
1276        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}