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 2024-04-14 Version 1.1.6
- Use raw byte string for regex; fixes SyntaxWarning in Python 3.12 due to - 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 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 # VERSION
This is dkimpy 1.1.7. This is dkimpy 1.1.9.
# REQUIREMENTS # 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 Similarly, extras_requires feature 'asyncio' will add the extra dependencies
needed for asyncio. needed for asyncio.
- Python 3.x >= 3.5. Recent versions have not been on python3 < 3.4, but - Python 3.x >= 3.5. Recent versions have not been tested on python3 < 3.4,
may still work on earlier python3 versions. but may still work on earlier python3 versions.
- dnspython or py3dns. dnspython is preferred if both are present and - dnspython or py3dns. dnspython is preferred if both are present and
installed to satisfy the DNS module requirement if neither are installed. installed to satisfy the DNS module requirement if neither are installed.
- authres. Needed for ARC. - 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']) t_sign = int(sig[b't'])
if t_sign > now + slop: if t_sign > now + slop:
raise ValidationError("t= value is in the future (%s)" % sig[b't']) 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": if b'v' in sig and sig[b'v'] != b"1":
raise ValidationError("v= value is not 1 (%s)" % sig[b'v']) 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: if x_sign < now - slop:
raise ValidationError( raise ValidationError(
"x= value is past (%s)" % sig[b'x']) "x= value is past (%s)" % sig[b'x'])
if x_sign < t_sign: if t_sign and x_sign < t_sign:
raise ValidationError( raise ValidationError(
"x= value is less than t= value (x=%s t=%s)" % "x= value is less than t= value (x=%s t=%s)" %
(sig[b'x'], sig[b't'])) (sig[b'x'], sig[b't']))
@@ -1052,28 +1054,26 @@ class ARC(DomainSigner):
# extract, parse, filter & group AR headers # extract, parse, filter & group AR headers
ar_headers = [res.strip() for [ar, res] in self.headers if ar == b'Authentication-Results'] 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: for res in ar_headers:
try: # see LP: #1884044 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: except authres.core.SyntaxError:
# Skip over invalid AR header fields # Skip over invalid AR header fields
pass 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: if len(auth_headers) == 0:
self.logger.debug("no AR headers found, chain terminated") self.logger.debug("no AR headers found, chain terminated")
return [] return []
# consolidate headers # consolidate headers
results_lists = [raw.replace(srv_id + b';', b'').strip() for (raw, parsed) in auth_headers] results = [res for header in auth_headers for res in header.results]
results_lists = [tags.split(b';') for tags in results_lists] auth_results = srv_id + b''.join(b';' + self.linesep + b' ' + str(res).encode('utf-8') for res in results)
results = [tag.strip() for sublist in results_lists for tag in sublist]
auth_results = srv_id + b'; ' + (b';' + self.linesep + b' ').join(results)
# extract cv # extract cv
parsed_auth_results = authres.AuthenticationResultsHeader.parse('Authentication-Results: ' + auth_results.decode('utf-8')) arc_results = [res for res in results if res.method == 'arc']
arc_results = [res for res in parsed_auth_results.results if res.method == 'arc']
if len(arc_results) == 0: if len(arc_results) == 0:
chain_validation_status = CV_None chain_validation_status = CV_None
elif len(arc_results) != 1: elif len(arc_results) != 1:
@@ -1120,16 +1120,13 @@ class ARC(DomainSigner):
arc_headers = [] arc_headers = []
# Compute ARC-Authentication-Results # Compute ARC-Authentication-Results
aar_value = ("i=%d; " % instance).encode('utf-8') + auth_results aar_value = ("i=%d; " % instance).encode('utf-8') + auth_results.rstrip() + self.linesep
if aar_value[-1] != b'\n': aar_value += b'\r\n' canon_policy = CanonicalizationPolicy.from_c_value(b'relaxed/relaxed')
new_arc_set.append(b"ARC-Authentication-Results: " + aar_value) 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)) arc_headers.insert(0, (b"ARC-Authentication-Results", aar_value))
self.headers = canon_policy.canonicalize_headers(arc_headers[:1]) + self.headers
# Compute bh= # Compute bh=
canon_policy = CanonicalizationPolicy.from_c_value(b'relaxed/relaxed')
self.hasher = HASH_ALGORITHMS[self.signature_algorithm] self.hasher = HASH_ALGORITHMS[self.signature_algorithm]
h = HashThrough(self.hasher(), self.debug_content) h = HashThrough(self.hasher(), self.debug_content)
h.update(canon_policy.canonicalize_body(self.body)) h.update(canon_policy.canonicalize_body(self.body))
@@ -1157,8 +1154,8 @@ class ARC(DomainSigner):
b"ARC-Message-Signature", pk, standardize) b"ARC-Message-Signature", pk, standardize)
new_arc_set.append(b"ARC-Message-Signature: " + res) 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)) arc_headers.insert(0, (b"ARC-Message-Signature", res))
self.headers = canon_policy.canonicalize_headers(arc_headers[:1]) + self.headers
# Compute ARC-Seal # Compute ARC-Seal
as_fields = [x for x in [ as_fields = [x for x in [
@@ -1186,8 +1183,8 @@ class ARC(DomainSigner):
b"ARC-Seal", pk, standardize) b"ARC-Seal", pk, standardize)
new_arc_set.append(b"ARC-Seal: " + res) new_arc_set.append(b"ARC-Seal: " + res)
self.headers.insert(0, (b"ARC-Seal", res))
arc_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() new_arc_set.reverse()
+1 -1
View File
@@ -58,7 +58,7 @@ def strip_trailing_lines(content):
return content[:end] return content[:end]
def unfold_header_value(content): 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): def correct_empty_body(content):
+4 -1
View File
@@ -34,15 +34,18 @@ def main():
epilog="message to be verified follows commands on stdin") epilog="message to be verified follows commands on stdin")
parser.add_argument('--index', metavar='N', type=int, default=0, parser.add_argument('--index', metavar='N', type=int, default=0,
help='Index of DKIM signature header to verify: 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() args=parser.parse_args()
if sys.version_info[0] >= 3: if sys.version_info[0] >= 3:
# Make sys.stdin a binary stream. # Make sys.stdin a binary stream.
sys.stdin = sys.stdin.detach() sys.stdin = sys.stdin.detach()
message = sys.stdin.read() message = sys.stdin.read()
verbose = '-v' in sys.argv verbose = args.verbose
if verbose: if verbose:
import logging import logging
logging.basicConfig(level=logging.DEBUG)
d = dkim.DKIM(message, logger=logging) d = dkim.DKIM(message, logger=logging)
else: else:
d = dkim.DKIM(message) 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( sig_lines = dkim.arc_sign(
self.message, b"test", b"example.com", self.key, b"lists.example.org", timestamp="12345") 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) self.assertEqual(expected_sig, sig_lines)
(cv, res, reason) = dkim.arc_verify(b''.join(sig_lines) + self.message, dnsfunc=self.dnsfunc) (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.message5 = read_test_data("rfc6376.signed.rsa.msg")
self.message6 = read_test_data("test.message.baddomain") self.message6 = read_test_data("test.message.baddomain")
self.message7 = read_test_data("rfc6376.w1258.msg") 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.key = read_test_data("test.private")
self.rfckey = read_test_data("rfc8032_7_1.key") self.rfckey = read_test_data("rfc8032_7_1.key")
@@ -250,6 +251,7 @@ b/mPfjC0QJTocVBq6Za/PlzfV+Py92VaCak19F4WrbVTK5Gg5tW220MCAwEAAQ=="""
d = dkim.DKIM(self.message4) d = dkim.DKIM(self.message4)
res = d.verify(dnsfunc=self.dnsfunc5) res = d.verify(dnsfunc=self.dnsfunc5)
self.assertTrue(res) self.assertTrue(res)
def test_non_utf8(self): def test_non_utf8(self):
# A message with Windows-1258 encoding is signed and verifies. # A message with Windows-1258 encoding is signed and verifies.
for header_algo in (b"simple", b"relaxed"): 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 # As of 1.1.0 this won't verify, but at least we don't crash. FIXME
self.assertFalse(res) 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): def test_catch_bad_key(self):
# Raise correct error for defective public key. # Raise correct error for defective public key.
d = dkim.DKIM(self.message5) 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. code 1.
.SH "USAGE" .SH "USAGE"
usage: dkimverify.py [\-h] [\-\-index N] <message usage: dkimverify.py [\-h] [\-\-index N] [\-\-verbose] <message
optional arguments: optional arguments:
\-h, \-\-help show this help message and exit \-h, \-\-help show this help message and exit
\-\-index N Index of DKIM signature header to verify: default=0 \-\-index N Index of DKIM signature header to verify: default=0
\-\-verbose Emit diagnostic output
.SH "AUTHORS" .SH "AUTHORS"
This version of \fBdkimverify\fR was written by Greg Hewgill <greg@hewgill.com>. 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 os
import sys import sys
version = "1.1.7" version = "1.1.9"
kw = {} # Work-around for lack of 'or' requires in setuptools. kw = {} # Work-around for lack of 'or' requires in setuptools.
try: try: