Compare commits

...

23 Commits

Author SHA1 Message Date
diskette 2f2a9e17d4 Update README.md 2025-10-16 07:45:35 +00:00
diskette 015d4bacfa Update README.md 2025-10-15 21:02:50 +00:00
Diskette Guy 9503fd60b0 Final commit, I've learned my lesson 2025-10-16 04:01:34 +07:00
Diskette Guy 003a405242 Cheatsheet update: filled in the files of the dkim directory 2025-10-14 23:31:18 +07:00
Diskette Guy f5ae75cd9a Update README.md 2025-10-14 18:37:27 +07:00
Diskette Guy a4fdd73ecb I figured out what canonicalization is 2025-10-14 06:07:54 +07:00
Diskette Guy bb1d5699af Add: canonicalization... whatever that is... 2025-10-14 06:03:32 +07:00
Diskette Guy 1f8aea2173 cheatsheet for loser (me) 2025-10-14 05:58:46 +07:00
Diskette Guy 369cab0047 more test files 2025-10-12 23:47:03 +07:00
Diskette Guy 5b9aaba817 dkim files 2025-10-09 01:49:59 +07:00
Diskette Guy 7aedfbe7c8 change those twos 2025-10-09 01:49:33 +07:00
Diskette Guy 74fe4dc02e add a duplicate test file 2025-10-03 12:55:26 +07:00
diskette 6fbc39565f Update LICENSE 2025-10-02 19:42:33 +00:00
Scott Kitterman 2275718e74 Fix dkimverify verbose option so it works and add documentation, thanks to Uwe Kleine-König for the patch (Debian: #1075791), bump version to 1.1.9 2024-07-05 09:42:26 -04:00
Scott Kitterman 21b9410f4f Correctly handle verification of signatures without t= (timestamp) and with x= (expiration); both are optional (LP: 2071892) 2024-07-04 18:09:50 -04:00
Scott Kitterman 0167ba92ea Bump version to 1.1.8 2024-07-04 18:05:54 -04:00
Scott Kitterman 19303e23d7 Add test case verifying no t= (timestamp) with x= (expiration) in signature 2024-07-04 18:03:50 -04:00
Scott Kitterman 31ed6e9055 Fix README.md typo 2024-06-23 18:04:06 -04:00
Scott Kitterman f29f2ba3a7 Release 1.1.7 2024-06-23 17:57:39 -04:00
Scott Kitterman 1ffa2cb090 Correct signature in ARC-Seal on LF as linesep (LP: #2052720) - Thanks to Nikolay Vizovitin for the report and the fix 2024-06-23 17:26:31 -04:00
Scott Kitterman 71f5d118e6 - Correct line separtor after AAR header field (LP: #2049018) - Thanks to Nikolay Vizovitin for the report and the fix 2024-06-23 17:16:44 -04:00
Scott Kitterman 9380655a6e 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 2024-06-23 17:06:31 -04:00
Scott Kitterman ed5931c0c9 Fix error in validate_signature_fields which prevented signature expiration from being properly evaluated (LP: #2068937) 2024-06-23 16:34:58 -04:00
17 changed files with 505 additions and 30 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
+85
View File
@@ -0,0 +1,85 @@
Welcome to this plaintext file.
This file is strictly licensed as CC0, public domain - only this file though.
I'm Uea-angkun "Diskette" Khunpradith (diskette@unix.in.th), the author of this file.
This file exists because I'm a self-taught ametuer programmer and I wanted to keep notes of the things that this software does. And I do not want to use generative AI, as it does not help or teach me in anyway. It's just a cheatsheet of doing something.
This is what Stallman would've wanted... a free software for education....
Flags:
[IDK] : Will return later and figure out how it works
[NSURE]: Not sure, under the assumption of guessing how it works
[IK] : The author seemed to have an understanding of this
[$TITLE] : the title of the file
[WONDR] : Wonder if I can improve this...
# Personal notes:
Python's __main__ and __init__
As far as I know, __init__ is the initialization script, just like Unix and alike's /etc/init.rc script. It's required to make it so that it's a package.
The double underscores is for signifying that it's a global variable.
b before string, for example b'relaxed' is a way to say that it's a binary string
-----START DKIM DIRECTORY-----
[__init__]
__init__ in this case, import the libraries required for dkim magic. And tests authres, the dkim magic library, and nacl (sodium) library for ed25519 signing.
dkim.canonicalization is quite literally, ../dkim/canonicalization/
[IDK]
There's a variable named "sig" in which I don't know
[arcsign.py]
This is for ARC... we don't use that YET
[arcverify.py]
Same goes for that too
[asn1.py]
cryptography interface language stuff
[asyncsupport.py]
asynchronous support. quite literally again..
[canonicalization.py]
Canonicalization, in computer science is a process for converting data that has more than one possible representation into a standard "canonical" form --- thanks wikipedia, CC BY-SA 4.0
imports regular expression module
unused (potential future code) exception
lots of regular expression codes..
[WONDR]
I wonder if I can fit my U-label convertor here...
[crypto.py]
quite literally cryptography stuff with private key and public key
[dkimsign.py]
this does dkim signing stuff
[dkimverify.py]
this does dkim verifying stuff
[dknewkey.py]
this generates a diffie-hellman private and public keys
[dnsplug.py]
this gets the DNS record
[util.py]
I'm not sure what this does yet. From the looks of it, it's just a logger and praser for tag value
-----START TEST DIRECTORY-----
-----START ROOT DIRECTORY-----
+15 -3
View File
@@ -1,3 +1,15 @@
dkimpy-smtputf8 an RFC 8616-compliant fork of dkimpy.
This fork adds support for internationalized email address (U-labels converter) and a cheatsheet note for learning
NOTE: For SMTPUTF8 support, make sure you use A-label (punycoded) domain name instead. Otherwise it will not work, per RFC 8616
**Another note: The actual repository that's responsible for UTF-8 converter is at** https://gitskette.dailitation.xyz/DandelionNStuff/dkimpy-milter-smtputf8
As this python module do not actually have the convertor, rather, it's the backend of the milter.
# Original README
dkimpy - DKIM (DomainKeys Identified Mail) dkimpy - DKIM (DomainKeys Identified Mail)
https://launchpad.net/dkimpy/ https://launchpad.net/dkimpy/
@@ -13,7 +25,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 +36,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.
+20 -18
View File
@@ -30,6 +30,9 @@
# Copyright (c) 2017 Valimail Inc # Copyright (c) 2017 Valimail Inc
# Contact: Gene Shuman <gene@valimail.com> # Contact: Gene Shuman <gene@valimail.com>
# #
# This has been modified from the original software.
# Copyright (c) 2025 dailitation.xyz
# Contact: Uea-angkun Khunpradith <diskette@dailitation.xyz>
import base64 import base64
@@ -298,6 +301,7 @@ def validate_signature_fields(sig, mandatory_fields=[b'v', b'a', b'b', b'bh', b'
if b'cv' in sig and sig[b'cv'] not in (CV_Pass, CV_Fail, CV_None): if b'cv' in sig and sig[b'cv'] not in (CV_Pass, CV_Fail, CV_None):
raise ValidationError("cv= value is not valid (%s)" % sig[b'cv']) raise ValidationError("cv= value is not valid (%s)" % sig[b'cv'])
# Somehow convert it to UTF8 before going to this?
# Limit domain validation to ASCII domains because too hard # Limit domain validation to ASCII domains because too hard
try: try:
str(sig[b'd'], 'ascii') str(sig[b'd'], 'ascii')
@@ -308,6 +312,7 @@ def validate_signature_fields(sig, mandatory_fields=[b'v', b'a', b'b', b'bh', b'
# Not an ASCII domain # Not an ASCII domain
pass pass
# Nasty hack to support both str and bytes... check for both the # Nasty hack to support both str and bytes... check for both the
# character and integer values. # character and integer values.
if not arc and b'i' in sig and ( if not arc and b'i' in sig and (
@@ -331,6 +336,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 +352,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 +1059,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 +1125,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 +1159,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 +1188,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
@@ -1,7 +1,7 @@
import unittest import unittest
import doctest import doctest
import dkim import dkim
from tests import test_suite from dkim.tests import test_suite
doctest.testmod(dkim) doctest.testmod(dkim)
unittest.TextTestRunner().run(test_suite()) unittest.TextTestRunner().run(test_suite())
+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)
+2
View File
@@ -36,6 +36,7 @@ def test_suite():
test_arc, test_arc,
test_dnsplug, test_dnsplug,
test_dkim_generate, test_dkim_generate,
test_dkim_utf8,
) )
modules = [ modules = [
test_canonicalization, test_canonicalization,
@@ -48,6 +49,7 @@ def test_suite():
test_arc, test_arc,
test_dnsplug, test_dnsplug,
test_dkim_generate, test_dkim_generate,
test_dkim_utf8,
] ]
suites = [x.test_suite() for x in modules] suites = [x.test_suite() for x in modules]
return unittest.TestSuite(suites) return unittest.TestSuite(suites)
+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
View File
@@ -0,0 +1 @@
fL+5V9EquCZAovKik3pA6Lk9zwCzoEtjIuIqK9ZXHHA=
+9
View File
@@ -0,0 +1,9 @@
Authentication-Results: lists.example.org; arc=none; spf=pass smtp.mfrom=jqd@d1.example; dkim=pass (1024-bit key) header.i=@d1.example; dmarc=pass
Received: from localhost
Message-ID: <example@example.com>
Date: Mon, 01 Jan 2011 01:02:03 +0400
From: Test User <test@example.com>
To: somebody@example.com
Subject: Testing
This is a test message.
+14
View File
@@ -0,0 +1,14 @@
DKIM-Signature: v=1; a=ed25519-sha256; q=dns/txt; c=relaxed/relaxed; d=test.ex
; s=sed; h=From:To:Subject; bh=/Ab0giHZitYQbDhFszoqQRUkgqueaX9zatJttIU/plc=;
b=5fhyD3EILDrnL4DnkD4hDaeis7+GSzL9GMHrhIDZJjuJ00WD5iI8SQ1q9rDfzFL/Kdw0VIyB4R
Dq0a4H6HI+Bw==;
Received: from jgh by myhost.test.ex with local (Exim x.yz)
envelope-from <jgh@myhost.test.ex>)
1dtXln-0000YP-Hb
a@test.ex; Sun, 17 Sep 2017 12:29:51 +0100
From: nobody@example.com
Message-Id: <E1dtXln-0000YP-Hb@myhost.test.ex>
Sender: CALLER_NAME <jgh@myhost.test.ex>
Date: Sun, 17 Sep 2017 12:29:51 +0100
content
+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)
+298
View File
@@ -0,0 +1,298 @@
# This software is provided 'as-is', without any express or implied
# warranty. In no event will the author be held liable for any damages
# arising from the use of this software.
#
# Permission is granted to anyone to use this software for any purpose,
# including commercial applications, and to alter it and redistribute it
# freely, subject to the following restrictions:
#
# 1. The origin of this software must not be misrepresented; you must not
# claim that you wrote the original software. If you use this software
# in a product, an acknowledgment in the product documentation would be
# appreciated but is not required.
# 2. Altered source versions must be plainly marked as such, and must not be
# misrepresented as being the original software.
# 3. This notice may not be removed or altered from any source distribution.
#
# Copyright (c) 2011 William Grant <me@williamgrant.id.au>
# Copyright (c) 2018 Scott Kitterman <scott@kitterman.com>
import email
import os.path
import unittest
import time
import dkim
def read_test_data(filename):
"""Get the content of the given test data file.
The files live in dkim/tests/data.
"""
path = os.path.join(os.path.dirname(__file__), 'data', filename)
with open(path, 'rb') as f:
return f.read()
class TestFold(unittest.TestCase):
def test_short_line(self):
self.assertEqual(
b"foo", dkim.fold(b"foo"))
def test_long_line(self):
# The function is terribly broken, not passing even this simple
# test.
self.assertEqual(
b"foo" * 24 + b"\r\n foo", dkim.fold(b"foo" * 25))
class TestSignAndVerify(unittest.TestCase):
"""End-to-end signature and verification tests."""
def setUp(self):
self.message = read_test_data("utf8test.msg")
self.message2 = read_test_data("utf8test2.msg")
self.message3 = read_test_data("rfc6376.msg")
self.message4 = read_test_data("rfc6376.signed.msg")
self.key = read_test_data("utf8.key")
self.rfckey = read_test_data("rfc8032_7_1.key")
def dnsfunc(self, domain, timeout=5):
sample_dns = """\
k=ed25519; \
p=yi50DjK5O9pqbFpNHklsv9lqaS0ArSYu02qp1S0DW1Y="""
_dns_responses = {
'example._domainkey.canonical.com.': sample_dns,
'test._domainkey.example.net.': """v=DKIM1; k=ed25519; \
p=yi50DjK5O9pqbFpNHklsv9lqaS0ArSYu02qp1S0DW1Y=""",
'sed._domainkey.test.ex.': read_test_data("eximtest.dns"),
'brisbane._domainkey.football.example.com.': """v=DKIM1; k=ed25519; \
p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo="""
}
try:
domain = domain.decode('ascii')
except UnicodeDecodeError:
return None
self.assertTrue(domain in _dns_responses,domain)
return _dns_responses[domain]
def test_verifies(self):
# A message verifies after being signed.
for header_algo in (b"simple", b"relaxed"):
for body_algo in (b"simple", b"relaxed"):
sig = dkim.sign(
self.message, b"test", b"example.net", self.key,
canonicalize=(header_algo, body_algo), signature_algorithm=b'ed25519-sha256')
res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc)
self.assertTrue(res)
def test_rfc8032_verifies(self):
# A message using RFC 8032 sample keys verifies after being signed.
for header_algo in (b"simple", b"relaxed"):
for body_algo in (b"simple", b"relaxed"):
sig = dkim.sign(
self.message3, b"brisbane", b"football.example.com", self.rfckey,
canonicalize=(header_algo, body_algo), signature_algorithm=b'ed25519-sha256')
res = dkim.verify(sig + self.message3, dnsfunc=self.dnsfunc)
self.assertTrue(res)
def test_rfc8032_previous_verifies(self):
# A message previously signed using RFC 8032 sample keys verifies after being signed.
for header_algo in (b"simple", b"relaxed"):
for body_algo in (b"simple", b"relaxed"):
sig = dkim.sign(
self.message3, b"brisbane", b"football.example.com", self.rfckey,
canonicalize=(header_algo, body_algo), signature_algorithm=b'ed25519-sha256')
d = dkim.DKIM(self.message4)
res = d.verify(dnsfunc=self.dnsfunc)
self.assertTrue(res)
def test_simple_signature(self):
# A message verifies after being signed with SHOULD headers
for header_algo in (b"simple", b"relaxed"):
for body_algo in (b"simple", b"relaxed"):
sig = dkim.sign(
self.message, b"test", b"example.net", self.key,
canonicalize=(header_algo, body_algo),
include_headers=(b'from',) + dkim.DKIM.SHOULD,
signature_algorithm=b'ed25519-sha256')
res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc)
self.assertTrue(res)
def test_verify_third_party(self):
# Message signed by prototype Exim implementation
res = dkim.verify(self.message2, dnsfunc=self.dnsfunc)
self.assertTrue(res)
def test_add_body_length(self):
sig = dkim.sign(
self.message, b"test", b"example.net", self.key, length=True,
signature_algorithm=b'ed25519-sha256')
msg = email.message_from_string(self.message.decode('utf-8'))
self.assertIn('; l=%s' % len(msg.get_payload() + '\n'), sig.decode('utf-8'))
res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc)
self.assertTrue(res)
def test_altered_body_fails(self):
# An altered body fails verification.
for header_algo in (b"simple", b"relaxed"):
for body_algo in (b"simple", b"relaxed"):
sig = dkim.sign(
self.message, b"test", b"example.net", self.key,
signature_algorithm=b'ed25519-sha256')
res = dkim.verify(
sig + self.message + b"foo", dnsfunc=self.dnsfunc)
self.assertFalse(res)
def test_badly_encoded_domain_fails(self):
# Domains should be ASCII. Bad ASCII causes verification to fail.
sig = dkim.sign(self.message, b"test", b"example.net\xe9", self.key,
signature_algorithm=b'ed25519-sha256')
res = dkim.verify(sig + self.message, dnsfunc=self.dnsfunc)
self.assertFalse(res)
def test_dkim_signature_canonicalization(self):
# <https://bugs.launchpad.net/ubuntu/+source/pydkim/+bug/587783>
# Relaxed-mode header signing is wrong
# <https://bugs.launchpad.net/dkimpy/+bug/939128>
# Simple-mode signature header verification is wrong
# (should ignore FWS anywhere in signature tag: b=)
sample_msg = b"""\
From: mbp@canonical.com
To: scottk@example.net
Subject: this is my
test message
""".replace(b'\n', b'\r\n')
sample_privkey = b"""\
fL+5V9EquCZAovKik3pA6Lk9zwCzoEtjIuIqK9ZXHHA=\
"""
sample_pubkey = """\
yi50DjK5O9pqbFpNHklsv9lqaS0ArSYu02qp1S0DW1Y=\
"""
for header_mode in [dkim.Relaxed, dkim.Simple]:
dkim_header = dkim.sign(sample_msg, b'example', b'canonical.com',
sample_privkey, canonicalize=(header_mode, dkim.Relaxed),
signature_algorithm=b'ed25519-sha256')
# Folding dkim_header affects b= tag only, since dkim.sign folds
# sig_value with empty b= before hashing, and then appends the
# signature. So folding dkim_header again adds FWS to
# the b= tag only. This should be ignored even with
# simple canonicalization.
# http://tools.ietf.org/html/rfc4871#section-3.5
signed = dkim.fold(dkim_header) + sample_msg
result = dkim.verify(signed,dnsfunc=self.dnsfunc)
self.assertTrue(result)
dkim_header = dkim.fold(dkim_header)
# use a tab for last fold to test tab in FWS bug
pos = dkim_header.rindex(b'\r\n ')
dkim_header = dkim_header[:pos]+b'\r\n\t'+dkim_header[pos+3:]
result = dkim.verify(dkim_header + sample_msg,
dnsfunc=self.dnsfunc)
self.assertTrue(result)
def test_extra_headers(self):
# <https://bugs.launchpad.net/dkimpy/+bug/737311>
# extra headers above From caused failure
#message = read_test_data("test_extra.message")
message = read_test_data("message.mbox")
for header_algo in (b"simple", b"relaxed"):
for body_algo in (b"simple", b"relaxed"):
d = dkim.DKIM(message)
# bug requires a repeated header to manifest
d.should_not_sign.remove(b'received')
sig = d.sign(b"test", b"example.net", self.key,
signature_algorithm=b'ed25519-sha256',
include_headers=d.all_sign_headers(),
canonicalize=(header_algo, body_algo))
dv = dkim.DKIM(sig + message)
res = dv.verify(dnsfunc=self.dnsfunc)
self.assertEqual(d.include_headers,dv.include_headers)
s = dkim.select_headers(d.headers,d.include_headers)
sv = dkim.select_headers(dv.headers,dv.include_headers)
self.assertEqual(s,sv)
self.assertTrue(res)
def test_multiple_from_fails(self):
# <https://bugs.launchpad.net/dkimpy/+bug/644046>
# additional From header fields should cause verify failure
hfrom = b'From: "Resident Evil" <sales@spammer.com>\r\n'
h,b = self.message.split(b'\n\n',1)
for header_algo in (b"simple", b"relaxed"):
for body_algo in (b"simple", b"relaxed"):
sig = dkim.sign(
self.message, b"test", b"example.net", self.key,
signature_algorithm=b'ed25519-sha256')
# adding an unknown header still verifies
h1 = h+b'\r\n'+b'X-Foo: bar'
message = b'\n\n'.join((h1,b))
res = dkim.verify(sig+message, dnsfunc=self.dnsfunc)
self.assertTrue(res)
# adding extra from at end should not verify
h1 = h+b'\r\n'+hfrom.strip()
message = b'\n\n'.join((h1,b))
res = dkim.verify(sig+message, dnsfunc=self.dnsfunc)
self.assertFalse(res)
# add extra from in front should not verify either
h1 = hfrom+h
message = b'\n\n'.join((h1,b))
res = dkim.verify(sig+message, dnsfunc=self.dnsfunc)
self.assertFalse(res)
def test_no_from_fails(self):
# Body From is mandatory to be in the message and mandatory to sign
sigerror = False
sig = ''
message = read_test_data('test_nofrom.message')
selector = 'test'
domain = 'example.net'
identity = None
try:
sig = dkim.sign(message, selector, domain,
read_test_data('ed25519test.key'), identity = identity,
signature_algorithm=b'ed25519-sha256')
except dkim.ParameterError as x:
sigerror = True
self.assertTrue(sigerror)
def test_validate_signature_fields(self):
sig = {b'v': b'1',
b'a': b'ed25519-sha256',
b'b': b'K/UUOt8lCtgjp3kSTogqBm9lY1Yax/NwZ+bKm39/WKzo5KYe3L/6RoIA/0oiDX4kO\n \t Qut49HCV6ZUe6dY9V5qWBwLanRs1sCnObaOGMpFfs8tU4TWpDSVXaNZAqn15XVW0WH\n \t EzOzUfVuatpa1kF4voIgSbmZHR1vN3WpRtcTBe/I=',
b'bh': b'n0HUwGCP28PkesXBPH82Kboy8LhNFWU9zUISIpAez7M=',
b'c': b'simple/simple',
b'd': b'kitterman.com',
b'i': b'scott@Kitterman.com',
b'h': b'From:To:Subject:Date:Cc:MIME-Version:Content-Type:\n \t Content-Transfer-Encoding:Message-Id',
b's': b'2007-00',
b't': b'1299525798'}
dkim.validate_signature_fields(sig)
# try new version
sigVer = sig.copy()
sigVer[b'v'] = 2
self.assertRaises(dkim.ValidationError, dkim.validate_signature_fields, sigVer)
# try with x
sigX = sig.copy()
sigX[b'x'] = b'1399525798'
dkim.validate_signature_fields(sig)
# try with late t
sigX[b't'] = b'1400000000'
self.assertRaises(dkim.ValidationError, dkim.validate_signature_fields, sigX)
# try without t
now = int(time.time())
sigX[b'x'] = str(now+400000).encode('ascii')
dkim.validate_signature_fields(sigX)
# try when expired a day ago
sigX[b'x'] = str(now - 24*3600).encode('ascii')
self.assertRaises(dkim.ValidationError, dkim.validate_signature_fields, sigX)
def test_suite():
from unittest import TestLoader
return TestLoader().loadTestsFromName(__name__)
+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: