1   
   2   
   3   
   4   
   5   
   6   
   7   
   8   
   9   
  10   
  11   
  12   
  13   
  14   
  15   
  16   
  17   
  18   
  19   
  20   
  21   
  22   
  23   
  24   
  25   
  26   
  27   
  28   
  29   
  30   
  31   
  32   
  33   
  34   
  35  import base64 
  36  import hashlib 
  37  import logging 
  38  import re 
  39  import sys 
  40  import time 
  41  import binascii 
  42   
  43   
  44  try: 
  45      from authres import AuthenticationResultsHeader 
  46  except ImportError: 
  47      pass 
  48   
  49   
  50  try: 
  51      import nacl.signing 
  52      import nacl.encoding 
  53  except ImportError: 
  54      pass 
  55   
  56  from dkim.canonicalization import ( 
  57      CanonicalizationPolicy, 
  58      InvalidCanonicalizationPolicyError, 
  59      ) 
  60  from dkim.canonicalization import Relaxed as RelaxedCanonicalization 
  61   
  62  from dkim.crypto import ( 
  63      DigestTooLargeError, 
  64      HASH_ALGORITHMS, 
  65      ARC_HASH_ALGORITHMS, 
  66      parse_pem_private_key, 
  67      parse_public_key, 
  68      RSASSA_PKCS1_v1_5_sign, 
  69      RSASSA_PKCS1_v1_5_verify, 
  70      UnparsableKeyError, 
  71      ) 
  72  try: 
  73      from dkim.dnsplug import get_txt 
  74  except ImportError: 
  75      try: 
  76          import aiodns 
  77          from dkim.asyncsupport import get_txt_async as get_txt 
  78      except: 
  79           
  81              raise RuntimeError("DKIM.verify requires DNS or dnspython module") 
   82  from dkim.util import ( 
  83      get_default_logger, 
  84      InvalidTagValueList, 
  85      parse_tag_value, 
  86      ) 
  87   
  88  __all__ = [ 
  89      "DKIMException", 
  90      "InternalError", 
  91      "KeyFormatError", 
  92      "MessageFormatError", 
  93      "ParameterError", 
  94      "ValidationError", 
  95      "AuthresNotFoundError", 
  96      "NaClNotFoundError", 
  97      "CV_Pass", 
  98      "CV_Fail", 
  99      "CV_None", 
 100      "Relaxed", 
 101      "Simple", 
 102      "DKIM", 
 103      "ARC", 
 104      "sign", 
 105      "verify", 
 106      "dkim_sign", 
 107      "dkim_verify", 
 108      "arc_sign", 
 109      "arc_verify", 
 110  ] 
 111   
 112  Relaxed = b'relaxed'     
 113  Simple = b'simple'       
 114   
 115   
 116  CV_Pass = b'pass' 
 117  CV_Fail = b'fail' 
 118  CV_None = b'none' 
 119   
 120   
 122 -  def __init__(self, hasher, debug=False): 
  123      self.data = [] 
 124      self.hasher = hasher 
 125      self.name = hasher.name 
 126      self.debug = debug 
  127   
 129      if self.debug: 
 130          self.data.append(data) 
 131      return self.hasher.update(data) 
  132   
 134      return self.hasher.digest() 
  135   
 138   
 140      return b''.join(self.data) 
   141   
 142   
 144      """Return size of long in bits.""" 
 145      return len(bin(x)) - 2 
  146   
 147   
 149      """Base class for DKIM errors.""" 
 150      pass 
  151   
 152   
 154      """Internal error in dkim module. Should never happen.""" 
 155      pass 
  156   
 157   
 161   
 162   
 166   
 167   
 169      """Input parameter error.""" 
 170      pass 
  171   
 172   
 174      """Validation error.""" 
 175      pass 
  176   
 177   
 179      """ Authres Package not installed, needed for ARC """ 
 180      pass 
  181   
 182   
 184      """ Nacl package not installed, needed for ed25119 signatures """ 
 185      pass 
  186   
 188      """ Key type (k tag) is not known (rsa/ed25519) """ 
  189   
 190   
 192      """Select message header fields to be signed/verified. 
 193   
 194      >>> h = [('from','biz'),('foo','bar'),('from','baz'),('subject','boring')] 
 195      >>> i = ['from','subject','to','from'] 
 196      >>> select_headers(h,i) 
 197      [('from', 'baz'), ('subject', 'boring'), ('from', 'biz')] 
 198      >>> h = [('From','biz'),('Foo','bar'),('Subject','Boring')] 
 199      >>> i = ['from','subject','to','from'] 
 200      >>> select_headers(h,i) 
 201      [('From', 'biz'), ('Subject', 'Boring')] 
 202      """ 
 203      sign_headers = [] 
 204      lastindex = {} 
 205      for h in include_headers: 
 206          assert h == h.lower() 
 207          i = lastindex.get(h, len(headers)) 
 208          while i > 0: 
 209              i -= 1 
 210              if h == headers[i][0].lower(): 
 211                  sign_headers.append(headers[i]) 
 212                  break 
 213          lastindex[h] = i 
 214      return sign_headers 
  215   
 216   
 217   
 218  FWS = br'(?:(?:\s*\r?\n)?\s+)?' 
 219  RE_BTAG = re.compile(br'([;\s]b'+FWS+br'=)(?:'+FWS+br'[a-zA-Z0-9+/=])*(?:\r?\n\Z)?') 
 220   
 221   
 224      """Update hash for signed message header fields.""" 
 225      sign_headers = select_headers(headers,include_headers) 
 226       
 227       
 228      cheaders = canonicalize_headers.canonicalize_headers( 
 229          [(sigheader[0], RE_BTAG.sub(b'\\1',sigheader[1]))]) 
 230       
 231       
 232      for x,y in sign_headers + [(x, y.rstrip()) for x,y in cheaders]: 
 233          hasher.update(x) 
 234          hasher.update(b":") 
 235          hasher.update(y) 
 236      return sign_headers 
  237   
 238   
 241      """Update hash for signed message header fields.""" 
 242      hash_header = '' 
 243      sign_headers = select_headers(headers,include_headers) 
 244       
 245       
 246      cheaders = canonicalize_headers.canonicalize_headers( 
 247          [(sigheader[0], RE_BTAG.sub(b'\\1',sigheader[1]))]) 
 248       
 249       
 250      for x,y in sign_headers + [(x, y.rstrip()) for x,y in cheaders]: 
 251          hash_header += x + y 
 252      return sign_headers, hash_header 
  253   
 254   
 256      """Validate DKIM or ARC Signature fields. 
 257      Basic checks for presence and correct formatting of mandatory fields. 
 258      Raises a ValidationError if checks fail, otherwise returns None. 
 259      @param sig: A dict mapping field keys to values. 
 260      @param mandatory_fields: A list of non-optional fields 
 261      @param arc: flag to differentiate between dkim & arc 
 262      """ 
 263      if arc: 
 264          hashes = ARC_HASH_ALGORITHMS 
 265      else: 
 266          hashes = HASH_ALGORITHMS 
 267      for field in mandatory_fields: 
 268          if field not in sig: 
 269              raise ValidationError("missing %s=" % field) 
 270   
 271      if b'a' in sig and not sig[b'a'] in hashes: 
 272          raise ValidationError("unknown signature algorithm: %s" % sig[b'a']) 
 273   
 274      if b'b' in sig: 
 275          if re.match(br"[\s0-9A-Za-z+/]+=*$", sig[b'b']) is None: 
 276              raise ValidationError("b= value is not valid base64 (%s)" % sig[b'b']) 
 277          if len(re.sub(br"\s+", b"", sig[b'b'])) % 4 != 0: 
 278              raise ValidationError("b= value is not valid base64 (%s)" % sig[b'b']) 
 279   
 280      if b'bh' in sig: 
 281          if re.match(br"[\s0-9A-Za-z+/]+=*$", sig[b'bh']) is None: 
 282              raise ValidationError("bh= value is not valid base64 (%s)" % sig[b'bh']) 
 283          if len(re.sub(br"\s+", b"", sig[b'bh'])) % 4 != 0: 
 284              raise ValidationError("bh= value is not valid base64 (%s)" % sig[b'bh']) 
 285   
 286      if b'cv' in sig and sig[b'cv'] not in (CV_Pass, CV_Fail, CV_None): 
 287          raise ValidationError("cv= value is not valid (%s)" % sig[b'cv']) 
 288   
 289       
 290       
 291      if not arc and b'i' in sig and ( 
 292          not sig[b'i'].lower().endswith(sig[b'd'].lower()) or 
 293          sig[b'i'][-len(sig[b'd'])-1] not in ('@', '.', 64, 46)): 
 294          raise ValidationError( 
 295              "i= domain is not a subdomain of d= (i=%s d=%s)" % 
 296              (sig[b'i'], sig[b'd'])) 
 297      if b'l' in sig and re.match(br"\d{,76}$", sig[b'l']) is None: 
 298          raise ValidationError( 
 299              "l= value is not a decimal integer (%s)" % sig[b'l']) 
 300      if b'q' in sig and sig[b'q'] != b"dns/txt": 
 301          raise ValidationError("q= value is not dns/txt (%s)" % sig[b'q']) 
 302   
 303      if b't' in sig: 
 304          if re.match(br"\d+$", sig[b't']) is None: 
 305              raise ValidationError( 
 306                  "t= value is not a decimal integer (%s)" % sig[b't']) 
 307          now = int(time.time()) 
 308          slop = 36000  
 309          t_sign = int(sig[b't']) 
 310          if t_sign > now + slop: 
 311              raise ValidationError("t= value is in the future (%s)" % sig[b't']) 
 312   
 313      if b'v' in sig and sig[b'v'] != b"1": 
 314          raise ValidationError("v= value is not 1 (%s)" % sig[b'v']) 
 315   
 316      if b'x' in sig: 
 317          if re.match(br"\d+$", sig[b'x']) is None: 
 318              raise ValidationError( 
 319                "x= value is not a decimal integer (%s)" % sig[b'x']) 
 320          x_sign = int(sig[b'x']) 
 321          now = int(time.time()) 
 322          slop = 36000  
 323          if x_sign < now - slop: 
 324              raise ValidationError( 
 325                  "x= value is past (%s)" % sig[b'x']) 
 326              if x_sign < t_sign: 
 327                  raise ValidationError( 
 328                      "x= value is less than t= value (x=%s t=%s)" % 
 329                      (sig[b'x'], sig[b't'])) 
  330   
 331   
 333      """Parse a message in RFC822 format. 
 334   
 335      @param message: The message in RFC822 format. Either CRLF or LF is an accepted line separator. 
 336      @return: Returns a tuple of (headers, body) where headers is a list of (name, value) pairs. 
 337      The body is a CRLF-separated string. 
 338      """ 
 339      headers = [] 
 340      lines = re.split(b"\r?\n", message) 
 341      i = 0 
 342      while i < len(lines): 
 343          if len(lines[i]) == 0: 
 344               
 345              i += 1 
 346              break 
 347          if lines[i][0] in ("\x09", "\x20", 0x09, 0x20): 
 348              headers[-1][1] += lines[i]+b"\r\n" 
 349          else: 
 350              m = re.match(br"([\x21-\x7e]+?):", lines[i]) 
 351              if m is not None: 
 352                  headers.append([m.group(1), lines[i][m.end(0):]+b"\r\n"]) 
 353              elif lines[i].startswith(b"From "): 
 354                  pass 
 355              else: 
 356                  raise MessageFormatError("Unexpected characters in RFC822 header: %s" % lines[i]) 
 357          i += 1 
 358      return (headers, b"\r\n".join(lines[i:])) 
  359   
 360   
 362      """Normalize bytes/str to str for python 2/3 compatible doctests. 
 363      >>> text(b'foo') 
 364      'foo' 
 365      >>> text(u'foo') 
 366      'foo' 
 367      >>> text('foo') 
 368      'foo' 
 369      """ 
 370      if type(s) is str: return s 
 371      s = s.decode('ascii') 
 372      if type(s) is str: return s 
 373      return s.encode('ascii') 
  374   
 375   
 376 -def fold(header, namelen=0, linesep=b'\r\n'): 
  377      """Fold a header line into multiple crlf-separated lines of text at column 
 378       72.  The crlf does not count for line length. 
 379   
 380      >>> text(fold(b'foo')) 
 381      'foo' 
 382      >>> text(fold(b'foo  '+b'foo'*24).splitlines()[0]) 
 383      'foo ' 
 384      >>> text(fold(b'foo'*25).splitlines()[-1]) 
 385      ' foo' 
 386      >>> len(fold(b'foo'*25).splitlines()[0]) 
 387      72 
 388      >>> text(fold(b'x')) 
 389      'x' 
 390      >>> text(fold(b'xyz'*24)) 
 391      'xyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyz' 
 392      >>> len(fold(b'xyz'*48)) 
 393      150 
 394      """ 
 395       
 396       
 397      maxleng = 72 - namelen 
 398      if len(header) <= maxleng: 
 399          return header 
 400      if len(header) - header.rfind(b"\r\n") == 2 and len(header) <= maxleng +2: 
 401          return header 
 402      i = header.rfind(b"\r\n ") 
 403      if i == -1: 
 404          pre = b"" 
 405      else: 
 406          i += 3 
 407          pre = header[:i] 
 408          header = header[i:] 
 409      while len(header) > maxleng: 
 410          i = header[:maxleng].rfind(b" ") 
 411          if i == -1: 
 412              j = maxleng 
 413              pre += header[:j] + linesep + b" " 
 414          else: 
 415              j = i + 1 
 416              pre += header[:i] + linesep + b" " 
 417          header = header[j:] 
 418          maxleng = 71 
 419      if len(header) > 2: 
 420          return pre + header 
 421      else: 
 422          if pre[0] == b' ': 
 423              return pre[:-1] 
 424          else: 
 425              return pre + header 
  426   
 427   
 429    if not s: 
 430        raise KeyFormatError("missing public key: %s"%name) 
 431    try: 
 432        if type(s) is str: 
 433          s = s.encode('ascii') 
 434        pub = parse_tag_value(s) 
 435    except InvalidTagValueList as e: 
 436        raise KeyFormatError(e) 
 437    try: 
 438        if pub[b'v'] != b'DKIM1': 
 439            raise KeyFormatError("bad version") 
 440    except KeyError as e: 
 441         
 442        pass 
 443    try: 
 444        if pub[b'k'] == b'ed25519': 
 445            try: 
 446                pk = nacl.signing.VerifyKey(pub[b'p'], encoder=nacl.encoding.Base64Encoder) 
 447            except NameError: 
 448                raise NaClNotFoundError('pynacl module required for ed25519 signing, see README.md') 
 449            keysize = 256 
 450            ktag = b'ed25519' 
 451    except KeyError: 
 452        pub[b'k'] = b'rsa' 
 453    if pub[b'k'] == b'rsa': 
 454        try: 
 455            pk = parse_public_key(base64.b64decode(pub[b'p'])) 
 456            keysize = bitsize(pk['modulus']) 
 457        except KeyError: 
 458            raise KeyFormatError("incomplete public key: %s" % s) 
 459        except (TypeError,UnparsableKeyError) as e: 
 460            raise KeyFormatError("could not parse public key (%s): %s" % (pub[b'p'],e)) 
 461        ktag = b'rsa' 
 462    if pub[b'k'] != b'rsa' and pub[b'k'] != b'ed25519': 
 463        raise KeyFormatError('unknown algorithm in k= tag: {0}'.format(pub[b'k'])) 
 464    seqtlsrpt = False 
 465    try: 
 466         
 467        if pub[b's'] != b'*' and pub[b's'] != b'email' and pub[b's'] != b'tlsrpt': 
 468            pk = None 
 469            keysize = None 
 470            ktag = None 
 471            raise KeyFormatError('unknown service type in s= tag: {0}'.format(pub[b's'])) 
 472        elif pub[b's'] == b'tlsrpt': 
 473            seqtlsrpt = True 
 474    except: 
 475         
 476        pass 
 477    return pk, keysize, ktag, seqtlsrpt 
  478   
 479   
 484   
 485   
 486   
 487 -class DomainSigner(object): 
  488     
 489     
 490   
 491     
 492     
 493     
 494     
 495     
 496     
 497     
 498     
 499     
 500     
 501 -  def __init__(self,message=None,logger=None,signature_algorithm=b'rsa-sha256', 
 502          minkey=1024, linesep=b'\r\n', debug_content=False, timeout=5, 
 503          tlsrpt=False): 
  504      self.set_message(message) 
 505      if logger is None: 
 506          logger = get_default_logger() 
 507      self.logger = logger 
 508      self.debug_content = debug_content and logger.isEnabledFor(logging.DEBUG) 
 509      if signature_algorithm not in HASH_ALGORITHMS: 
 510          raise ParameterError( 
 511              "Unsupported signature algorithm: "+signature_algorithm) 
 512      self.signature_algorithm = signature_algorithm 
 513       
 514      self.should_sign = set(DKIM.SHOULD) 
 515       
 516       
 517       
 518       
 519      self.should_not_sign = set(DKIM.SHOULD_NOT) 
 520       
 521      self.frozen_sign = set(DKIM.FROZEN) 
 522       
 523       
 524      self.minkey = minkey 
 525       
 526      self.linesep = linesep 
 527      self.timeout = timeout 
 528      self.tlsrpt = tlsrpt 
 529       
 530      self.seqtlsrpt = False 
  531   
 532   
 533     
 534     
 535     
 536     
 537    FROZEN = (b'from',) 
 538   
 539     
 540     
 541    SHOULD = ( 
 542      b'from', b'sender', b'reply-to', b'subject', b'date', b'message-id', b'to', b'cc', 
 543      b'mime-version', b'content-type', b'content-transfer-encoding', 
 544      b'content-id', b'content-description', b'resent-date', b'resent-from', 
 545      b'resent-sender', b'resent-to', b'resent-cc', b'resent-message-id', 
 546      b'in-reply-to', b'references', b'list-id', b'list-help', b'list-unsubscribe', 
 547      b'list-subscribe', b'list-post', b'list-owner', b'list-archive' 
 548    ) 
 549   
 550     
 551     
 552    SHOULD_NOT = ( 
 553      b'return-path',b'received',b'comments',b'keywords',b'bcc',b'resent-bcc', 
 554      b'dkim-signature' 
 555    ) 
 556   
 557     
 558     
 559     
 560     
 561     
 562     
 563     
 564     
 565     
 566    RFC5322_SINGLETON = (b'date',b'from',b'sender',b'reply-to',b'to',b'cc',b'bcc', 
 567          b'message-id',b'in-reply-to',b'references') 
 568   
 569 -  def add_frozen(self,s): 
  570      """ Add headers not in should_not_sign to frozen_sign. 
 571      @param s: list of headers to add to frozen_sign 
 572      @since: 0.5 
 573   
 574      >>> dkim = DKIM() 
 575      >>> dkim.add_frozen(DKIM.RFC5322_SINGLETON) 
 576      >>> [text(x) for x in sorted(dkim.frozen_sign)] 
 577      ['cc', 'date', 'from', 'in-reply-to', 'message-id', 'references', 'reply-to', 'sender', 'to'] 
 578      >>> dkim2 = DKIM() 
 579      >>> dkim2.add_frozen((b'date',b'subject')) 
 580      >>> [text(x) for x in sorted(dkim2.frozen_sign)] 
 581      ['date', 'from', 'subject'] 
 582      """ 
 583      self.frozen_sign.update(x.lower() for x in s 
 584          if x.lower() not in self.should_not_sign) 
  585   
 586   
 587 -  def add_should_not(self,s): 
  588      """ Add headers not in should_not_sign to frozen_sign. 
 589      @param s: list of headers to add to frozen_sign 
 590      @since: 0.9 
 591   
 592      >>> dkim = DKIM() 
 593      >>> dkim.add_should_not(DKIM.RFC5322_SINGLETON) 
 594      >>> [text(x) for x in sorted(dkim.should_not_sign)] 
 595      ['bcc', 'cc', 'comments', 'date', 'dkim-signature', 'in-reply-to', 'keywords', 'message-id', 'received', 'references', 'reply-to', 'resent-bcc', 'return-path', 'sender', 'to'] 
 596      """ 
 597      self.should_not_sign.update(x.lower() for x in s 
 598          if x.lower() not in self.frozen_sign) 
  599   
 600   
 601     
 602     
 603     
 604     
 605 -  def set_message(self,message): 
  606      if message: 
 607        self.headers, self.body = rfc822_parse(message) 
 608      else: 
 609        self.headers, self.body = [],'' 
 610       
 611      self.domain = None 
 612       
 613      self.selector = 'default' 
 614       
 615       
 616       
 617      self.signature_fields = {} 
 618       
 619       
 620       
 621      self.signed_headers = [] 
 622       
 623      self.keysize = 0 
  624   
 626      """Return the default list of headers to sign: those in should_sign or 
 627      frozen_sign, with those in frozen_sign signed an extra time to prevent 
 628      additions. 
 629      @since: 0.5""" 
 630      hset = self.should_sign | self.frozen_sign 
 631      include_headers = [ x for x,y in self.headers 
 632          if x.lower() in hset ] 
 633      return include_headers + [ x for x in include_headers 
 634          if x.lower() in self.frozen_sign] 
  635   
 637      """Return header list of all existing headers not in should_not_sign. 
 638      @since: 0.5""" 
 639      return [x for x,y in self.headers if x.lower() not in self.should_not_sign] 
  640   
 641   
 642     
 643     
 644     
 645     
 646     
 647     
 648     
 649 -  def gen_header(self, fields, include_headers, canon_policy, header_name, pk, standardize=False): 
  650      if standardize: 
 651          lower = [(x,y.lower().replace(b' ', b'')) for (x,y) in fields if x != b'bh'] 
 652          reg   = [(x,y.replace(b' ', b'')) for (x,y) in fields if x == b'bh'] 
 653          fields = lower + reg 
 654          fields = sorted(fields, key=(lambda x: x[0])) 
 655   
 656      header_value = b"; ".join(b"=".join(x) for x in fields) 
 657      if not standardize: 
 658        header_value = fold(header_value, namelen=len(header_name), linesep=b'\r\n') 
 659      header_value = RE_BTAG.sub(b'\\1',header_value) 
 660      header = (header_name, b' ' + header_value) 
 661      h = HashThrough(self.hasher(), self.debug_content) 
 662      sig = dict(fields) 
 663   
 664      headers = canon_policy.canonicalize_headers(self.headers) 
 665      self.signed_headers = hash_headers( 
 666          h, canon_policy, headers, include_headers, header, sig) 
 667      if self.debug_content: 
 668          self.logger.debug("sign %s headers: %r" % (header_name, h.hashed())) 
 669   
 670      if self.signature_algorithm == b'rsa-sha256' or self.signature_algorithm == b'rsa-sha1': 
 671          try: 
 672              sig2 = RSASSA_PKCS1_v1_5_sign(h, pk) 
 673          except DigestTooLargeError: 
 674              raise ParameterError("digest too large for modulus") 
 675      elif self.signature_algorithm == b'ed25519-sha256': 
 676          sigobj = pk.sign(h.digest()) 
 677          sig2 = sigobj.signature 
 678       
 679       
 680       
 681       
 682       
 683      idx = [i for i in range(len(fields)) if fields[i][0] == b'b'][0] 
 684      fields[idx] = (b'b', base64.b64encode(bytes(sig2))) 
 685      header_value = b"; ".join(b"=".join(x) for x in fields) + self.linesep 
 686   
 687      if not standardize: 
 688        header_value = fold(header_value, namelen=len(header_name), linesep=self.linesep) 
 689   
 690      return header_value 
  691   
 692 -  def verify_sig_process(self, sig, include_headers, sig_header, dnsfunc): 
  693      """Non-async sensitive verify_sig elements.  Separated to avoid async code 
 694      duplication.""" 
 695       
 696      if self.tlsrpt == 'strict' and not self.seqtlsrpt: 
 697          raise ValidationError("Message is tlsrpt and Service Type is not tlsrpt") 
 698       
 699      if not self.tlsrpt and self.seqtlsrpt: 
 700          raise ValidationError("Message is not tlsrpt and Service Type is tlsrpt") 
 701   
 702      try: 
 703          canon_policy = CanonicalizationPolicy.from_c_value(sig.get(b'c', b'simple/simple')) 
 704      except InvalidCanonicalizationPolicyError as e: 
 705          raise MessageFormatError("invalid c= value: %s" % e.args[0]) 
 706   
 707      hasher = HASH_ALGORITHMS[sig[b'a']] 
 708   
 709       
 710      if b'bh' in sig: 
 711        h = HashThrough(hasher(), self.debug_content) 
 712   
 713        body = canon_policy.canonicalize_body(self.body) 
 714        if b'l' in sig and not self.tlsrpt: 
 715          body = body[:int(sig[b'l'])] 
 716        h.update(body) 
 717        if self.debug_content: 
 718            self.logger.debug("body hashed: %r" % h.hashed()) 
 719        bodyhash = h.digest() 
 720   
 721        self.logger.debug("bh: %s" % base64.b64encode(bodyhash)) 
 722        try: 
 723            bh = base64.b64decode(re.sub(br"\s+", b"", sig[b'bh'])) 
 724        except TypeError as e: 
 725            raise MessageFormatError(str(e)) 
 726        if bodyhash != bh: 
 727            raise ValidationError( 
 728                "body hash mismatch (got %s, expected %s)" % 
 729                (base64.b64encode(bodyhash), sig[b'bh'])) 
 730   
 731       
 732       
 733       
 734       
 735      if b'from' in include_headers: 
 736        include_headers.append(b'from') 
 737      h = HashThrough(hasher(), self.debug_content) 
 738   
 739      headers = canon_policy.canonicalize_headers(self.headers) 
 740      self.signed_headers = hash_headers( 
 741          h, canon_policy, headers, include_headers, sig_header, sig) 
 742      if self.debug_content: 
 743          self.logger.debug("signed for %s: %r" % (sig_header[0], h.hashed())) 
 744      signature = base64.b64decode(re.sub(br"\s+", b"", sig[b'b'])) 
 745      if self.ktag == b'rsa': 
 746          try: 
 747              res = RSASSA_PKCS1_v1_5_verify(h, signature, self.pk) 
 748              self.logger.debug("%s valid: %s" % (sig_header[0], res)) 
 749              if res and self.keysize < self.minkey: 
 750                  raise KeyFormatError("public key too small: %d" % self.keysize) 
 751              return res 
 752          except (TypeError,DigestTooLargeError) as e: 
 753              raise KeyFormatError("digest too large for modulus: %s"%e) 
 754      elif self.ktag == b'ed25519': 
 755          try: 
 756              self.pk.verify(h.digest(), signature) 
 757              self.logger.debug("%s valid" % (sig_header[0])) 
 758              return True 
 759          except (nacl.exceptions.BadSignatureError) as e: 
 760              return False 
 761      else: 
 762          raise UnknownKeyTypeError(self.ktag) 
  763   
 764   
 765     
 766     
 767     
 768     
 769     
 770 -  def verify_sig(self, sig, include_headers, sig_header, dnsfunc): 
  771      name = sig[b's'] + b"._domainkey." + sig[b'd'] + b"." 
 772      try: 
 773        self.pk, self.keysize, self.ktag, self.seqtlsrpt = load_pk_from_dns(name, 
 774                dnsfunc, timeout=self.timeout) 
 775      except KeyFormatError as e: 
 776        self.logger.error("%s" % e) 
 777        return False 
 778      except binascii.Error as e: 
 779        self.logger.error('KeyFormatError: {0}'.format(e)) 
 780        return False 
 781      return self.verify_sig_process(sig, include_headers, sig_header, dnsfunc) 
   782   
 783   
 784   
 785 -class DKIM(DomainSigner): 
  786     
 787     
 788     
 789     
 790     
 791     
 792     
 793     
 794     
 795     
 796     
 797     
 798     
 799     
 800     
 801     
 802     
 803     
 804     
 805     
 806     
 807     
 808     
 809     
 810     
 811     
 812     
 813     
 814     
 815     
 816     
 817     
 818     
 819     
 820     
 821 -  def sign(self, selector, domain, privkey, signature_algorithm=None, identity=None, 
 822          canonicalize=(b'relaxed',b'simple'), include_headers=None, length=False): 
  823      if signature_algorithm: 
 824          self.signature_algorithm = signature_algorithm 
 825      if self.signature_algorithm == b'rsa-sha256' or self.signature_algorithm == b'rsa-sha1': 
 826          try: 
 827              pk = parse_pem_private_key(privkey) 
 828          except UnparsableKeyError as e: 
 829              raise KeyFormatError(str(e)) 
 830      elif self.signature_algorithm == b'ed25519-sha256': 
 831          try: 
 832              pk = nacl.signing.SigningKey(privkey, encoder=nacl.encoding.Base64Encoder) 
 833          except NameError: 
 834              raise NaClNotFoundError('pynacl module required for ed25519 signing, see README.md') 
 835   
 836      if identity is not None and not identity.endswith(domain): 
 837          raise ParameterError("identity must end with domain") 
 838   
 839      canon_policy = CanonicalizationPolicy.from_c_value(b'/'.join(canonicalize)) 
 840   
 841      if include_headers is None: 
 842          include_headers = self.default_sign_headers() 
 843      try: 
 844          include_headers = [bytes(x, 'utf-8') for x in include_headers] 
 845      except TypeError: 
 846           
 847           
 848          pass 
 849   
 850      include_headers = tuple([x.lower() for x in include_headers]) 
 851       
 852      self.include_headers = include_headers 
 853   
 854      if self.tlsrpt: 
 855           
 856          length = False 
 857   
 858       
 859      if b'from' not in include_headers: 
 860          raise ParameterError("The From header field MUST be signed") 
 861   
 862       
 863       
 864      for x in set(include_headers).intersection(self.should_not_sign): 
 865        raise ParameterError("The %s header field SHOULD NOT be signed"%x) 
 866   
 867      body = canon_policy.canonicalize_body(self.body) 
 868   
 869      self.hasher = HASH_ALGORITHMS[self.signature_algorithm] 
 870      h = self.hasher() 
 871      h.update(body) 
 872      bodyhash = base64.b64encode(h.digest()) 
 873   
 874      sigfields = [x for x in [ 
 875          (b'v', b"1"), 
 876          (b'a', self.signature_algorithm), 
 877          (b'c', canon_policy.to_c_value()), 
 878          (b'd', domain), 
 879          (b'i', identity or b"@"+domain), 
 880          length and (b'l', str(len(body)).encode('ascii')), 
 881          (b'q', b"dns/txt"), 
 882          (b's', selector), 
 883          (b't', str(int(time.time())).encode('ascii')), 
 884          (b'h', b" : ".join(include_headers)), 
 885          (b'bh', bodyhash), 
 886           
 887           
 888          (b'b', b'0'*60), 
 889      ] if x] 
 890   
 891      res = self.gen_header(sigfields, include_headers, canon_policy, 
 892                             b"DKIM-Signature", pk) 
 893   
 894      self.domain = domain 
 895      self.selector = selector 
 896      self.signature_fields = dict(sigfields) 
 897      return b'DKIM-Signature: ' + res 
  898   
 899     
 900     
 902      return (len([(x,y) for x,y in self.headers if x.lower() == b"dkim-signature"]) > 0) 
  903   
 905      """Non-DNS verify parts to minimize asyncio code duplication.""" 
 906   
 907      sigheaders = [(x,y) for x,y in self.headers if x.lower() == b"dkim-signature"] 
 908      if len(sigheaders) <= idx: 
 909          return False 
 910   
 911       
 912      try: 
 913          sig = parse_tag_value(sigheaders[idx][1]) 
 914          self.signature_fields = sig 
 915      except InvalidTagValueList as e: 
 916          raise MessageFormatError(e) 
 917   
 918      self.logger.debug("sig: %r" % sig) 
 919   
 920      validate_signature_fields(sig) 
 921      self.domain = sig[b'd'] 
 922      self.selector = sig[b's'] 
 923   
 924      include_headers = [x.lower() for x in re.split(br"\s*:\s*", sig[b'h'])] 
 925      self.include_headers = tuple(include_headers) 
 926      return sig, include_headers, sigheaders 
  927   
 928     
 929     
 930     
 931     
 932     
 933     
 934     
 935     
  939   
 940   
 941   
 942 -class ARC(DomainSigner): 
  943     
 944    ARC_HEADERS = (b'arc-seal', b'arc-message-signature', b'arc-authentication-results') 
 945   
 946     
 947    INSTANCE_RE = re.compile(br'[\s;]?i\s*=\s*(\d+)', re.MULTILINE | re.IGNORECASE) 
 948   
 950      headers = [] 
 951       
 952      relaxed_headers = RelaxedCanonicalization.canonicalize_headers(self.headers) 
 953      for x,y in relaxed_headers: 
 954        if x.lower() in ARC.ARC_HEADERS: 
 955          m = ARC.INSTANCE_RE.search(y) 
 956          if m is not None: 
 957            try: 
 958              i = int(m.group(1)) 
 959              headers.append((i, (x, y))) 
 960            except ValueError: 
 961              self.logger.debug("invalid instance number %s: '%s: %s'" % (m.group(1), x, y)) 
 962          else: 
 963            self.logger.debug("not instance number: '%s: %s'" % (x, y)) 
 964   
 965      if len(headers) == 0: 
 966        return 0, [] 
 967   
 968      def arc_header_key(a): 
 969        return [a[0], a[1][0].lower(), a[1][1].lower()] 
  970   
 971      headers = sorted(headers, key=arc_header_key) 
 972      headers.reverse() 
 973      return headers[0][0], headers 
  974   
 975     
 976     
 977     
 978     
 979     
 980     
 981     
 982     
 983     
 984     
 985     
 986     
 987     
 988     
 989     
 990     
 991     
 992     
 993     
 994     
 995     
 996     
 997     
 998     
 999     
1000     
1001     
1002 -  def sign(self, selector, domain, privkey, srv_id, include_headers=None, 
1003             timestamp=None, standardize=False): 
 1004   
1005      INSTANCE_LIMIT = 50  
1006      self.add_should_not(('Authentication-Results',)) 
1007       
1008      try: 
1009          AuthenticationResultsHeader 
1010      except: 
1011          self.logger.debug("authres package not installed") 
1012          raise AuthresNotFoundError 
1013   
1014      try: 
1015          pk = parse_pem_private_key(privkey) 
1016      except UnparsableKeyError as e: 
1017          raise KeyFormatError(str(e)) 
1018   
1019       
1020      ar_headers = [res.strip() for [ar, res] in self.headers if ar == b'Authentication-Results'] 
1021      grouped_headers = [(res, AuthenticationResultsHeader.parse('Authentication-Results: ' + res.decode('utf-8'))) 
1022                         for res in ar_headers] 
1023      auth_headers = [res for res in grouped_headers if res[1].authserv_id == srv_id.decode('utf-8')] 
1024   
1025      if len(auth_headers) == 0: 
1026        self.logger.debug("no AR headers found, chain terminated") 
1027        return [] 
1028   
1029       
1030      results_lists = [raw.replace(srv_id + b';', b'').strip() for (raw, parsed) in auth_headers] 
1031      results_lists = [tags.split(b';') for tags in results_lists] 
1032      results = [tag.strip() for sublist in results_lists for tag in sublist] 
1033      auth_results = srv_id + b'; ' + (b';' + self.linesep + b'  ').join(results) 
1034   
1035       
1036      parsed_auth_results = AuthenticationResultsHeader.parse('Authentication-Results: ' + auth_results.decode('utf-8')) 
1037      arc_results = [res for res in parsed_auth_results.results if res.method == 'arc'] 
1038      if len(arc_results) == 0: 
1039        chain_validation_status = CV_None 
1040      elif len(arc_results) != 1: 
1041        self.logger.debug("multiple AR arc stamps found, failing chain") 
1042        chain_validation_status = CV_Fail 
1043      else: 
1044        chain_validation_status = arc_results[0].result.lower().encode('utf-8') 
1045   
1046       
1047      if include_headers is None: 
1048          include_headers = self.default_sign_headers() 
1049   
1050      include_headers = tuple([x.lower() for x in include_headers]) 
1051   
1052       
1053      self.include_headers = include_headers 
1054   
1055       
1056      if b'from' not in include_headers: 
1057          raise ParameterError("The From header field MUST be signed") 
1058   
1059       
1060       
1061      for x in set(include_headers).intersection(self.should_not_sign): 
1062          raise ParameterError("The %s header field SHOULD NOT be signed"%x) 
1063   
1064      max_instance, arc_headers_w_instance = self.sorted_arc_headers() 
1065      instance = 1 
1066      if len(arc_headers_w_instance) != 0: 
1067          instance = max_instance + 1 
1068      if instance > INSTANCE_LIMIT: 
1069          raise ParameterError("Maximum instance tag value exceeded") 
1070   
1071      if instance == 1 and chain_validation_status != CV_None: 
1072          raise ParameterError("No existing chain found on message, cv should be none") 
1073      elif instance != 1 and chain_validation_status == CV_None: 
1074        self.logger.debug("no previous AR arc results found and instance > 1, chain terminated") 
1075        return [] 
1076   
1077      new_arc_set = [] 
1078      if chain_validation_status != CV_Fail: 
1079        arc_headers = [y for x,y in arc_headers_w_instance] 
1080      else:  
1081        arc_headers = [] 
1082   
1083       
1084      aar_value = ("i=%d; " % instance).encode('utf-8') + auth_results 
1085      if aar_value[-1] != b'\n': aar_value += b'\r\n' 
1086   
1087      new_arc_set.append(b"ARC-Authentication-Results: " + aar_value) 
1088      self.headers.insert(0, (b"arc-authentication-results", aar_value)) 
1089      arc_headers.insert(0, (b"ARC-Authentication-Results", aar_value)) 
1090   
1091       
1092      canon_policy = CanonicalizationPolicy.from_c_value(b'relaxed/relaxed') 
1093   
1094      self.hasher = HASH_ALGORITHMS[self.signature_algorithm] 
1095      h = HashThrough(self.hasher(), self.debug_content) 
1096      h.update(canon_policy.canonicalize_body(self.body)) 
1097      if self.debug_content: 
1098          self.logger.debug("sign ams body hashed: %r" % h.hashed()) 
1099      bodyhash = base64.b64encode(h.digest()) 
1100   
1101       
1102      timestamp = str(timestamp or int(time.time())).encode('ascii') 
1103      ams_fields = [x for x in [ 
1104          (b'i', str(instance).encode('ascii')), 
1105          (b'a', self.signature_algorithm), 
1106          (b'c', b'relaxed/relaxed'), 
1107          (b'd', domain), 
1108          (b's', selector), 
1109          (b't', timestamp), 
1110          (b'h', b" : ".join(include_headers)), 
1111          (b'bh', bodyhash), 
1112           
1113           
1114          (b'b', b'0'*60), 
1115      ] if x] 
1116   
1117      res = self.gen_header(ams_fields, include_headers, canon_policy, 
1118                            b"ARC-Message-Signature", pk, standardize) 
1119   
1120      new_arc_set.append(b"ARC-Message-Signature: " + res) 
1121      self.headers.insert(0, (b"ARC-Message-Signature", res)) 
1122      arc_headers.insert(0, (b"ARC-Message-Signature", res)) 
1123   
1124       
1125      as_fields = [x for x in [ 
1126          (b'i', str(instance).encode('ascii')), 
1127          (b'cv', chain_validation_status), 
1128          (b'a', self.signature_algorithm), 
1129          (b'd', domain), 
1130          (b's', selector), 
1131          (b't', timestamp), 
1132           
1133           
1134          (b'b', b'0'*60), 
1135      ] if x] 
1136   
1137      as_include_headers = [x[0].lower() for x in arc_headers] 
1138      as_include_headers.reverse() 
1139   
1140       
1141       
1142      if chain_validation_status == CV_Fail: 
1143        self.headers.reverse() 
1144      if b'h' in as_fields: 
1145          raise ValidationError("h= tag not permitted in ARC-Seal header field")     
1146      res = self.gen_header(as_fields, as_include_headers, canon_policy, 
1147                             b"ARC-Seal", pk, standardize) 
1148   
1149      new_arc_set.append(b"ARC-Seal: " + res) 
1150      self.headers.insert(0, (b"ARC-Seal", res)) 
1151      arc_headers.insert(0, (b"ARC-Seal", res)) 
1152   
1153      new_arc_set.reverse() 
1154   
1155      return new_arc_set 
 1156   
1157     
1158     
1159     
1160     
1161     
1162     
1163     
1164     
1165     
1166     
1168      result_data = [] 
1169      max_instance, arc_headers_w_instance = self.sorted_arc_headers() 
1170      if max_instance == 0: 
1171          return CV_None, result_data, "Message is not ARC signed" 
1172      for instance in range(max_instance, 0, -1): 
1173          try: 
1174              result = self.verify_instance(arc_headers_w_instance, instance, dnsfunc=dnsfunc) 
1175              result_data.append(result) 
1176          except DKIMException as e: 
1177              self.logger.error("%s" % e) 
1178              return CV_Fail, result_data, "%s" % e 
1179   
1180       
1181      if not result_data[0]['ams-valid']: 
1182          return CV_Fail, result_data, "Most recent ARC-Message-Signature did not validate" 
1183      for result in result_data: 
1184        if result['cv'] == CV_Fail: 
1185          return None, result_data, "ARC-Seal[%d] reported failure, the chain is terminated" % result['instance'] 
1186        elif not result['as-valid']: 
1187          return CV_Fail, result_data, "ARC-Seal[%d] did not validate" % result['instance'] 
1188        elif (result['instance'] == 1) and (result['cv'] != CV_None): 
1189          return CV_Fail, result_data, "ARC-Seal[%d] reported invalid status %s" % (result['instance'], result['cv']) 
1190        elif (result['instance'] != 1) and (result['cv'] == CV_None): 
1191          return CV_Fail, result_data, "ARC-Seal[%d] reported invalid status %s" % (result['instance'], result['cv']) 
1192      return CV_Pass, result_data, "success" 
 1193   
1194     
1195     
1196     
1197     
1198     
1199     
1200     
1201     
1202     
1203     
1204     
1206      if (instance == 0) or (len(arc_headers_w_instance) == 0): 
1207          raise ParameterError("request to verify instance %d not present" % (instance)) 
1208   
1209      aar_value = None 
1210      ams_value = None 
1211      as_value = None 
1212      arc_headers = [] 
1213      output = { 'instance': instance } 
1214   
1215      for i, arc_header in arc_headers_w_instance: 
1216        if i > instance: continue 
1217        arc_headers.append(arc_header) 
1218        if i == instance: 
1219          if arc_header[0].lower() == b"arc-authentication-results": 
1220            if aar_value is not None: 
1221              raise MessageFormatError("Duplicate ARC-Authentication-Results for instance %d" % instance) 
1222            aar_value = arc_header[1] 
1223          elif arc_header[0].lower() == b"arc-message-signature": 
1224            if ams_value is not None: 
1225              raise MessageFormatError("Duplicate ARC-Message-Signature for instance %d" % instance) 
1226            ams_value = arc_header[1] 
1227          elif arc_header[0].lower() == b"arc-seal": 
1228            if as_value is not None: 
1229              raise MessageFormatError("Duplicate ARC-Seal for instance %d" % instance) 
1230            as_value = arc_header[1] 
1231   
1232      if (aar_value is None) or (ams_value is None) or (as_value is None): 
1233          raise MessageFormatError("Incomplete ARC set for instance %d" % instance) 
1234   
1235      output['aar-value'] = aar_value 
1236   
1237       
1238      try: 
1239          sig = parse_tag_value(ams_value) 
1240      except InvalidTagValueList as e: 
1241          raise MessageFormatError(e) 
1242   
1243      self.logger.debug("ams sig[%d]: %r" % (instance, sig)) 
1244   
1245      validate_signature_fields(sig, [b'i', b'a', b'b', b'bh', b'd', b'h', b's'], True) 
1246      output['ams-domain'] = sig[b'd'] 
1247      output['ams-selector'] = sig[b's'] 
1248   
1249      include_headers = [x.lower() for x in re.split(br"\s*:\s*", sig[b'h'])] 
1250      if b'arc-seal' in include_headers: 
1251          raise ParameterError("The Arc-Message-Signature MUST NOT sign ARC-Seal") 
1252   
1253      ams_header = (b'ARC-Message-Signature', b' ' + ams_value) 
1254   
1255   
1256       
1257       
1258       
1259      raw_ams_header = [(x, y) for (x, y) in self.headers if x.lower() == b'arc-message-signature'][0] 
1260   
1261       
1262      if b'c' not in sig: 
1263          sig[b'c'] = b'relaxed/relaxed' 
1264      try: 
1265        ams_valid = self.verify_sig(sig, include_headers, raw_ams_header, dnsfunc) 
1266      except DKIMException as e: 
1267        self.logger.error("%s" % e) 
1268        ams_valid = False 
1269   
1270      output['ams-valid'] = ams_valid 
1271      self.logger.debug("ams valid: %r" % ams_valid) 
1272   
1273       
1274      try: 
1275          sig = parse_tag_value(as_value) 
1276      except InvalidTagValueList as e: 
1277          raise MessageFormatError(e) 
1278   
1279      self.logger.debug("as sig[%d]: %r" % (instance, sig)) 
1280   
1281      validate_signature_fields(sig, [b'i', b'a', b'b', b'cv', b'd', b's'], True) 
1282      if b'h' in sig: 
1283          raise ValidationError("h= tag not permitted in ARC-Seal header field") 
1284   
1285      output['as-domain'] = sig[b'd'] 
1286      output['as-selector'] = sig[b's'] 
1287      output['cv'] = sig[b'cv'] 
1288   
1289      as_include_headers = [x[0].lower() for x in arc_headers] 
1290      as_include_headers.reverse() 
1291      as_header = (b'ARC-Seal', b' ' + as_value) 
1292       
1293      if b'c' not in sig: 
1294          sig[b'c'] = b'relaxed/relaxed' 
1295      try: 
1296        as_valid = self.verify_sig(sig, as_include_headers[:-1], as_header, dnsfunc) 
1297      except DKIMException as e: 
1298        self.logger.error("%s" % e) 
1299        as_valid = False 
1300   
1301      output['as-valid'] = as_valid 
1302      self.logger.debug("as valid: %r" % as_valid) 
1303      return output 
 1304   
1305   
1306 -def sign(message, selector, domain, privkey, identity=None, 
1307           canonicalize=(b'relaxed', b'simple'), 
1308           signature_algorithm=b'rsa-sha256', 
1309           include_headers=None, length=False, logger=None, 
1310           linesep=b'\r\n', tlsrpt=False): 
 1311       
1312      """Sign an RFC822 message and return the DKIM-Signature header line. 
1313      @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings) 
1314      @param selector: the DKIM selector value for the signature 
1315      @param domain: the DKIM domain value for the signature 
1316      @param privkey: a PKCS#1 private key in base64-encoded text form 
1317      @param identity: the DKIM identity value for the signature (default "@"+domain) 
1318      @param canonicalize: the canonicalization algorithms to use (default (Simple, Simple)) 
1319      @param signature_algorithm: the signing algorithm to use when signing 
1320      @param include_headers: a list of strings indicating which headers are to be signed (default all headers not listed as SHOULD NOT sign) 
1321      @param length: true if the l= tag should be included to indicate body length (default False) 
1322      @param logger: a logger to which debug info will be written (default None) 
1323      @param linesep: use this line seperator for folding the headers 
1324      @param tlsrpt: message is an RFC 8460 TLS report (default False) 
1325       False: Not a tlsrpt, True: Is a tlsrpt, 'strict': tlsrpt, invalid if 
1326       service type is missing. For signing, if True, length is never used. 
1327      @return: DKIM-Signature header field terminated by \\r\\n 
1328      @raise DKIMException: when the message, include_headers, or key are badly formed. 
1329      """ 
1330   
1331      d = DKIM(message,logger=logger,signature_algorithm=signature_algorithm,linesep=linesep,tlsrpt=tlsrpt) 
1332      return d.sign(selector, domain, privkey, identity=identity, canonicalize=canonicalize, include_headers=include_headers, length=length) 
 1333   
1334   
1335 -def verify(message, logger=None, dnsfunc=get_txt, minkey=1024, 
1336          timeout=5, tlsrpt=False): 
 1337      """Verify the first (topmost) DKIM signature on an RFC822 formatted message. 
1338      @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings) 
1339      @param logger: a logger to which debug info will be written (default None) 
1340      @param timeout: number of seconds for DNS lookup timeout (default = 5) 
1341      @param tlsrpt: message is an RFC 8460 TLS report (default False) 
1342       False: Not a tlsrpt, True: Is a tlsrpt, 'strict': tlsrpt, invalid if 
1343       service type is missing. For signing, if True, length is never used. 
1344      @return: True if signature verifies or False otherwise 
1345      """ 
1346       
1347      d = DKIM(message,logger=logger,minkey=minkey,timeout=timeout,tlsrpt=tlsrpt) 
1348      try: 
1349          return d.verify(dnsfunc=dnsfunc) 
1350      except DKIMException as x: 
1351          if logger is not None: 
1352              logger.error("%s" % x) 
1353          return False 
 1354   
1355   
1356   
1357  if sys.version_info >= (3, 5): 
1358      try: 
1359          import aiodns 
1360          from dkim.asyncsupport import verify_async 
1361          dkim_verify_async = verify_async 
1362      except ImportError: 
1363           
1364          pass 
1365   
1366   
1367   
1368  dkim_sign = sign 
1369  dkim_verify = verify 
1370   
1371   
1372 -def arc_sign(message, selector, domain, privkey, 
1373               srv_id, signature_algorithm=b'rsa-sha256', 
1374               include_headers=None, timestamp=None, 
1375               logger=None, standardize=False, linesep=b'\r\n'): 
 1376       
1377      """Sign an RFC822 message and return the ARC set header lines for the next instance 
1378      @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings) 
1379      @param selector: the DKIM selector value for the signature 
1380      @param domain: the DKIM domain value for the signature 
1381      @param privkey: a PKCS#1 private key in base64-encoded text form 
1382      @param srv_id: the authserv_id used to identify the ADMD's AR headers and to use for ARC authserv_id 
1383      @param signature_algorithm: the signing algorithm to use when signing 
1384      @param include_headers: a list of strings indicating which headers are to be signed (default all headers not listed as SHOULD NOT sign) 
1385      @param timestamp: the time in integer seconds when the message is sealed (default is int(time.time) based on platform, can be string or int) 
1386      @param logger: a logger to which debug info will be written (default None) 
1387      @param linesep: use this line seperator for folding the headers 
1388      @return: A list containing the ARC set of header fields for the next instance 
1389      @raise DKIMException: when the message, include_headers, or key are badly formed. 
1390      """ 
1391   
1392      a = ARC(message,logger=logger,signature_algorithm=b'rsa-sha256',linesep=linesep) 
1393      if not include_headers: 
1394          include_headers = a.default_sign_headers() 
1395      return a.sign(selector, domain, privkey, srv_id, include_headers=include_headers, 
1396                    timestamp=timestamp, standardize=standardize) 
 1397   
1398   
1400       
1401      """Verify the ARC chain on an RFC822 formatted message. 
1402      @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings) 
1403      @param logger: a logger to which debug info will be written (default None) 
1404      @param dnsfunc: an optional function to lookup TXT resource records 
1405      @param minkey: the minimum key size to accept 
1406      @param timeout: number of seconds for DNS lookup timeout (default = 5) 
1407      @return: three-tuple of (CV Result (CV_Pass, CV_Fail or CV_None), list of 
1408      result dictionaries, result reason) 
1409      """ 
1410      a = ARC(message,logger=logger,minkey=minkey,timeout=5) 
1411      try: 
1412          return a.verify(dnsfunc=dnsfunc) 
1413      except DKIMException as x: 
1414          if logger is not None: 
1415              logger.error("%s" % x) 
1416          return CV_Fail, [], "%s" % x 
 1417