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