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 time
40
41
42 try:
43 from authres import AuthenticationResultsHeader
44 except:
45 pass
46
47
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:
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'
105 Simple = b'simple'
106
107
108 CV_Pass = b'pass'
109 CV_Fail = b'fail'
110 CV_None = b'none'
111
114 self.data = []
115 self.hasher = hasher
116 self.name = hasher.name
117
119 self.data.append(data)
120 return self.hasher.update(data)
121
123 return self.hasher.digest()
124
127
129 return b''.join(self.data)
130
132 """Return size of long in bits."""
133 return len(bin(x)) - 2
134
136 """Base class for DKIM errors."""
137 pass
138
140 """Internal error in dkim module. Should never happen."""
141 pass
142
146
150
152 """Input parameter error."""
153 pass
154
156 "Validation error."
157 pass
158
160 "DNS error."
161 pass
162
164 """ Authres Package not installed, needed for ARC """
165 pass
166
168 """ Nacl package not installed, needed for ed25119 signatures """
169 pass
170
172 """ Key type (k tag) is not known (rsa/ed25519) """
173
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
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
205 """Update hash for signed message header fields."""
206 sign_headers = select_headers(headers,include_headers)
207
208
209 cheaders = canonicalize_headers.canonicalize_headers(
210 [(sigheader[0], RE_BTAG.sub(b'\\1',sigheader[1]))])
211
212
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
221 """Update hash for signed message header fields."""
222 hash_header = ''
223 sign_headers = select_headers(headers,include_headers)
224
225
226 cheaders = canonicalize_headers.canonicalize_headers(
227 [(sigheader[0], RE_BTAG.sub(b'\\1',sigheader[1]))])
228
229
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
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
265
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
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
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
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
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
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
368
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
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
410 -class DomainSigner(object):
411
412
413
414
415
416
417
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
429 self.should_sign = set(DKIM.SHOULD)
430
431
432
433
434 self.should_not_sign = set(DKIM.SHOULD_NOT)
435
436 self.frozen_sign = set(DKIM.FROZEN)
437
438
439 self.minkey = minkey
440
441
442
443
444
445 FROZEN = (b'from',b'date',b'subject')
446
447
448
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
459
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
466
467
468
469
470
471
472
473
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
491
492
493
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
500 self.domain = None
501
502 self.selector = 'default'
503
504
505
506 self.signature_fields = {}
507
508
509
510 self.signed_headers = []
511
512 self.keysize = 0
513
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
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
532
533
534
535
536
537
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
567
568
569
570
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
581
582
583
584
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
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
622
623
624
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
654 -class DKIM(DomainSigner):
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
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
712 self.include_headers = include_headers
713
714
715 if b'from' not in include_headers:
716 raise ParameterError("The From header field MUST be signed")
717
718
719
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
743
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
756
757
758
759
760
761
762
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
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
835 -class ARC(DomainSigner):
836
837 ARC_HEADERS = (b'arc-seal', b'arc-message-signature', b'arc-authentication-results')
838
839
840 INSTANCE_RE = re.compile(br'[\s;]?i\s*=\s*(\d+)', re.MULTILINE | re.IGNORECASE)
841
843 headers = []
844
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
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895 - def sign(self, selector, domain, privkey, srv_id, include_headers=None,
896 timestamp=None, standardize=False):
897
898
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
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
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
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
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
945 self.include_headers = include_headers
946
947
948 if b'from' not in include_headers:
949 raise ParameterError("The From header field MUST be signed")
950
951
952
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:
970 arc_headers = []
971
972
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
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
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
1001
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
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
1021
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
1029
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
1045
1046
1047
1048
1049
1050
1051
1052
1053
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
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
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
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
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
1144
1145
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
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
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
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
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