Compare commits

...

10 Commits

10 changed files with 78 additions and 29 deletions
+19
View File
@@ -1,3 +1,22 @@
UNRELEASED Version 1.1.9
- Fix dkimverify verbose option so it works and add documentation, thanks
to Uwe Kleine-König for the patch (Debian: #1075791)
2024-07-04 Version 1.1.8
- Correctly handle verification of signatures without t= (timestamp) and
with x= (expiration); both are optional (LP: #2071892)
2024-06-23 Version 1.1.7
- Fix error in validate_signature_fields which prevented signature
expiration from being properly evaluated (LP: #2068937)
- Correct ARC signing for AR headers with authres-version or comments
before resinfo (LP: #2052526) - Thanks to Nikolay Vizovitin for the
report and the fix
- Correct line separtor after AAR header field (LP: #2049018) - Thanks to
Nikolay Vizovitin for the report and the fix
- Correct signature in ARC-Seal on LF as linesep (LP: #2052720) - Thanks to
Nikolay Vizovitin for the report and the fix
2024-04-14 Version 1.1.6
- Use raw byte string for regex; fixes SyntaxWarning in Python 3.12 due to
invalid escape sequence (LP: #2049518) - Thanks to Simon Chopin for the
+3 -3
View File
@@ -13,7 +13,7 @@ https://tools.ietf.org/html/rfc6376
# VERSION
This is dkimpy 1.1.7.
This is dkimpy 1.1.9.
# REQUIREMENTS
@@ -24,8 +24,8 @@ extras_requires feature 'ARC' will add the extra dependencies needed for ARC.
Similarly, extras_requires feature 'asyncio' will add the extra dependencies
needed for asyncio.
- Python 3.x >= 3.5. Recent versions have not been on python3 < 3.4, but
may still work on earlier python3 versions.
- Python 3.x >= 3.5. Recent versions have not been tested on python3 < 3.4,
but may still work on earlier python3 versions.
- dnspython or py3dns. dnspython is preferred if both are present and
installed to satisfy the DNS module requirement if neither are installed.
- authres. Needed for ARC.
+15 -18
View File
@@ -331,6 +331,8 @@ def validate_signature_fields(sig, mandatory_fields=[b'v', b'a', b'b', b'bh', b'
t_sign = int(sig[b't'])
if t_sign > now + slop:
raise ValidationError("t= value is in the future (%s)" % sig[b't'])
else:
t_sign = None
if b'v' in sig and sig[b'v'] != b"1":
raise ValidationError("v= value is not 1 (%s)" % sig[b'v'])
@@ -345,7 +347,7 @@ def validate_signature_fields(sig, mandatory_fields=[b'v', b'a', b'b', b'bh', b'
if x_sign < now - slop:
raise ValidationError(
"x= value is past (%s)" % sig[b'x'])
if x_sign < t_sign:
if t_sign and x_sign < t_sign:
raise ValidationError(
"x= value is less than t= value (x=%s t=%s)" %
(sig[b'x'], sig[b't']))
@@ -1052,28 +1054,26 @@ class ARC(DomainSigner):
# extract, parse, filter & group AR headers
ar_headers = [res.strip() for [ar, res] in self.headers if ar == b'Authentication-Results']
grouped_headers = []
parsed_ar_headers = []
for res in ar_headers:
try: # see LP: #1884044
grouped_headers.append((res, authres.AuthenticationResultsHeader.parse('Authentication-Results: ' + res.decode('utf-8'))))
# Note: parsing headers currently strips embedded comments
parsed_ar_headers.append(authres.AuthenticationResultsHeader.parse('Authentication-Results: ' + res.decode('utf-8')))
except authres.core.SyntaxError:
# Skip over invalid AR header fields
pass
auth_headers = [res for res in grouped_headers if res[1].authserv_id == srv_id.decode('utf-8')]
auth_headers = [header for header in parsed_ar_headers if header.authserv_id == srv_id.decode('utf-8')]
if len(auth_headers) == 0:
self.logger.debug("no AR headers found, chain terminated")
return []
# consolidate headers
results_lists = [raw.replace(srv_id + b';', b'').strip() for (raw, parsed) in auth_headers]
results_lists = [tags.split(b';') for tags in results_lists]
results = [tag.strip() for sublist in results_lists for tag in sublist]
auth_results = srv_id + b'; ' + (b';' + self.linesep + b' ').join(results)
results = [res for header in auth_headers for res in header.results]
auth_results = srv_id + b''.join(b';' + self.linesep + b' ' + str(res).encode('utf-8') for res in results)
# extract cv
parsed_auth_results = authres.AuthenticationResultsHeader.parse('Authentication-Results: ' + auth_results.decode('utf-8'))
arc_results = [res for res in parsed_auth_results.results if res.method == 'arc']
arc_results = [res for res in results if res.method == 'arc']
if len(arc_results) == 0:
chain_validation_status = CV_None
elif len(arc_results) != 1:
@@ -1120,16 +1120,13 @@ class ARC(DomainSigner):
arc_headers = []
# Compute ARC-Authentication-Results
aar_value = ("i=%d; " % instance).encode('utf-8') + auth_results
if aar_value[-1] != b'\n': aar_value += b'\r\n'
aar_value = ("i=%d; " % instance).encode('utf-8') + auth_results.rstrip() + self.linesep
canon_policy = CanonicalizationPolicy.from_c_value(b'relaxed/relaxed')
new_arc_set.append(b"ARC-Authentication-Results: " + aar_value)
self.headers.insert(0, (b"arc-authentication-results", aar_value))
arc_headers.insert(0, (b"ARC-Authentication-Results", aar_value))
self.headers = canon_policy.canonicalize_headers(arc_headers[:1]) + self.headers
# Compute bh=
canon_policy = CanonicalizationPolicy.from_c_value(b'relaxed/relaxed')
self.hasher = HASH_ALGORITHMS[self.signature_algorithm]
h = HashThrough(self.hasher(), self.debug_content)
h.update(canon_policy.canonicalize_body(self.body))
@@ -1157,8 +1154,8 @@ class ARC(DomainSigner):
b"ARC-Message-Signature", pk, standardize)
new_arc_set.append(b"ARC-Message-Signature: " + res)
self.headers.insert(0, (b"ARC-Message-Signature", res))
arc_headers.insert(0, (b"ARC-Message-Signature", res))
self.headers = canon_policy.canonicalize_headers(arc_headers[:1]) + self.headers
# Compute ARC-Seal
as_fields = [x for x in [
@@ -1186,8 +1183,8 @@ class ARC(DomainSigner):
b"ARC-Seal", pk, standardize)
new_arc_set.append(b"ARC-Seal: " + res)
self.headers.insert(0, (b"ARC-Seal", res))
arc_headers.insert(0, (b"ARC-Seal", res))
self.headers = canon_policy.canonicalize_headers(arc_headers[:1]) + self.headers
new_arc_set.reverse()
+1 -1
View File
@@ -58,7 +58,7 @@ def strip_trailing_lines(content):
return content[:end]
def unfold_header_value(content):
return re.sub(b"\r\n", b"", content)
return re.sub(b"\r?\n", b"", content)
def correct_empty_body(content):
+4 -1
View File
@@ -34,15 +34,18 @@ def main():
epilog="message to be verified follows commands on stdin")
parser.add_argument('--index', metavar='N', type=int, default=0,
help='Index of DKIM signature header to verify: default=0')
parser.add_argument('-v', '--verbose', default=False, action='store_true',
help='Add some debugging output')
args=parser.parse_args()
if sys.version_info[0] >= 3:
# Make sys.stdin a binary stream.
sys.stdin = sys.stdin.detach()
message = sys.stdin.read()
verbose = '-v' in sys.argv
verbose = args.verbose
if verbose:
import logging
logging.basicConfig(level=logging.DEBUG)
d = dkim.DKIM(message, logger=logging)
else:
d = dkim.DKIM(message)
+20
View File
@@ -0,0 +1,20 @@
DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple;
d=football.example.com; i=@football.example.com;
q=dns/txt; s=test; h=from : to : subject :
date : message-id : from : subject : date; x=100000000000;
bh=4bLNXImK9drULnmePzZNEBleUanJCX5PIsDIFoH4KTQ=;
b=icKcLSEZYXJ95flvWE8FT6hl5iqd8MC/LEKYH0QjsqYy6MO/4pgVNCZH
l/RAXAuADxE/40Fg7uTlxwwD1hjN2Ple6J//cJfslBdDOq6zTVbne1dqtl
NOat7iamJ1AfRqyG+ja7a2AZsrpUuJ7VA6O+0zRYPqpwMEkEFIzI9i/Xk=
From: Joe SixPack <joe@football.example.com>
To: Suzie Q <suzie@shopping.example.net>
Subject: Is dinner ready?
Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
Message-ID: <20030712040037.46341.5F8J@football.example.com>
Hi.
We lost the game. Are you hungry yet?
Joe.
+1 -1
View File
@@ -74,7 +74,7 @@ Y+vtSBczUiKERHv1yRbcaQtZFh5wtiRrN04BLUTD21MycBX5jYchHjPY/wIDAQAB"""
sig_lines = dkim.arc_sign(
self.message, b"test", b"example.com", self.key, b"lists.example.org", timestamp="12345")
expected_sig = [b'ARC-Seal: i=1; cv=none; a=rsa-sha256; d=example.com; s=test; t=12345;\r\n b=MBw2+L1/4PuYWJlt1tZlDtbOvyfbyH2t2N6DinFV/BIaB2LqbDKTYjXXk9HuuK1/qEkTd\r\n TxCYScIrtVO7pFbGiSawMuLatVzHNCqTURa1zBTXr2mKW1hgdmrtMMUcMVCYxr1AJpu6IYX\r\n VMIoOAn7tIDdO0VLokK6FnIXTWEAplQ=\r\n', b'ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed;\r\n d=example.com; s=test; t=12345; h=message-id : date : from : to :\r\n subject : from; bh=wE7NXSkgnx9PGiavN4OZhJztvkqPDlemV3OGuEnLwNo=;\r\n b=a0f6qc3k9eECTSR155A0TQS+LjqPFWfI/brQBA83EUz00SNxj1wmWykvs1hhBVeM0r1kE\r\n Qc6CKbzRYaBNSiFj4q8JBpRIujLz1qLyGmPuAI6ddu/Z/1hQxgpVcp/odmI1UMV2R+d+yQ7\r\n tUp3EQxF/GYNt22rV4rNmDmANZVqJ90=\r\n', b'ARC-Authentication-Results: i=1; lists.example.org; arc=none;\r\n spf=pass smtp.mfrom=jqd@d1.example;\r\n dkim=pass (1024-bit key) header.i=@d1.example;\r\n dmarc=pass\r\n']
expected_sig = [b'ARC-Seal: i=1; cv=none; a=rsa-sha256; d=example.com; s=test; t=12345;\r\n b=iSKjTQ93xUC6gt4yutHrOf0F/qb4E5voEeuucd66VhM4n/7ifBMMHqYwncgz9sefduM6C\r\n UthuUSzqE2YamkGXQgKPIG9t4ZCOrx1OXGE34WF9ZeI/E0csrN+wK7sq/RjgN3z4qxLPMsp\r\n lW+BUUHCNuCIvxcZ55Ky6evIb/Saj2o=\r\n', b'ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed;\r\n d=example.com; s=test; t=12345; h=message-id : date : from : to :\r\n subject : from; bh=wE7NXSkgnx9PGiavN4OZhJztvkqPDlemV3OGuEnLwNo=;\r\n b=a0f6qc3k9eECTSR155A0TQS+LjqPFWfI/brQBA83EUz00SNxj1wmWykvs1hhBVeM0r1kE\r\n Qc6CKbzRYaBNSiFj4q8JBpRIujLz1qLyGmPuAI6ddu/Z/1hQxgpVcp/odmI1UMV2R+d+yQ7\r\n tUp3EQxF/GYNt22rV4rNmDmANZVqJ90=\r\n', b'ARC-Authentication-Results: i=1; lists.example.org;\r\n arc=none;\r\n spf=pass smtp.mfrom=jqd@d1.example;\r\n dkim=pass header.i=@d1.example;\r\n dmarc=pass\r\n']
self.assertEqual(expected_sig, sig_lines)
(cv, res, reason) = dkim.arc_verify(b''.join(sig_lines) + self.message, dnsfunc=self.dnsfunc)
+9
View File
@@ -62,6 +62,7 @@ class TestSignAndVerify(unittest.TestCase):
self.message5 = read_test_data("rfc6376.signed.rsa.msg")
self.message6 = read_test_data("test.message.baddomain")
self.message7 = read_test_data("rfc6376.w1258.msg")
self.message8 = read_test_data("rfc6376.signed.no_t.msg")
self.key = read_test_data("test.private")
self.rfckey = read_test_data("rfc8032_7_1.key")
@@ -250,6 +251,7 @@ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ=="""
d = dkim.DKIM(self.message4)
res = d.verify(dnsfunc=self.dnsfunc5)
self.assertTrue(res)
def test_non_utf8(self):
# A message with Windows-1258 encoding is signed and verifies.
for header_algo in (b"simple", b"relaxed"):
@@ -262,6 +264,13 @@ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ=="""
# As of 1.1.0 this won't verify, but at least we don't crash. FIXME
self.assertFalse(res)
def test_no_t(self):
# Signature Timestamp is optional, so don't crash if it's missing.
d = dkim.DKIM(self.message8)
res = d.verify(dnsfunc=self.dnsfunc5)
# Signature won't verify, but that's not what we are testing.
self.assertFalse(res)
def test_catch_bad_key(self):
# Raise correct error for defective public key.
d = dkim.DKIM(self.message5)
+2 -1
View File
@@ -142,11 +142,12 @@ code 0 if the signature verifies successfully. Otherwise, it returns with exit
code 1.
.SH "USAGE"
usage: dkimverify.py [\-h] [\-\-index N] <message
usage: dkimverify.py [\-h] [\-\-index N] [\-\-verbose] <message
optional arguments:
\-h, \-\-help show this help message and exit
\-\-index N Index of DKIM signature header to verify: default=0
\-\-verbose Emit diagnostic output
.SH "AUTHORS"
This version of \fBdkimverify\fR was written by Greg Hewgill <greg@hewgill.com>.
+1 -1
View File
@@ -25,7 +25,7 @@ from setuptools import setup
import os
import sys
version = "1.1.7"
version = "1.1.9"
kw = {} # Work-around for lack of 'or' requires in setuptools.
try: