Compare commits
10 Commits
ab569cc77c
...
2275718e74
| Author | SHA1 | Date | |
|---|---|---|---|
| 2275718e74 | |||
| 21b9410f4f | |||
| 0167ba92ea | |||
| 19303e23d7 | |||
| 31ed6e9055 | |||
| f29f2ba3a7 | |||
| 1ffa2cb090 | |||
| 71f5d118e6 | |||
| 9380655a6e | |||
| ed5931c0c9 |
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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>.
|
||||
|
||||
Reference in New Issue
Block a user