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 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 time 
  40   
  41  # only needed for arc 
  42  try: 
  43    from authres import AuthenticationResultsHeader 
  44  except: 
  45    pass 
  46   
  47  # only needed for ed25519 signing/verification 
  48  try: 
  49      import nacl.signing 
  50      import nacl.encoding 
  51  except: 
  52      pass 
  53   
  54  from dkim.canonicalization import ( 
  55      CanonicalizationPolicy, 
  56      InvalidCanonicalizationPolicyError, 
  57      ) 
  58  from dkim.canonicalization import Relaxed as RelaxedCanonicalization 
  59   
  60  from dkim.crypto import ( 
  61      DigestTooLargeError, 
  62      HASH_ALGORITHMS, 
  63      parse_pem_private_key, 
  64      parse_public_key, 
  65      RSASSA_PKCS1_v1_5_sign, 
  66      RSASSA_PKCS1_v1_5_verify, 
  67      UnparsableKeyError, 
  68      ) 
  69  try: 
  70    from dkim.dnsplug import get_txt 
  71  except: 
72 - def get_txt(s):
73 raise RuntimeError("DKIM.verify requires DNS or dnspython module")
74 from dkim.util import ( 75 get_default_logger, 76 InvalidTagValueList, 77 parse_tag_value, 78 ) 79 80 __all__ = [ 81 "DKIMException", 82 "InternalError", 83 "KeyFormatError", 84 "MessageFormatError", 85 "ParameterError", 86 "ValidationError", 87 "AuthresNotFoundError", 88 "NaClNotFoundError", 89 "CV_Pass", 90 "CV_Fail", 91 "CV_None", 92 "Relaxed", 93 "Simple", 94 "DKIM", 95 "ARC", 96 "sign", 97 "verify", 98 "dkim_sign", 99 "dkim_verify", 100 "arc_sign", 101 "arc_verify", 102 ] 103 104 Relaxed = b'relaxed' # for clients passing dkim.Relaxed 105 Simple = b'simple' # for clients passing dkim.Simple 106 107 # for ARC 108 CV_Pass = b'pass' 109 CV_Fail = b'fail' 110 CV_None = b'none' 111
112 -class HashThrough(object):
113 - def __init__(self, hasher):
114 self.data = [] 115 self.hasher = hasher 116 self.name = hasher.name
117
118 - def update(self, data):
119 self.data.append(data) 120 return self.hasher.update(data)
121
122 - def digest(self):
123 return self.hasher.digest()
124
125 - def hexdigest(self):
126 return self.hasher.hexdigest()
127
128 - def hashed(self):
129 return b''.join(self.data)
130
131 -def bitsize(x):
132 """Return size of long in bits.""" 133 return len(bin(x)) - 2
134
135 -class DKIMException(Exception):
136 """Base class for DKIM errors.""" 137 pass
138
139 -class InternalError(DKIMException):
140 """Internal error in dkim module. Should never happen.""" 141 pass
142
143 -class KeyFormatError(DKIMException):
144 """Key format error while parsing an RSA public or private key.""" 145 pass
146
147 -class MessageFormatError(DKIMException):
148 """RFC822 message format error.""" 149 pass
150
151 -class ParameterError(DKIMException):
152 """Input parameter error.""" 153 pass
154
155 -class ValidationError(DKIMException):
156 "Validation error." 157 pass
158
159 -class DNSError(DKIMException):
160 "DNS error." 161 pass
162
163 -class AuthresNotFoundError(DKIMException):
164 """ Authres Package not installed, needed for ARC """ 165 pass
166
167 -class NaClNotFoundError(DKIMException):
168 """ Nacl package not installed, needed for ed25119 signatures """ 169 pass
170
171 -class UnknownKeyTypeError(DKIMException):
172 """ Key type (k tag) is not known (rsa/ed25519) """
173
174 -def select_headers(headers, include_headers):
175 """Select message header fields to be signed/verified. 176 177 >>> h = [('from','biz'),('foo','bar'),('from','baz'),('subject','boring')] 178 >>> i = ['from','subject','to','from'] 179 >>> select_headers(h,i) 180 [('from', 'baz'), ('subject', 'boring'), ('from', 'biz')] 181 >>> h = [('From','biz'),('Foo','bar'),('Subject','Boring')] 182 >>> i = ['from','subject','to','from'] 183 >>> select_headers(h,i) 184 [('From', 'biz'), ('Subject', 'Boring')] 185 """ 186 sign_headers = [] 187 lastindex = {} 188 for h in include_headers: 189 assert h == h.lower() 190 i = lastindex.get(h, len(headers)) 191 while i > 0: 192 i -= 1 193 if h == headers[i][0].lower(): 194 sign_headers.append(headers[i]) 195 break 196 lastindex[h] = i 197 return sign_headers
198 199 # FWS = ([*WSP CRLF] 1*WSP) / obs-FWS ; Folding white space [RFC5322] 200 FWS = br'(?:(?:\s*\r?\n)?\s+)?' 201 RE_BTAG = re.compile(br'([;\s]b'+FWS+br'=)(?:'+FWS+br'[a-zA-Z0-9+/=])*(?:\r?\n\Z)?') 202
203 -def hash_headers(hasher, canonicalize_headers, headers, include_headers, 204 sigheader, sig):
205 """Update hash for signed message header fields.""" 206 sign_headers = select_headers(headers,include_headers) 207 # The call to _remove() assumes that the signature b= only appears 208 # once in the signature header 209 cheaders = canonicalize_headers.canonicalize_headers( 210 [(sigheader[0], RE_BTAG.sub(b'\\1',sigheader[1]))]) 211 # the dkim sig is hashed with no trailing crlf, even if the 212 # canonicalization algorithm would add one. 213 for x,y in sign_headers + [(x, y.rstrip()) for x,y in cheaders]: 214 hasher.update(x) 215 hasher.update(b":") 216 hasher.update(y) 217 return sign_headers
218
219 -def hash_headers_ed25519(pk, canonicalize_headers, headers, include_headers, 220 sigheader, sig):
221 """Update hash for signed message header fields.""" 222 hash_header = '' 223 sign_headers = select_headers(headers,include_headers) 224 # The call to _remove() assumes that the signature b= only appears 225 # once in the signature header 226 cheaders = canonicalize_headers.canonicalize_headers( 227 [(sigheader[0], RE_BTAG.sub(b'\\1',sigheader[1]))]) 228 # the dkim sig is hashed with no trailing crlf, even if the 229 # canonicalization algorithm would add one. 230 for x,y in sign_headers + [(x, y.rstrip()) for x,y in cheaders]: 231 hash_header += x + y 232 return sign_headers, hash_header
233
234 -def validate_signature_fields(sig, mandatory_fields=[b'v', b'a', b'b', b'bh', b'd', b'h', b's'], arc=False):
235 """Validate DKIM or ARC Signature fields. 236 Basic checks for presence and correct formatting of mandatory fields. 237 Raises a ValidationError if checks fail, otherwise returns None. 238 @param sig: A dict mapping field keys to values. 239 @param mandatory_fields: A list of non-optional fields 240 @param arc: flag to differentiate between dkim & arc 241 """ 242 for field in mandatory_fields: 243 if field not in sig: 244 raise ValidationError("missing %s=" % field) 245 246 if b'a' in sig and not sig[b'a'] in HASH_ALGORITHMS: 247 raise ValidationError("unknown signature algorithm: %s" % sig[b'a']) 248 249 if b'b' in sig: 250 if re.match(br"[\s0-9A-Za-z+/]+=*$", sig[b'b']) is None: 251 raise ValidationError("b= value is not valid base64 (%s)" % sig[b'b']) 252 if len(re.sub(br"\s+", b"", sig[b'b'])) % 4 != 0: 253 raise ValidationError("b= value is not valid base64 (%s)" % sig[b'b']) 254 255 if b'bh' in sig: 256 if re.match(br"[\s0-9A-Za-z+/]+=*$", sig[b'bh']) is None: 257 raise ValidationError("bh= value is not valid base64 (%s)" % sig[b'bh']) 258 if len(re.sub(br"\s+", b"", sig[b'bh'])) % 4 != 0: 259 raise ValidationError("bh= value is not valid base64 (%s)" % sig[b'bh']) 260 261 if b'cv' in sig and sig[b'cv'] not in (CV_Pass, CV_Fail, CV_None): 262 raise ValidationError("cv= value is not valid (%s)" % sig[b'cv']) 263 264 # Nasty hack to support both str and bytes... check for both the 265 # character and integer values. 266 if not arc and b'i' in sig and ( 267 not sig[b'i'].lower().endswith(sig[b'd'].lower()) or 268 sig[b'i'][-len(sig[b'd'])-1] not in ('@', '.', 64, 46)): 269 raise ValidationError( 270 "i= domain is not a subdomain of d= (i=%s d=%s)" % 271 (sig[b'i'], sig[b'd'])) 272 if b'l' in sig and re.match(br"\d{,76}$", sig[b'l']) is None: 273 raise ValidationError( 274 "l= value is not a decimal integer (%s)" % sig[b'l']) 275 if b'q' in sig and sig[b'q'] != b"dns/txt": 276 raise ValidationError("q= value is not dns/txt (%s)" % sig[b'q']) 277 278 if b't' in sig: 279 if re.match(br"\d+$", sig[b't']) is None: 280 raise ValidationError( 281 "t= value is not a decimal integer (%s)" % sig[b't']) 282 now = int(time.time()) 283 slop = 36000 # 10H leeway for mailers with inaccurate clocks 284 t_sign = int(sig[b't']) 285 if t_sign > now + slop: 286 raise ValidationError("t= value is in the future (%s)" % sig[b't']) 287 288 if b'v' in sig and sig[b'v'] != b"1": 289 raise ValidationError("v= value is not 1 (%s)" % sig[b'v']) 290 291 if b'x' in sig: 292 if re.match(br"\d+$", sig[b'x']) is None: 293 raise ValidationError( 294 "x= value is not a decimal integer (%s)" % sig[b'x']) 295 x_sign = int(sig[b'x']) 296 now = int(time.time()) 297 slop = 36000 # 10H leeway for mailers with inaccurate clocks 298 if x_sign < now - slop: 299 raise ValidationError( 300 "x= value is past (%s)" % sig[b'x']) 301 if x_sign < t_sign: 302 raise ValidationError( 303 "x= value is less than t= value (x=%s t=%s)" % 304 (sig[b'x'], sig[b't']))
305
306 -def rfc822_parse(message):
307 """Parse a message in RFC822 format. 308 309 @param message: The message in RFC822 format. Either CRLF or LF is an accepted line separator. 310 @return: Returns a tuple of (headers, body) where headers is a list of (name, value) pairs. 311 The body is a CRLF-separated string. 312 """ 313 headers = [] 314 lines = re.split(b"\r?\n", message) 315 i = 0 316 while i < len(lines): 317 if len(lines[i]) == 0: 318 # End of headers, return what we have plus the body, excluding the blank line. 319 i += 1 320 break 321 if lines[i][0] in ("\x09", "\x20", 0x09, 0x20): 322 headers[-1][1] += lines[i]+b"\r\n" 323 else: 324 m = re.match(br"([\x21-\x7e]+?):", lines[i]) 325 if m is not None: 326 headers.append([m.group(1), lines[i][m.end(0):]+b"\r\n"]) 327 elif lines[i].startswith(b"From "): 328 pass 329 else: 330 raise MessageFormatError("Unexpected characters in RFC822 header: %s" % lines[i]) 331 i += 1 332 return (headers, b"\r\n".join(lines[i:]))
333
334 -def text(s):
335 """Normalize bytes/str to str for python 2/3 compatible doctests. 336 >>> text(b'foo') 337 'foo' 338 >>> text(u'foo') 339 'foo' 340 >>> text('foo') 341 'foo' 342 """ 343 if type(s) is str: return s 344 s = s.decode('ascii') 345 if type(s) is str: return s 346 return s.encode('ascii')
347
348 -def fold(header, namelen=0):
349 """Fold a header line into multiple crlf-separated lines at column 72. 350 351 >>> text(fold(b'foo')) 352 'foo' 353 >>> text(fold(b'foo '+b'foo'*24).splitlines()[0]) 354 'foo ' 355 >>> text(fold(b'foo'*25).splitlines()[-1]) 356 ' foo' 357 >>> len(fold(b'foo'*25).splitlines()[0]) 358 72 359 """ 360 i = header.rfind(b"\r\n ") 361 if i == -1: 362 pre = b"" 363 else: 364 i += 3 365 pre = header[:i] 366 header = header[i:] 367 # 72 is the max line length we actually want, but the header field name 368 # has to fit in the first line too (See Debian Bug #863690). 369 maxleng = 72 - namelen 370 while len(header) > maxleng: 371 i = header[:maxleng].rfind(b" ") 372 if i == -1: 373 j = maxleng 374 else: 375 j = i + 1 376 pre += header[:j] + b"\r\n " 377 header = header[j:] 378 namelen = 0 379 return pre + header
380
381 -def load_pk_from_dns(name, dnsfunc=get_txt):
382 s = dnsfunc(name) 383 if not s: 384 raise KeyFormatError("missing public key: %s"%name) 385 try: 386 if type(s) is str: 387 s = s.encode('ascii') 388 pub = parse_tag_value(s) 389 except InvalidTagValueList as e: 390 raise KeyFormatError(e) 391 try: 392 if pub[b'k'] == b'ed25519': 393 pk = nacl.signing.VerifyKey(pub[b'p'], encoder=nacl.encoding.Base64Encoder) 394 keysize = 256 395 ktag = b'ed25519' 396 except KeyError: 397 pub[b'k'] = b'rsa' 398 if pub[b'k'] == b'rsa': 399 try: 400 pk = parse_public_key(base64.b64decode(pub[b'p'])) 401 keysize = bitsize(pk['modulus']) 402 except KeyError: 403 raise KeyFormatError("incomplete public key: %s" % s) 404 except (TypeError,UnparsableKeyError) as e: 405 raise KeyFormatError("could not parse public key (%s): %s" % (pub[b'p'],e)) 406 ktag = b'rsa' 407 return pk, keysize, ktag
408 409 #: Abstract base class for holding messages and options during DKIM/ARC signing and verification.
410 -class DomainSigner(object):
411 # NOTE - the first 2 indentation levels are 2 instead of 4 412 # to minimize changed lines from the function only version. 413 414 #: @param message: an RFC822 formatted message to be signed or verified 415 #: (with either \\n or \\r\\n line endings) 416 #: @param logger: a logger to which debug info will be written (default None) 417 #: @param signature_algorithm: the signing algorithm to use when signing
418 - def __init__(self,message=None,logger=None,signature_algorithm=b'rsa-sha256', 419 minkey=1024):
420 self.set_message(message) 421 if logger is None: 422 logger = get_default_logger() 423 self.logger = logger 424 if signature_algorithm not in HASH_ALGORITHMS: 425 raise ParameterError( 426 "Unsupported signature algorithm: "+signature_algorithm) 427 self.signature_algorithm = signature_algorithm 428 #: Header fields which should be signed. Default as suggested by RFC6376 429 self.should_sign = set(DKIM.SHOULD) 430 #: Header fields which should not be signed. The default is from RFC6376. 431 #: Attempting to sign these headers results in an exception. 432 #: If it is necessary to sign one of these, it must be removed 433 #: from this list first. 434 self.should_not_sign = set(DKIM.SHOULD_NOT) 435 #: Header fields to sign an extra time to prevent additions. 436 self.frozen_sign = set(DKIM.FROZEN) 437 #: Minimum public key size. Shorter keys raise KeyFormatError. The 438 #: default is 1024 439 self.minkey = minkey
440 441 #: Header fields to protect from additions by default. 442 #: 443 #: The short list below is the result more of instinct than logic. 444 #: @since: 0.5 445 FROZEN = (b'from',b'date',b'subject') 446 447 #: The rfc6376 recommended header fields to sign 448 #: @since: 0.5 449 SHOULD = ( 450 b'sender', b'reply-to', b'subject', b'date', b'message-id', b'to', b'cc', 451 b'mime-version', b'content-type', b'content-transfer-encoding', 452 b'content-id', b'content-description', b'resent-date', b'resent-from', 453 b'resent-sender', b'resent-to', b'resent-cc', b'resent-message-id', 454 b'in-reply-to', b'references', b'list-id', b'list-help', b'list-unsubscribe', 455 b'list-subscribe', b'list-post', b'list-owner', b'list-archive' 456 ) 457 458 #: The rfc6376 recommended header fields not to sign. 459 #: @since: 0.5 460 SHOULD_NOT = ( 461 b'return-path',b'received',b'comments',b'keywords',b'bcc',b'resent-bcc', 462 b'dkim-signature' 463 ) 464 465 # Doesn't seem to be used (GS) 466 #: The U{RFC5322<http://tools.ietf.org/html/rfc5322#section-3.6>} 467 #: complete list of singleton headers (which should 468 #: appear at most once). This can be used for a "paranoid" or 469 #: "strict" signing mode. 470 #: Bcc in this list is in the SHOULD NOT sign list, the rest could 471 #: be in the default FROZEN list, but that could also make signatures 472 #: more fragile than necessary. 473 #: @since: 0.5 474 RFC5322_SINGLETON = (b'date',b'from',b'sender',b'reply-to',b'to',b'cc',b'bcc', 475 b'message-id',b'in-reply-to',b'references') 476
477 - def add_frozen(self,s):
478 """ Add headers not in should_not_sign to frozen_sign. 479 @param s: list of headers to add to frozen_sign 480 @since: 0.5 481 482 >>> dkim = DKIM() 483 >>> dkim.add_frozen(DKIM.RFC5322_SINGLETON) 484 >>> [text(x) for x in sorted(dkim.frozen_sign)] 485 ['cc', 'date', 'from', 'in-reply-to', 'message-id', 'references', 'reply-to', 'sender', 'subject', 'to'] 486 """ 487 self.frozen_sign.update(x.lower() for x in s 488 if x.lower() not in self.should_not_sign)
489 490 #: Load a new message to be signed or verified. 491 #: @param message: an RFC822 formatted message to be signed or verified 492 #: (with either \\n or \\r\\n line endings) 493 #: @since: 0.5
494 - def set_message(self,message):
495 if message: 496 self.headers, self.body = rfc822_parse(message) 497 else: 498 self.headers, self.body = [],'' 499 #: The DKIM signing domain last signed or verified. 500 self.domain = None 501 #: The DKIM key selector last signed or verified. 502 self.selector = 'default' 503 #: Signature parameters of last sign or verify. To parse 504 #: a DKIM-Signature header field that you have in hand, 505 #: use L{dkim.util.parse_tag_value}. 506 self.signature_fields = {} 507 #: The list of headers last signed or verified. Each header 508 #: is a name,value tuple. FIXME: The headers are canonicalized. 509 #: This could be more useful as original headers. 510 self.signed_headers = [] 511 #: The public key size last verified. 512 self.keysize = 0
513
514 - def default_sign_headers(self):
515 """Return the default list of headers to sign: those in should_sign or 516 frozen_sign, with those in frozen_sign signed an extra time to prevent 517 additions. 518 @since: 0.5""" 519 hset = self.should_sign | self.frozen_sign 520 include_headers = [ x for x,y in self.headers 521 if x.lower() in hset ] 522 return include_headers + [ x for x in include_headers 523 if x.lower() in self.frozen_sign]
524
525 - def all_sign_headers(self):
526 """Return header list of all existing headers not in should_not_sign. 527 @since: 0.5""" 528 return [x for x,y in self.headers if x.lower() not in self.should_not_sign]
529 530 531 # Abstract helper method to generate a tag=value header from a list of fields 532 #: @param fields: A list of key value tuples to be included in the header 533 #: @param include_headers: A list message headers to include in the b= signature computation 534 #: @param canon_policy: A canonicialization policy for b= & bh= 535 #: @param header_name: The name of the generated header 536 #: @param pk: The private key used for signature generation 537 #: @param standardize: Flag to enable 'standard' header syntax
538 - def gen_header(self, fields, include_headers, canon_policy, header_name, pk, standardize=False):
539 if standardize: 540 lower = [(x,y.lower().replace(b' ', b'')) for (x,y) in fields if x != b'bh'] 541 reg = [(x,y.replace(b' ', b'')) for (x,y) in fields if x == b'bh'] 542 fields = lower + reg 543 fields = sorted(fields, key=(lambda x: x[0])) 544 545 header_value = b"; ".join(b"=".join(x) for x in fields) 546 if not standardize: 547 header_value = fold(header_value, namelen=len(header_name)) 548 header_value = RE_BTAG.sub(b'\\1',header_value) 549 header = (header_name, b' ' + header_value) 550 h = HashThrough(self.hasher()) 551 sig = dict(fields) 552 553 headers = canon_policy.canonicalize_headers(self.headers) 554 self.signed_headers = hash_headers( 555 h, canon_policy, headers, include_headers, header, sig) 556 self.logger.debug("sign %s headers: %r" % (header_name, h.hashed())) 557 558 if self.signature_algorithm == b'rsa-sha256' or self.signature_algorithm == b'rsa-sha1': 559 try: 560 sig2 = RSASSA_PKCS1_v1_5_sign(h, pk) 561 except DigestTooLargeError: 562 raise ParameterError("digest too large for modulus") 563 elif self.signature_algorithm == b'ed25519-sha256': 564 sigobj = pk.sign(h.digest()) 565 sig2 = sigobj.signature 566 # Folding b= is explicity allowed, but yahoo and live.com are broken 567 #header_value += base64.b64encode(bytes(sig2)) 568 # Instead of leaving unfolded (which lets an MTA fold it later and still 569 # breaks yahoo and live.com), we change the default signing mode to 570 # relaxed/simple (for broken receivers), and fold now. 571 idx = [i for i in range(len(fields)) if fields[i][0] == b'b'][0] 572 fields[idx] = (b'b', base64.b64encode(bytes(sig2))) 573 header_value = b"; ".join(b"=".join(x) for x in fields) + b"\r\n" 574 575 if not standardize: 576 header_value = fold(header_value, namelen=len(header_name)) 577 578 return header_value
579 580 # Abstract helper method to verify a signed header 581 #: @param sig: List of (key, value) tuples containing tag=values of the header 582 #: @param include_headers: headers to validate b= signature against 583 #: @param sig_header: (header_name, header_value) 584 #: @param dnsfunc: interface to dns
585 - def verify_sig(self, sig, include_headers, sig_header, dnsfunc):
586 name = sig[b's'] + b"._domainkey." + sig[b'd'] + b"." 587 try: 588 pk, self.keysize, ktag = load_pk_from_dns(name, dnsfunc) 589 except KeyFormatError as e: 590 self.logger.error("%s" % e) 591 return False 592 593 try: 594 canon_policy = CanonicalizationPolicy.from_c_value(sig.get(b'c', b'relaxed/relaxed')) 595 except InvalidCanonicalizationPolicyError as e: 596 raise MessageFormatError("invalid c= value: %s" % e.args[0]) 597 598 hasher = HASH_ALGORITHMS[sig[b'a']] 599 600 # validate body if present 601 if b'bh' in sig: 602 h = HashThrough(hasher()) 603 604 body = canon_policy.canonicalize_body(self.body) 605 if b'l' in sig: 606 body = body[:int(sig[b'l'])] 607 h.update(body) 608 self.logger.debug("body hashed: %r" % h.hashed()) 609 bodyhash = h.digest() 610 611 self.logger.debug("bh: %s" % base64.b64encode(bodyhash)) 612 try: 613 bh = base64.b64decode(re.sub(br"\s+", b"", sig[b'bh'])) 614 except TypeError as e: 615 raise MessageFormatError(str(e)) 616 if bodyhash != bh: 617 raise ValidationError( 618 "body hash mismatch (got %s, expected %s)" % 619 (base64.b64encode(bodyhash), sig[b'bh'])) 620 621 # address bug#644046 by including any additional From header 622 # fields when verifying. Since there should be only one From header, 623 # this shouldn't break any legitimate messages. This could be 624 # generalized to check for extras of other singleton headers. 625 if b'from' in include_headers: 626 include_headers.append(b'from') 627 h = HashThrough(hasher()) 628 629 headers = canon_policy.canonicalize_headers(self.headers) 630 self.signed_headers = hash_headers( 631 h, canon_policy, headers, include_headers, sig_header, sig) 632 self.logger.debug("signed for %s: %r" % (sig_header[0], h.hashed())) 633 signature = base64.b64decode(re.sub(br"\s+", b"", sig[b'b'])) 634 if ktag == b'rsa': 635 try: 636 res = RSASSA_PKCS1_v1_5_verify(h, signature, pk) 637 self.logger.debug("%s valid: %s" % (sig_header[0], res)) 638 if res and self.keysize < self.minkey: 639 raise KeyFormatError("public key too small: %d" % self.keysize) 640 return res 641 except (TypeError,DigestTooLargeError) as e: 642 raise KeyFormatError("digest too large for modulus: %s"%e) 643 elif ktag == b'ed25519': 644 try: 645 pk.verify(h.digest(), signature) 646 self.logger.debug("%s valid" % (sig_header[0])) 647 return True 648 except (nacl.exceptions.BadSignatureError) as e: 649 return False 650 else: 651 raise UnknownKeyTypeError(ktag)
652 653 #: Hold messages and options during DKIM signing and verification.
654 -class DKIM(DomainSigner):
655 #: Sign an RFC822 message and return the DKIM-Signature header line. 656 #: 657 #: The include_headers option gives full control over which header fields 658 #: are signed. Note that signing a header field that doesn't exist prevents 659 #: that field from being added without breaking the signature. Repeated 660 #: fields (such as Received) can be signed multiple times. Instances 661 #: of the field are signed from bottom to top. Signing a header field more 662 #: times than are currently present prevents additional instances 663 #: from being added without breaking the signature. 664 #: 665 #: The length option allows the message body to be appended to by MTAs 666 #: enroute (e.g. mailing lists that append unsubscribe information) 667 #: without breaking the signature. 668 #: 669 #: The default include_headers for this method differs from the backward 670 #: compatible sign function, which signs all headers not 671 #: in should_not_sign. The default list for this method can be modified 672 #: by tweaking should_sign and frozen_sign (or even should_not_sign). 673 #: It is only necessary to pass an include_headers list when precise control 674 #: is needed. 675 #: 676 #: @param selector: the DKIM selector value for the signature 677 #: @param domain: the DKIM domain value for the signature 678 #: @param privkey: a PKCS#1 private key in base64-encoded text form 679 #: @param identity: the DKIM identity value for the signature 680 #: (default "@"+domain) 681 #: @param canonicalize: the canonicalization algorithms to use 682 #: (default (Simple, Simple)) 683 #: @param include_headers: a list of strings indicating which headers 684 #: are to be signed (default rfc4871 recommended headers) 685 #: @param length: true if the l= tag should be included to indicate 686 #: body length signed (default False). 687 #: @return: DKIM-Signature header field terminated by '\r\n' 688 #: @raise DKIMException: when the message, include_headers, or key are badly 689 #: formed.
690 - def sign(self, selector, domain, privkey, signature_algorithm=None, identity=None, 691 canonicalize=(b'relaxed',b'simple'), include_headers=None, length=False):
692 if signature_algorithm: 693 self.signature_algorithm = signature_algorithm 694 if self.signature_algorithm == b'rsa-sha256' or self.signature_algorithm == b'rsa-sha1': 695 try: 696 pk = parse_pem_private_key(privkey) 697 except UnparsableKeyError as e: 698 raise KeyFormatError(str(e)) 699 elif self.signature_algorithm == b'ed25519-sha256': 700 pk = nacl.signing.SigningKey(privkey, encoder=nacl.encoding.Base64Encoder) 701 702 if identity is not None and not identity.endswith(domain): 703 raise ParameterError("identity must end with domain") 704 705 canon_policy = CanonicalizationPolicy.from_c_value(b'/'.join(canonicalize)) 706 707 if include_headers is None: 708 include_headers = self.default_sign_headers() 709 710 include_headers = tuple([x.lower() for x in include_headers]) 711 # record what verify should extract 712 self.include_headers = include_headers 713 714 # rfc4871 says FROM is required 715 if b'from' not in include_headers: 716 raise ParameterError("The From header field MUST be signed") 717 718 # raise exception for any SHOULD_NOT headers, call can modify 719 # SHOULD_NOT if really needed. 720 for x in set(include_headers).intersection(self.should_not_sign): 721 raise ParameterError("The %s header field SHOULD NOT be signed"%x) 722 723 body = canon_policy.canonicalize_body(self.body) 724 725 self.hasher = HASH_ALGORITHMS[self.signature_algorithm] 726 h = self.hasher() 727 h.update(body) 728 bodyhash = base64.b64encode(h.digest()) 729 730 sigfields = [x for x in [ 731 (b'v', b"1"), 732 (b'a', self.signature_algorithm), 733 (b'c', canon_policy.to_c_value()), 734 (b'd', domain), 735 (b'i', identity or b"@"+domain), 736 length and (b'l', str(len(body)).encode('ascii')), 737 (b'q', b"dns/txt"), 738 (b's', selector), 739 (b't', str(int(time.time())).encode('ascii')), 740 (b'h', b" : ".join(include_headers)), 741 (b'bh', bodyhash), 742 # Force b= to fold onto it's own line so that refolding after 743 # adding sig doesn't change whitespace for previous tags. 744 (b'b', b'0'*60), 745 ] if x] 746 747 res = self.gen_header(sigfields, include_headers, canon_policy, 748 b"DKIM-Signature", pk) 749 750 self.domain = domain 751 self.selector = selector 752 self.signature_fields = dict(sigfields) 753 return b'DKIM-Signature: ' + res
754 755 #: Verify a DKIM signature. 756 #: @type idx: int 757 #: @param idx: which signature to verify. The first (topmost) signature is 0. 758 #: @type dnsfunc: callable 759 #: @param dnsfunc: an option function to lookup TXT resource records 760 #: for a DNS domain. The default uses dnspython or pydns. 761 #: @return: True if signature verifies or False otherwise 762 #: @raise DKIMException: when the message, signature, or key are badly formed
763 - def verify(self,idx=0,dnsfunc=get_txt):
764 sigheaders = [(x,y) for x,y in self.headers if x.lower() == b"dkim-signature"] 765 if len(sigheaders) <= idx: 766 return False 767 768 # By default, we validate the first DKIM-Signature line found. 769 try: 770 sig = parse_tag_value(sigheaders[idx][1]) 771 self.signature_fields = sig 772 except InvalidTagValueList as e: 773 raise MessageFormatError(e) 774 775 self.logger.debug("sig: %r" % sig) 776 777 validate_signature_fields(sig) 778 self.domain = sig[b'd'] 779 self.selector = sig[b's'] 780 781 try: 782 canon_policy = CanonicalizationPolicy.from_c_value(sig.get(b'c')) 783 except InvalidCanonicalizationPolicyError as e: 784 raise MessageFormatError("invalid c= value: %s" % e.args[0]) 785 headers = canon_policy.canonicalize_headers(self.headers) 786 body = canon_policy.canonicalize_body(self.body) 787 788 try: 789 hasher = HASH_ALGORITHMS[sig[b'a']] 790 except KeyError as e: 791 raise MessageFormatError("unknown signature algorithm: %s" % e.args[0]) 792 793 if b'l' in sig: 794 body = body[:int(sig[b'l'])] 795 796 h = hasher() 797 h.update(body) 798 bodyhash = h.digest() 799 self.logger.debug("bh: %s" % base64.b64encode(bodyhash)) 800 try: 801 bh = base64.b64decode(re.sub(br"\s+", b"", sig[b'bh'])) 802 except TypeError as e: 803 raise MessageFormatError(str(e)) 804 if bodyhash != bh: 805 raise ValidationError( 806 "body hash mismatch (got %s, expected %s)" % 807 (base64.b64encode(bodyhash), sig[b'bh'])) 808 809 name = sig[b's'] + b"._domainkey." + sig[b'd'] + b"." 810 try: 811 s = dnsfunc(name) 812 except Exception as e: 813 raise DNSError(str(e)+': '+name) 814 if not s: 815 raise KeyFormatError("missing public key: %s"%name) 816 try: 817 if type(s) is str: 818 s = s.encode('ascii') 819 pub = parse_tag_value(s) 820 except InvalidTagValueList as e: 821 raise KeyFormatError(e) 822 try: 823 pk = parse_public_key(base64.b64decode(pub[b'p'])) 824 self.keysize = bitsize(pk['modulus']) 825 except KeyError: 826 raise KeyFormatError("incomplete public key: %s" % s) 827 except (TypeError,UnparsableKeyError) as e: 828 raise KeyFormatError("could not parse public key (%s): %s" % (pub[b'p'],e)) 829 include_headers = [x.lower() for x in re.split(br"\s*:\s*", sig[b'h'])] 830 self.include_headers = tuple(include_headers) 831 832 return self.verify_sig(sig, include_headers, sigheaders[idx], dnsfunc)
833 834 #: Hold messages and options during ARC signing and verification.
835 -class ARC(DomainSigner):
836 #: Header fields used by ARC 837 ARC_HEADERS = (b'arc-seal', b'arc-message-signature', b'arc-authentication-results') 838 839 #: Regex to extract i= value from ARC headers 840 INSTANCE_RE = re.compile(br'[\s;]?i\s*=\s*(\d+)', re.MULTILINE | re.IGNORECASE) 841
842 - def sorted_arc_headers(self):
843 headers = [] 844 # Use relaxed canonicalization to unfold and clean up headers 845 relaxed_headers = RelaxedCanonicalization.canonicalize_headers(self.headers) 846 for x,y in relaxed_headers: 847 if x.lower() in ARC.ARC_HEADERS: 848 m = ARC.INSTANCE_RE.search(y) 849 if m is not None: 850 try: 851 i = int(m.group(1)) 852 headers.append((i, (x, y))) 853 except ValueError: 854 self.logger.debug("invalid instance number %s: '%s: %s'" % (m.group(1), x, y)) 855 else: 856 self.logger.debug("not instance number: '%s: %s'" % (x, y)) 857 858 if len(headers) == 0: 859 return 0, [] 860 861 def arc_header_key(a): 862 return [a[0], a[1][0].lower(), a[1][1].lower()]
863 864 headers = sorted(headers, key=arc_header_key) 865 headers.reverse() 866 return headers[0][0], headers
867 868 #: Sign an RFC822 message and return the list of ARC set header lines 869 #: 870 #: The include_headers option gives full control over which header fields 871 #: are signed for the ARC-Message-Signature. Note that signing a header 872 #: field that doesn't exist prevents 873 #: that field from being added without breaking the signature. Repeated 874 #: fields (such as Received) can be signed multiple times. Instances 875 #: of the field are signed from bottom to top. Signing a header field more 876 #: times than are currently present prevents additional instances 877 #: from being added without breaking the signature. 878 #: 879 #: The default include_headers for this method differs from the backward 880 #: compatible sign function, which signs all headers not 881 #: in should_not_sign. The default list for this method can be modified 882 #: by tweaking should_sign and frozen_sign (or even should_not_sign). 883 #: It is only necessary to pass an include_headers list when precise control 884 #: is needed. 885 #: 886 #: @param selector: the DKIM selector value for the signature 887 #: @param domain: the DKIM domain value for the signature 888 #: @param privkey: a PKCS#1 private key in base64-encoded text form 889 #: @param srv_id: an srv_id for identitfying AR headers to sign & extract cv from 890 #: @param include_headers: a list of strings indicating which headers 891 #: are to be signed (default rfc4871 recommended headers) 892 #: @return: list of ARC set header fields 893 #: @raise DKIMException: when the message, include_headers, or key are badly 894 #: formed.
895 - def sign(self, selector, domain, privkey, srv_id, include_headers=None, 896 timestamp=None, standardize=False):
897 898 # check if authres has been imported 899 try: 900 AuthenticationResultsHeader 901 except: 902 self.logger.debug("authres package not installed") 903 raise AuthresNotFoundError 904 905 try: 906 pk = parse_pem_private_key(privkey) 907 except UnparsableKeyError as e: 908 raise KeyFormatError(str(e)) 909 910 # extract, parse, filter & group AR headers 911 ar_headers = [res.strip() for [ar, res] in self.headers if ar == b'Authentication-Results'] 912 grouped_headers = [(res, AuthenticationResultsHeader.parse('Authentication-Results: ' + res.decode('utf-8'))) 913 for res in ar_headers] 914 auth_headers = [res for res in grouped_headers if res[1].authserv_id == srv_id.decode('utf-8')] 915 916 if len(auth_headers) == 0: 917 self.logger.debug("no AR headers found, chain terminated") 918 return [] 919 920 # consolidate headers 921 results_lists = [raw.replace(srv_id + b';', b'').strip() for (raw, parsed) in auth_headers] 922 results_lists = [tags.split(b';') for tags in results_lists] 923 results = [tag.strip() for sublist in results_lists for tag in sublist] 924 auth_results = srv_id + b'; ' + b';\r\n '.join(results) 925 926 # extract cv 927 parsed_auth_results = AuthenticationResultsHeader.parse('Authentication-Results: ' + auth_results.decode('utf-8')) 928 arc_results = [res for res in parsed_auth_results.results if res.method == 'arc'] 929 if len(arc_results) == 0: 930 self.logger.debug("no AR arc stamps found, chain terminated") 931 return [] 932 elif len(arc_results) != 1: 933 self.logger.debug("multiple AR arc stamps found, failing chain") 934 chain_validation_status = CV_Fail 935 else: 936 chain_validation_status = arc_results[0].result.lower().encode('utf-8') 937 938 # Setup headers 939 if include_headers is None: 940 include_headers = self.default_sign_headers() 941 942 include_headers = tuple([x.lower() for x in include_headers]) 943 944 # record what verify should extract 945 self.include_headers = include_headers 946 947 # rfc4871 says FROM is required 948 if b'from' not in include_headers: 949 raise ParameterError("The From header field MUST be signed") 950 951 # raise exception for any SHOULD_NOT headers, call can modify 952 # SHOULD_NOT if really needed. 953 for x in set(include_headers).intersection(self.should_not_sign): 954 raise ParameterError("The %s header field SHOULD NOT be signed"%x) 955 956 max_instance, arc_headers_w_instance = self.sorted_arc_headers() 957 instance = 1 958 if len(arc_headers_w_instance) != 0: 959 instance = max_instance + 1 960 961 if instance == 1 and chain_validation_status != CV_None: 962 raise ParameterError("No existing chain found on message, cv should be none") 963 elif instance != 1 and chain_validation_status == CV_None: 964 raise ParameterError("cv=none not allowed on instance %d" % instance) 965 966 new_arc_set = [] 967 if chain_validation_status != CV_Fail: 968 arc_headers = [y for x,y in arc_headers_w_instance] 969 else: # don't include previous sets for a failed/invalid chain 970 arc_headers = [] 971 972 # Compute ARC-Authentication-Results 973 aar_value = ("i=%d; " % instance).encode('utf-8') + auth_results 974 if aar_value[-1] != b'\n': aar_value += b'\r\n' 975 976 new_arc_set.append(b"ARC-Authentication-Results: " + aar_value) 977 self.headers.insert(0, (b"arc-authentication-results", aar_value)) 978 arc_headers.insert(0, (b"ARC-Authentication-Results", aar_value)) 979 980 # Compute bh= 981 canon_policy = CanonicalizationPolicy.from_c_value(b'relaxed/relaxed') 982 983 self.hasher = HASH_ALGORITHMS[self.signature_algorithm] 984 h = HashThrough(self.hasher()) 985 h.update(canon_policy.canonicalize_body(self.body)) 986 self.logger.debug("sign ams body hashed: %r" % h.hashed()) 987 bodyhash = base64.b64encode(h.digest()) 988 989 # Compute ARC-Message-Signature 990 timestamp = str(timestamp or int(time.time())).encode('ascii') 991 ams_fields = [x for x in [ 992 (b'i', str(instance).encode('ascii')), 993 (b'a', self.signature_algorithm), 994 (b'c', b'relaxed/relaxed'), 995 (b'd', domain), 996 (b's', selector), 997 (b't', timestamp), 998 (b'h', b" : ".join(include_headers)), 999 (b'bh', bodyhash), 1000 # Force b= to fold onto it's own line so that refolding after 1001 # adding sig doesn't change whitespace for previous tags. 1002 (b'b', b'0'*60), 1003 ] if x] 1004 1005 res = self.gen_header(ams_fields, include_headers, canon_policy, 1006 b"ARC-Message-Signature", pk, standardize) 1007 1008 new_arc_set.append(b"ARC-Message-Signature: " + res) 1009 self.headers.insert(0, (b"ARC-Message-Signature", res)) 1010 arc_headers.insert(0, (b"ARC-Message-Signature", res)) 1011 1012 # Compute ARC-Seal 1013 as_fields = [x for x in [ 1014 (b'i', str(instance).encode('ascii')), 1015 (b'cv', chain_validation_status), 1016 (b'a', self.signature_algorithm), 1017 (b'd', domain), 1018 (b's', selector), 1019 (b't', timestamp), 1020 # Force b= to fold onto it's own line so that refolding after 1021 # adding sig doesn't change whitespace for previous tags. 1022 (b'b', b'0'*60), 1023 ] if x] 1024 1025 as_include_headers = [x[0].lower() for x in arc_headers] 1026 as_include_headers.reverse() 1027 1028 # if our chain is failing or invalid, we only grab the most recent set 1029 # reversing the order of the headers accomplishes this 1030 if chain_validation_status == CV_Fail: 1031 self.headers.reverse() 1032 1033 res = self.gen_header(as_fields, as_include_headers, canon_policy, 1034 b"ARC-Seal", pk, standardize) 1035 1036 new_arc_set.append(b"ARC-Seal: " + res) 1037 self.headers.insert(0, (b"ARC-Seal", res)) 1038 arc_headers.insert(0, (b"ARC-Seal", res)) 1039 1040 new_arc_set.reverse() 1041 1042 return new_arc_set
1043 1044 #: Verify an ARC set. 1045 #: @type instance: int 1046 #: @param instance: which ARC set to verify, based on i= instance. 1047 #: @type dnsfunc: callable 1048 #: @param dnsfunc: an optional function to lookup TXT resource records 1049 #: for a DNS domain. The default uses dnspython or pydns. 1050 #: @return: True if signature verifies or False otherwise 1051 #: @return: three-tuple of (CV Result (CV_Pass, CV_Fail, CV_None or None, for a chain that has ended), list of 1052 #: result dictionaries, result reason) 1053 #: @raise DKIMException: when the message, signature, or key are badly formed
1054 - def verify(self,dnsfunc=get_txt):
1055 result_data = [] 1056 max_instance, arc_headers_w_instance = self.sorted_arc_headers() 1057 if max_instance == 0: 1058 return CV_None, result_data, "Message is not ARC signed" 1059 for instance in range(max_instance, 0, -1): 1060 try: 1061 result = self.verify_instance(arc_headers_w_instance, instance, dnsfunc=dnsfunc) 1062 result_data.append(result) 1063 except DKIMException as e: 1064 self.logger.error("%s" % e) 1065 return CV_Fail, result_data, "%s" % e 1066 1067 # Most recent instance must ams-validate 1068 if not result_data[0]['ams-valid']: 1069 return CV_Fail, result_data, "Most recent ARC-Message-Signature did not validate" 1070 for result in result_data: 1071 if result['cv'] == CV_Fail: 1072 return None, result_data, "ARC-Seal[%d] reported failure, the chain is terminated" % result['instance'] 1073 elif not result['as-valid']: 1074 return CV_Fail, result_data, "ARC-Seal[%d] did not validate" % result['instance'] 1075 elif (result['instance'] == 1) and (result['cv'] != CV_None): 1076 return CV_Fail, result_data, "ARC-Seal[%d] reported invalid status %s" % (result['instance'], result['cv']) 1077 elif (result['instance'] != 1) and (result['cv'] == CV_None): 1078 return CV_Fail, result_data, "ARC-Seal[%d] reported invalid status %s" % (result['instance'], result['cv']) 1079 return CV_Pass, result_data, "success"
1080 1081 #: Verify an ARC set. 1082 #: @type arc_headers_w_instance: list 1083 #: @param arc_headers_w_instance: list of tuples, (instance, (name, value)) of 1084 #: ARC headers 1085 #: @type instance: int 1086 #: @param instance: which ARC set to verify, based on i= instance. 1087 #: @type dnsfunc: callable 1088 #: @param dnsfunc: an optional function to lookup TXT resource records 1089 #: for a DNS domain. The default uses dnspython or pydns. 1090 #: @return: True if signature verifies or False otherwise 1091 #: @raise DKIMException: when the message, signature, or key are badly formed
1092 - def verify_instance(self,arc_headers_w_instance,instance,dnsfunc=get_txt):
1093 if (instance == 0) or (len(arc_headers_w_instance) == 0): 1094 raise ParameterError("request to verify instance %d not present" % (instance)) 1095 1096 aar_value = None 1097 ams_value = None 1098 as_value = None 1099 arc_headers = [] 1100 output = { 'instance': instance } 1101 1102 for i, arc_header in arc_headers_w_instance: 1103 if i > instance: continue 1104 arc_headers.append(arc_header) 1105 if i == instance: 1106 if arc_header[0].lower() == b"arc-authentication-results": 1107 if aar_value is not None: 1108 raise MessageFormatError("Duplicate ARC-Authentication-Results for instance %d" % instance) 1109 aar_value = arc_header[1] 1110 elif arc_header[0].lower() == b"arc-message-signature": 1111 if ams_value is not None: 1112 raise MessageFormatError("Duplicate ARC-Message-Signature for instance %d" % instance) 1113 ams_value = arc_header[1] 1114 elif arc_header[0].lower() == b"arc-seal": 1115 if as_value is not None: 1116 raise MessageFormatError("Duplicate ARC-Seal for instance %d" % instance) 1117 as_value = arc_header[1] 1118 1119 if (aar_value is None) or (ams_value is None) or (as_value is None): 1120 raise MessageFormatError("Incomplete ARC set for instance %d" % instance) 1121 1122 output['aar-value'] = aar_value 1123 1124 # Validate Arc-Message-Signature 1125 try: 1126 sig = parse_tag_value(ams_value) 1127 except InvalidTagValueList as e: 1128 raise MessageFormatError(e) 1129 1130 self.logger.debug("ams sig[%d]: %r" % (instance, sig)) 1131 1132 validate_signature_fields(sig, [b'i', b'a', b'b', b'bh', b'd', b'h', b's'], True) 1133 output['ams-domain'] = sig[b'd'] 1134 output['ams-selector'] = sig[b's'] 1135 1136 include_headers = [x.lower() for x in re.split(br"\s*:\s*", sig[b'h'])] 1137 if b'arc-seal' in include_headers: 1138 raise ParameterError("The Arc-Message-Signature MUST NOT sign ARC-Seal") 1139 1140 ams_header = (b'ARC-Message-Signature', b' ' + ams_value) 1141 1142 1143 # we can't use the AMS provided above, as it's already been canonicalized relaxed 1144 # for use in validating the AS. However the AMS is included in the AMS itself, 1145 # and this can use simple canonicalization 1146 raw_ams_header = [(x, y) for (x, y) in self.headers if x.lower() == b'arc-message-signature'][0] 1147 1148 try: 1149 ams_valid = self.verify_sig(sig, include_headers, raw_ams_header, dnsfunc) 1150 except DKIMException as e: 1151 self.logger.error("%s" % e) 1152 ams_valid = False 1153 1154 output['ams-valid'] = ams_valid 1155 self.logger.debug("ams valid: %r" % ams_valid) 1156 1157 # Validate Arc-Seal 1158 try: 1159 sig = parse_tag_value(as_value) 1160 except InvalidTagValueList as e: 1161 raise MessageFormatError(e) 1162 1163 self.logger.debug("as sig[%d]: %r" % (instance, sig)) 1164 1165 validate_signature_fields(sig, [b'i', b'a', b'b', b'cv', b'd', b's'], True) 1166 output['as-domain'] = sig[b'd'] 1167 output['as-selector'] = sig[b's'] 1168 output['cv'] = sig[b'cv'] 1169 1170 as_include_headers = [x[0].lower() for x in arc_headers] 1171 as_include_headers.reverse() 1172 as_header = (b'ARC-Seal', b' ' + as_value) 1173 try: 1174 as_valid = self.verify_sig(sig, as_include_headers[:-1], as_header, dnsfunc) 1175 except DKIMException as e: 1176 self.logger.error("%s" % e) 1177 as_valid = False 1178 1179 output['as-valid'] = as_valid 1180 self.logger.debug("as valid: %r" % as_valid) 1181 return output
1182
1183 -def sign(message, selector, domain, privkey, identity=None, 1184 canonicalize=(b'relaxed', b'simple'), 1185 signature_algorithm=b'rsa-sha256', 1186 include_headers=None, length=False, logger=None):
1187 """Sign an RFC822 message and return the DKIM-Signature header line. 1188 @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings) 1189 @param selector: the DKIM selector value for the signature 1190 @param domain: the DKIM domain value for the signature 1191 @param privkey: a PKCS#1 private key in base64-encoded text form 1192 @param identity: the DKIM identity value for the signature (default "@"+domain) 1193 @param canonicalize: the canonicalization algorithms to use (default (Simple, Simple)) 1194 @param signature_algorithm: the signing algorithm to use when signing 1195 @param include_headers: a list of strings indicating which headers are to be signed (default all headers not listed as SHOULD NOT sign) 1196 @param length: true if the l= tag should be included to indicate body length (default False) 1197 @param logger: a logger to which debug info will be written (default None) 1198 @return: DKIM-Signature header field terminated by \\r\\n 1199 @raise DKIMException: when the message, include_headers, or key are badly formed. 1200 """ 1201 1202 d = DKIM(message,logger=logger,signature_algorithm=signature_algorithm) 1203 return d.sign(selector, domain, privkey, identity=identity, canonicalize=canonicalize, include_headers=include_headers, length=length)
1204
1205 -def verify(message, logger=None, dnsfunc=get_txt, minkey=1024):
1206 """Verify the first (topmost) DKIM signature on an RFC822 formatted message. 1207 @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings) 1208 @param logger: a logger to which debug info will be written (default None) 1209 @return: True if signature verifies or False otherwise 1210 """ 1211 d = DKIM(message,logger=logger,minkey=minkey) 1212 try: 1213 return d.verify(dnsfunc=dnsfunc) 1214 except DKIMException as x: 1215 if logger is not None: 1216 logger.error("%s" % x) 1217 return False
1218 1219 # For consistency with ARC 1220 dkim_sign = sign 1221 dkim_verify = verify 1222
1223 -def arc_sign(message, selector, domain, privkey, 1224 srv_id, signature_algorithm=b'rsa-sha256', 1225 include_headers=None, timestamp=None, 1226 logger=None, standardize=False):
1227 """Sign an RFC822 message and return the ARC set header lines for the next instance 1228 @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings) 1229 @param selector: the DKIM selector value for the signature 1230 @param domain: the DKIM domain value for the signature 1231 @param privkey: a PKCS#1 private key in base64-encoded text form 1232 @param srv_id: the authserv_id used to identify the ADMD's AR headers 1233 @param signature_algorithm: the signing algorithm to use when signing 1234 @param include_headers: a list of strings indicating which headers are to be signed (default all headers not listed as SHOULD NOT sign) 1235 @param logger: a logger to which debug info will be written (default None) 1236 @return: A list containing the ARC set of header fields for the next instance 1237 @raise DKIMException: when the message, include_headers, or key are badly formed. 1238 """ 1239 1240 a = ARC(message,logger=logger,signature_algorithm=signature_algorithm) 1241 if not include_headers: 1242 include_headers = a.default_sign_headers() 1243 return a.sign(selector, domain, privkey, srv_id, include_headers=include_headers, 1244 timestamp=timestamp, standardize=standardize)
1245
1246 -def arc_verify(message, logger=None, dnsfunc=get_txt, minkey=1024):
1247 """Verify the ARC chain on an RFC822 formatted message. 1248 @param message: an RFC822 formatted message (with either \\n or \\r\\n line endings) 1249 @param logger: a logger to which debug info will be written (default None) 1250 @param dnsfunc: an optional function to lookup TXT resource records 1251 @param minkey: the minimum key size to accept 1252 @return: three-tuple of (CV Result (CV_Pass, CV_Fail or CV_None), list of 1253 result dictionaries, result reason) 1254 """ 1255 a = ARC(message,logger=logger,minkey=minkey) 1256 try: 1257 return a.verify(dnsfunc=dnsfunc) 1258 except DKIMException as x: 1259 if logger is not None: 1260 logger.error("%s" % x) 1261 return CV_Fail, [], "%s" % x
1262