Package dkim
[hide private]
[frames] | no frames]

Source Code for Package dkim

   1  # This software is provided 'as-is', without any express or implied 
   2  # warranty.  In no event will the author be held liable for any damages 
   3  # arising from the use of this software. 
   4  # 
   5  # Permission is granted to anyone to use this software for any purpose, 
   6  # including commercial applications, and to alter it and redistribute it 
   7  # freely, subject to the following restrictions: 
   8  # 
   9  # 1. The origin of this software must not be misrepresented; you must not 
  10  #    claim that you wrote the original software. If you use this software 
  11  #    in a product, an acknowledgment in the product documentation would be 
  12  #    appreciated but is not required. 
  13  # 2. Altered source versions must be plainly marked as such, and must not be 
  14  #    misrepresented as being the original software. 
  15  # 3. This notice may not be removed or altered from any source distribution. 
  16  # 
  17  # Copyright (c) 2008 Greg Hewgill http://hewgill.com 
  18  # 
  19  # This has been modified from the original software. 
  20  # Copyright (c) 2011 William Grant <me@williamgrant.id.au> 
  21  # 
  22  # This has been modified from the original software. 
  23  # Copyright (c) 2016 Google, Inc. 
  24  # Contact: Brandon Long <blong@google.com> 
  25  # 
  26  # This has been modified from the original software. 
  27  # Copyright (c) 2016, 2017, 2018, 2019 Scott Kitterman <scott@kitterman.com> 
  28  # 
  29  # This has been modified from the original software. 
  30  # Copyright (c) 2017 Valimail Inc 
  31  # Contact: Gene Shuman <gene@valimail.com> 
  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  # only needed for arc 
  44  try: 
  45      from authres import AuthenticationResultsHeader 
  46  except ImportError: 
  47      pass 
  48   
  49  # only needed for ed25519 signing/verification 
  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          # Only true if not using async 
80 - def get_txt(s,timeout=5):
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' # for clients passing dkim.Relaxed 113 Simple = b'simple' # for clients passing dkim.Simple 114 115 # for ARC 116 CV_Pass = b'pass' 117 CV_Fail = b'fail' 118 CV_None = b'none' 119 120
121 -class HashThrough(object):
122 - def __init__(self, hasher, debug=False):
123 self.data = [] 124 self.hasher = hasher 125 self.name = hasher.name 126 self.debug = debug
127
128 - def update(self, data):
129 if self.debug: 130 self.data.append(data) 131 return self.hasher.update(data)
132
133 - def digest(self):
134 return self.hasher.digest()
135
136 - def hexdigest(self):
137 return self.hasher.hexdigest()
138
139 - def hashed(self):
140 return b''.join(self.data)
141 142
143 -def bitsize(x):
144 """Return size of long in bits.""" 145 return len(bin(x)) - 2
146 147
148 -class DKIMException(Exception):
149 """Base class for DKIM errors.""" 150 pass
151 152
153 -class InternalError(DKIMException):
154 """Internal error in dkim module. Should never happen.""" 155 pass
156 157
158 -class KeyFormatError(DKIMException):
159 """Key format error while parsing an RSA public or private key.""" 160 pass
161 162
163 -class MessageFormatError(DKIMException):
164 """RFC822 message format error.""" 165 pass
166 167
168 -class ParameterError(DKIMException):
169 """Input parameter error.""" 170 pass
171 172
173 -class ValidationError(DKIMException):
174 """Validation error.""" 175 pass
176 177
178 -class AuthresNotFoundError(DKIMException):
179 """ Authres Package not installed, needed for ARC """ 180 pass
181 182
183 -class NaClNotFoundError(DKIMException):
184 """ Nacl package not installed, needed for ed25119 signatures """ 185 pass
186
187 -class UnknownKeyTypeError(DKIMException):
188 """ Key type (k tag) is not known (rsa/ed25519) """
189 190
191 -def select_headers(headers, include_headers):
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 # FWS = ([*WSP CRLF] 1*WSP) / obs-FWS ; Folding white space [RFC5322] 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
222 -def hash_headers(hasher, canonicalize_headers, headers, include_headers, 223 sigheader, sig):
224 """Update hash for signed message header fields.""" 225 sign_headers = select_headers(headers,include_headers) 226 # The call to _remove() assumes that the signature b= only appears 227 # once in the signature header 228 cheaders = canonicalize_headers.canonicalize_headers( 229 [(sigheader[0], RE_BTAG.sub(b'\\1',sigheader[1]))]) 230 # the dkim sig is hashed with no trailing crlf, even if the 231 # canonicalization algorithm would add one. 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
239 -def hash_headers_ed25519(pk, canonicalize_headers, headers, include_headers, 240 sigheader, sig):
241 """Update hash for signed message header fields.""" 242 hash_header = '' 243 sign_headers = select_headers(headers,include_headers) 244 # The call to _remove() assumes that the signature b= only appears 245 # once in the signature header 246 cheaders = canonicalize_headers.canonicalize_headers( 247 [(sigheader[0], RE_BTAG.sub(b'\\1',sigheader[1]))]) 248 # the dkim sig is hashed with no trailing crlf, even if the 249 # canonicalization algorithm would add one. 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
255 -def validate_signature_fields(sig, mandatory_fields=[b'v', b'a', b'b', b'bh', b'd', b'h', b's'], arc=False):
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 # Nasty hack to support both str and bytes... check for both the 290 # character and integer values. 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 # 10H leeway for mailers with inaccurate clocks 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 # 10H leeway for mailers with inaccurate clocks 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
332 -def rfc822_parse(message):
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 # End of headers, return what we have plus the body, excluding the blank line. 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
361 -def text(s):
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 # 72 is the max line length we actually want, but the header field name 396 # has to fit in the first line too (See Debian Bug #863690). 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
428 -def evaluate_pk(name, s):
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 # Version not required in key record: RFC 6376 3.6.1 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 # Ignore unknown service types, RFC 6376 3.6.1 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 # Default is '*' - all service types, so no error if missing from key record 476 pass 477 return pk, keysize, ktag, seqtlsrpt
478 479
480 -def load_pk_from_dns(name, dnsfunc=get_txt, timeout=5):
481 s = dnsfunc(name, timeout=timeout) 482 pk, keysize, ktag, seqtlsrpt = evaluate_pk(name, s) 483 return pk, keysize, ktag, seqtlsrpt
484 485 486 #: Abstract base class for holding messages and options during DKIM/ARC signing and verification.
487 -class DomainSigner(object):
488 # NOTE - the first 2 indentation levels are 2 instead of 4 489 # to minimize changed lines from the function only version. 490 491 #: @param message: an RFC822 formatted message to be signed or verified 492 #: (with either \\n or \\r\\n line endings) 493 #: @param logger: a logger to which debug info will be written (default None) 494 #: @param signature_algorithm: the signing algorithm to use when signing 495 #: @param debug_content: log headers and body after canonicalization (default False) 496 #: @param linesep: use this line seperator for folding the headers 497 #: @param timeout: number of seconds for DNS lookup timeout (default = 5) 498 #: @param tlsrpt: message is an RFC 8460 TLS report (default False) 499 #: False: Not a tlsrpt, True: Is a tlsrpt, 'strict': tlsrpt, invalid if 500 #: service type is missing. For signing, if True, length is never used.
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 #: Header fields which should be signed. Default as suggested by RFC6376 514 self.should_sign = set(DKIM.SHOULD) 515 #: Header fields which should not be signed. The default is from RFC6376. 516 #: Attempting to sign these headers results in an exception. 517 #: If it is necessary to sign one of these, it must be removed 518 #: from this list first. 519 self.should_not_sign = set(DKIM.SHOULD_NOT) 520 #: Header fields to sign an extra time to prevent additions. 521 self.frozen_sign = set(DKIM.FROZEN) 522 #: Minimum public key size. Shorter keys raise KeyFormatError. The 523 #: default is 1024 524 self.minkey = minkey 525 # use this line seperator for output 526 self.linesep = linesep 527 self.timeout = timeout 528 self.tlsrpt = tlsrpt 529 # Service type in DKIM record is s=tlsrpt 530 self.seqtlsrpt = False
531 532 533 #: Header fields to protect from additions by default. 534 #: 535 #: The short list below is the result more of instinct than logic. 536 #: @since: 0.5 537 FROZEN = (b'from',) 538 539 #: The rfc6376 recommended header fields to sign 540 #: @since: 0.5 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 #: The rfc6376 recommended header fields not to sign. 551 #: @since: 0.5 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 # Doesn't seem to be used (GS) 558 #: The U{RFC5322<http://tools.ietf.org/html/rfc5322#section-3.6>} 559 #: complete list of singleton headers (which should 560 #: appear at most once). This can be used for a "paranoid" or 561 #: "strict" signing mode. 562 #: Bcc in this list is in the SHOULD NOT sign list, the rest could 563 #: be in the default FROZEN list, but that could also make signatures 564 #: more fragile than necessary. 565 #: @since: 0.5 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 #: Load a new message to be signed or verified. 602 #: @param message: an RFC822 formatted message to be signed or verified 603 #: (with either \\n or \\r\\n line endings) 604 #: @since: 0.5
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 #: The DKIM signing domain last signed or verified. 611 self.domain = None 612 #: The DKIM key selector last signed or verified. 613 self.selector = 'default' 614 #: Signature parameters of last sign or verify. To parse 615 #: a DKIM-Signature header field that you have in hand, 616 #: use L{dkim.util.parse_tag_value}. 617 self.signature_fields = {} 618 #: The list of headers last signed or verified. Each header 619 #: is a name,value tuple. FIXME: The headers are canonicalized. 620 #: This could be more useful as original headers. 621 self.signed_headers = [] 622 #: The public key size last verified. 623 self.keysize = 0
624
625 - def default_sign_headers(self):
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
636 - def all_sign_headers(self):
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 # Abstract helper method to generate a tag=value header from a list of fields 643 #: @param fields: A list of key value tuples to be included in the header 644 #: @param include_headers: A list message headers to include in the b= signature computation 645 #: @param canon_policy: A canonicialization policy for b= & bh= 646 #: @param header_name: The name of the generated header 647 #: @param pk: The private key used for signature generation 648 #: @param standardize: Flag to enable 'standard' header syntax
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 # Folding b= is explicity allowed, but yahoo and live.com are broken 679 #header_value += base64.b64encode(bytes(sig2)) 680 # Instead of leaving unfolded (which lets an MTA fold it later and still 681 # breaks yahoo and live.com), we change the default signing mode to 682 # relaxed/simple (for broken receivers), and fold now. 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 # RFC 8460 MAY ignore signatures without tlsrpt Service Type 696 if self.tlsrpt == 'strict' and not self.seqtlsrpt: 697 raise ValidationError("Message is tlsrpt and Service Type is not tlsrpt") 698 # Inferred requirement from both RFC 8460 and RFC 6376 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 # validate body if present 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 # address bug#644046 by including any additional From header 732 # fields when verifying. Since there should be only one From header, 733 # this shouldn't break any legitimate messages. This could be 734 # generalized to check for extras of other singleton headers. 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 # Abstract helper method to verify a signed header 766 #: @param sig: List of (key, value) tuples containing tag=values of the header 767 #: @param include_headers: headers to validate b= signature against 768 #: @param sig_header: (header_name, header_value) 769 #: @param dnsfunc: interface to dns
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 #: Hold messages and options during DKIM signing and verification.
785 -class DKIM(DomainSigner):
786 #: Sign an RFC822 message and return the DKIM-Signature header line. 787 #: 788 #: The include_headers option gives full control over which header fields 789 #: are signed. Note that signing a header field that doesn't exist prevents 790 #: that field from being added without breaking the signature. Repeated 791 #: fields (such as Received) can be signed multiple times. Instances 792 #: of the field are signed from bottom to top. Signing a header field more 793 #: times than are currently present prevents additional instances 794 #: from being added without breaking the signature. 795 #: 796 #: The length option allows the message body to be appended to by MTAs 797 #: enroute (e.g. mailing lists that append unsubscribe information) 798 #: without breaking the signature. 799 #: 800 #: The default include_headers for this method differs from the backward 801 #: compatible sign function, which signs all headers not 802 #: in should_not_sign. The default list for this method can be modified 803 #: by tweaking should_sign and frozen_sign (or even should_not_sign). 804 #: It is only necessary to pass an include_headers list when precise control 805 #: is needed. 806 #: 807 #: @param selector: the DKIM selector value for the signature 808 #: @param domain: the DKIM domain value for the signature 809 #: @param privkey: a PKCS#1 private key in base64-encoded text form 810 #: @param identity: the DKIM identity value for the signature 811 #: (default "@"+domain) 812 #: @param canonicalize: the canonicalization algorithms to use 813 #: (default (Simple, Simple)) 814 #: @param include_headers: a list of strings indicating which headers 815 #: are to be signed (default rfc4871 recommended headers) 816 #: @param length: true if the l= tag should be included to indicate 817 #: body length signed (default False). 818 #: @return: DKIM-Signature header field terminated by '\r\n' 819 #: @raise DKIMException: when the message, include_headers, or key are badly 820 #: formed.
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 # TypeError means it's already bytes and we're good or we're in 847 # Python 2 and we don't care. See LP: #1776775. 848 pass 849 850 include_headers = tuple([x.lower() for x in include_headers]) 851 # record what verify should extract 852 self.include_headers = include_headers 853 854 if self.tlsrpt: 855 # RFC 8460 MUST NOT 856 length = False 857 858 # rfc4871 says FROM is required 859 if b'from' not in include_headers: 860 raise ParameterError("The From header field MUST be signed") 861 862 # raise exception for any SHOULD_NOT headers, call can modify 863 # SHOULD_NOT if really needed. 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 # Force b= to fold onto it's own line so that refolding after 887 # adding sig doesn't change whitespace for previous tags. 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 #: Checks if any DKIM signature is present 900 #: @return: True if there is one or more DKIM signatures present or False otherwise
901 - def present(self):
902 return (len([(x,y) for x,y in self.headers if x.lower() == b"dkim-signature"]) > 0)
903
904 - def verify_headerprep(self, idx=0):
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 # By default, we validate the first DKIM-Signature line found. 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 #: Verify a DKIM signature. 929 #: @type idx: int 930 #: @param idx: which signature to verify. The first (topmost) signature is 0. 931 #: @type dnsfunc: callable 932 #: @param dnsfunc: an option function to lookup TXT resource records 933 #: for a DNS domain. The default uses dnspython or pydns. 934 #: @return: True if signature verifies or False otherwise 935 #: @raise DKIMException: when the message, signature, or key are badly formed
936 - def verify(self,idx=0,dnsfunc=get_txt):
937 sig, include_headers, sigheaders = self.verify_headerprep(idx=0) 938 return self.verify_sig(sig, include_headers, sigheaders[idx], dnsfunc)
939 940 941 #: Hold messages and options during ARC signing and verification.
942 -class ARC(DomainSigner):
943 #: Header fields used by ARC 944 ARC_HEADERS = (b'arc-seal', b'arc-message-signature', b'arc-authentication-results') 945 946 #: Regex to extract i= value from ARC headers 947 INSTANCE_RE = re.compile(br'[\s;]?i\s*=\s*(\d+)', re.MULTILINE | re.IGNORECASE) 948
949 - def sorted_arc_headers(self):
950 headers = [] 951 # Use relaxed canonicalization to unfold and clean up headers 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 #: Sign an RFC822 message and return the list of ARC set header lines 976 #: 977 #: The include_headers option gives full control over which header fields 978 #: are signed for the ARC-Message-Signature. Note that signing a header 979 #: field that doesn't exist prevents 980 #: that field from being added without breaking the signature. Repeated 981 #: fields (such as Received) can be signed multiple times. Instances 982 #: of the field are signed from bottom to top. Signing a header field more 983 #: times than are currently present prevents additional instances 984 #: from being added without breaking the signature. 985 #: 986 #: The default include_headers for this method differs from the backward 987 #: compatible sign function, which signs all headers not 988 #: in should_not_sign. The default list for this method can be modified 989 #: by tweaking should_sign and frozen_sign (or even should_not_sign). 990 #: It is only necessary to pass an include_headers list when precise control 991 #: is needed. 992 #: 993 #: @param selector: the DKIM selector value for the signature 994 #: @param domain: the DKIM domain value for the signature 995 #: @param privkey: a PKCS#1 private key in base64-encoded text form 996 #: @param srv_id: an srv_id for identitfying AR headers to sign & extract cv from 997 #: @param include_headers: a list of strings indicating which headers 998 #: are to be signed (default rfc4871 recommended headers) 999 #: @return: list of ARC set header fields 1000 #: @raise DKIMException: when the message, include_headers, or key are badly 1001 #: formed.
1002 - def sign(self, selector, domain, privkey, srv_id, include_headers=None, 1003 timestamp=None, standardize=False):
1004 1005 INSTANCE_LIMIT = 50 # Maximum allowed i= value 1006 self.add_should_not(('Authentication-Results',)) 1007 # check if authres has been imported 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 # extract, parse, filter & group AR headers 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 # consolidate headers 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 # extract cv 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 # Setup headers 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 # record what verify should extract 1053 self.include_headers = include_headers 1054 1055 # rfc4871 says FROM is required 1056 if b'from' not in include_headers: 1057 raise ParameterError("The From header field MUST be signed") 1058 1059 # raise exception for any SHOULD_NOT headers, call can modify 1060 # SHOULD_NOT if really needed. 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: # don't include previous sets for a failed/invalid chain 1081 arc_headers = [] 1082 1083 # Compute ARC-Authentication-Results 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 # Compute bh= 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 # Compute ARC-Message-Signature 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 # Force b= to fold onto it's own line so that refolding after 1113 # adding sig doesn't change whitespace for previous tags. 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 # Compute ARC-Seal 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 # Force b= to fold onto it's own line so that refolding after 1133 # adding sig doesn't change whitespace for previous tags. 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 # if our chain is failing or invalid, we only grab the most recent set 1141 # reversing the order of the headers accomplishes this 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 #: Verify an ARC set. 1158 #: @type instance: int 1159 #: @param instance: which ARC set to verify, based on i= instance. 1160 #: @type dnsfunc: callable 1161 #: @param dnsfunc: an optional function to lookup TXT resource records 1162 #: for a DNS domain. The default uses dnspython or pydns. 1163 #: @return: True if signature verifies or False otherwise 1164 #: @return: three-tuple of (CV Result (CV_Pass, CV_Fail, CV_None or None, for a chain that has ended), list of 1165 #: result dictionaries, result reason) 1166 #: @raise DKIMException: when the message, signature, or key are badly formed
1167 - def verify(self,dnsfunc=get_txt):
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 # Most recent instance must ams-validate 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 #: Verify an ARC set. 1195 #: @type arc_headers_w_instance: list 1196 #: @param arc_headers_w_instance: list of tuples, (instance, (name, value)) of 1197 #: ARC headers 1198 #: @type instance: int 1199 #: @param instance: which ARC set to verify, based on i= instance. 1200 #: @type dnsfunc: callable 1201 #: @param dnsfunc: an optional function to lookup TXT resource records 1202 #: for a DNS domain. The default uses dnspython or pydns. 1203 #: @return: True if signature verifies or False otherwise 1204 #: @raise DKIMException: when the message, signature, or key are badly formed
1205 - def verify_instance(self,arc_headers_w_instance,instance,dnsfunc=get_txt):
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 # Validate Arc-Message-Signature 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 # we can't use the AMS provided above, as it's already been canonicalized relaxed 1257 # for use in validating the AS. However the AMS is included in the AMS itself, 1258 # and this can use simple canonicalization 1259 raw_ams_header = [(x, y) for (x, y) in self.headers if x.lower() == b'arc-message-signature'][0] 1260 1261 # Only relaxed canonicalization used by ARC 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 # Validate Arc-Seal 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 # Only relaxed canonicalization used by ARC 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 # type: (bytes, bytes, bytes, bytes, bytes, tuple, bytes, list, bool, any) -> bytes 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 # type: (bytes, any, function, int) -> bool 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 # aiodns requires Python 3.5+, so no async before that 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 # If aiodns is not installed, then async verification is not available 1364 pass 1365 1366 1367 # For consistency with ARC 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 # type: (bytes, bytes, bytes, bytes, bytes, bytes, list, any, any, bool) -> list 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
1399 -def arc_verify(message, logger=None, dnsfunc=get_txt, minkey=1024, timeout=5):
1400 # type: (bytes, any, function, int) -> tuple 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