Merge ~spitap/dkimpy:KeyGenerationUnitTest into dkimpy:master

Proposed by Adrien
Status: Merged
Merge reported by: Scott Kitterman
Merged at revision: 0540d3cb17bf4f43ed5a1f9fd8f41f9ef03346b7
Proposed branch: ~spitap/dkimpy:KeyGenerationUnitTest
Merge into: dkimpy:master
Diff against target: 217 lines (+133/-11)
3 files modified
dkim/dknewkey.py (+18/-11)
dkim/tests/__init__.py (+2/-0)
dkim/tests/test_dkim_generate.py (+113/-0)
Reviewer Review Type Date Requested Status
Scott Kitterman Approve
Review via email: mp+427673@code.launchpad.net

Commit message

Key generation unit test

Description of the change

As Scott suggested, this adds a unit test for key generation (mainly check OpenSSL and nacl).

To post a comment you must log in.
Revision history for this message
Adrien (spitap) wrote :

Note : Since openssl only got the '-quiet' argument from v3, I redirected everything to /dev/null. I can add a verbose check to enable openssl verbosity by default.

Revision history for this message
Scott Kitterman (kitterman) wrote :

Looks good.

Thanks,

Scott K

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/dkim/dknewkey.py b/dkim/dknewkey.py
2index 4750619..d189ede 100644
3--- a/dkim/dknewkey.py
4+++ b/dkim/dknewkey.py
5@@ -43,15 +43,17 @@ OPENSSL_BINARY = '/usr/bin/openssl'
6 def eprint(*args, **kwargs):
7 print(*args, file=sys.stderr, **kwargs)
8
9-def GenRSAKeys(private_key_file):
10+def GenRSAKeys(private_key_file, verbose=True):
11 """ Generates a suitable private key. Output is unprotected.
12 You should encrypt your keys.
13 """
14- eprint('generating ' + private_key_file)
15+ if verbose:
16+ eprint('generating ' + private_key_file)
17 subprocess.check_call([OPENSSL_BINARY, 'genrsa', '-out', private_key_file,
18- str(BITS_REQUIRED)])
19+ str(BITS_REQUIRED)],
20+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
21
22-def GenEd25519Keys(private_key_file):
23+def GenEd25519Keys(private_key_file, verbose=True):
24 """Generates a base64 encoded private key for ed25519 DKIM signing.
25 Output is unprotected. You should protect your keys.
26 """
27@@ -59,7 +61,8 @@ def GenEd25519Keys(private_key_file):
28 import nacl.encoding
29 import os
30 skg = nacl.signing.SigningKey(seed=os.urandom(32))
31- eprint('generating ' + private_key_file)
32+ if verbose:
33+ eprint('generating ' + private_key_file)
34 priv_key = skg.generate()
35 with open(private_key_file, 'w') as pkf:
36 pkf.write(priv_key.encode(encoder=nacl.encoding.Base64Encoder).decode("utf-8"))
37@@ -67,13 +70,15 @@ def GenEd25519Keys(private_key_file):
38 os.chmod(private_key_file, 0o600)
39 return(priv_key)
40
41-def ExtractRSADnsPublicKey(private_key_file, dns_file):
42+def ExtractRSADnsPublicKey(private_key_file, dns_file, verbose=True):
43 """ Given a key, extract the bit we should place in DNS.
44 """
45- eprint('extracting ' + private_key_file)
46+ if verbose:
47+ eprint('extracting ' + private_key_file)
48 working_file = tempfile.NamedTemporaryFile(delete=False).name
49 subprocess.check_call([OPENSSL_BINARY, 'rsa', '-in', private_key_file,
50- '-out', working_file, '-pubout', '-outform', 'PEM'])
51+ '-out', working_file, '-pubout', '-outform', 'PEM'],
52+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
53 try:
54 with open(working_file) as wf:
55 y = ''
56@@ -84,17 +89,19 @@ def ExtractRSADnsPublicKey(private_key_file, dns_file):
57 finally:
58 os.unlink(working_file)
59 with open(dns_file, 'w') as dns_fp:
60- eprint('writing ' + dns_file)
61+ if verbose:
62+ eprint('writing ' + dns_file)
63 dns_fp.write("v=DKIM1; k=rsa; h=sha256; p={0}".format(output))
64
65-def ExtractEd25519PublicKey(dns_file, priv_key):
66+def ExtractEd25519PublicKey(dns_file, priv_key, verbose=True):
67 """ Given a ed25519 key, extract the bit we should place in DNS.
68 """
69 import nacl.encoding # Yes, pep-8, but let's not make everyone install nacl
70 pubkey = priv_key.verify_key
71 output = pubkey.encode(encoder=nacl.encoding.Base64Encoder).decode("utf-8")
72 with open(dns_file, 'w') as dns_fp:
73- eprint('writing ' + dns_file)
74+ if verbose:
75+ eprint('writing ' + dns_file)
76 dns_fp.write("v=DKIM1; k=ed25519; p={0}".format(output))
77
78 def main():
79diff --git a/dkim/tests/__init__.py b/dkim/tests/__init__.py
80index 9bc88c8..4fb2926 100644
81--- a/dkim/tests/__init__.py
82+++ b/dkim/tests/__init__.py
83@@ -35,6 +35,7 @@ def test_suite():
84 test_util,
85 test_arc,
86 test_dnsplug,
87+ test_dkim_generate,
88 )
89 modules = [
90 test_canonicalization,
91@@ -46,6 +47,7 @@ def test_suite():
92 test_util,
93 test_arc,
94 test_dnsplug,
95+ test_dkim_generate,
96 ]
97 suites = [x.test_suite() for x in modules]
98 return unittest.TestSuite(suites)
99diff --git a/dkim/tests/test_dkim_generate.py b/dkim/tests/test_dkim_generate.py
100new file mode 100644
101index 0000000..1649ab5
102--- /dev/null
103+++ b/dkim/tests/test_dkim_generate.py
104@@ -0,0 +1,113 @@
105+# This software is provided 'as-is', without any express or implied
106+# warranty. In no event will the author be held liable for any damages
107+# arising from the use of this software.
108+#
109+# Permission is granted to anyone to use this software for any purpose,
110+# including commercial applications, and to alter it and redistribute it
111+# freely, subject to the following restrictions:
112+#
113+# 1. The origin of this software must not be misrepresented; you must not
114+# claim that you wrote the original software. If you use this software
115+# in a product, an acknowledgment in the product documentation would be
116+# appreciated but is not required.
117+# 2. Altered source versions must be plainly marked as such, and must not be
118+# misrepresented as being the original software.
119+# 3. This notice may not be removed or altered from any source distribution.
120+#
121+# Copyright (c) 2011 William Grant <me@williamgrant.id.au>
122+# Copyright (c) 2022 Adrien Precigout <dev@asdrip.fr>
123+
124+import os.path
125+import tempfile
126+import unittest
127+
128+import dkim
129+import dknewkey
130+
131+def read_data(path):
132+ """Get the content of the given test data file."""
133+
134+ with open(path, 'rb') as f:
135+ return f.read()
136+
137+
138+class TestSignAndVerify(unittest.TestCase):
139+ """End-to-end signature and verification tests with a generated key."""
140+
141+ def setUp(self):
142+ message_dir = os.path.join(os.path.dirname(__file__), 'data', "test.message")
143+ self.message = read_data(message_dir)
144+ self.ed25519_dns_key_file = ""
145+ self.rsa_dns_key_file = ""
146+
147+
148+ def test_generate_verifies_new_RSA_key(self):
149+ #Create temporary dir
150+ tmpdir = tempfile.TemporaryDirectory()
151+ keydir = tmpdir.name
152+ rsa_key_file = os.path.join(keydir, "dkim.rsa.key")
153+ self.rsa_dns_key_file = os.path.join(keydir, "dkim.rsa.key.pub.txt")
154+ #Generate a rsa key
155+ dknewkey.GenRSAKeys(rsa_key_file, False)
156+ dknewkey.ExtractRSADnsPublicKey(rsa_key_file, self.rsa_dns_key_file, False)
157+ #Load the key
158+ rsakey = read_data(rsa_key_file)
159+ #Test signature with the newely generated key
160+ for header_algo in (b"simple", b"relaxed"):
161+ for body_algo in (b"simple", b"relaxed"):
162+ sig = dkim.sign(
163+ self.message, b"test", b"example.com", rsakey,
164+ canonicalize=(header_algo, body_algo))
165+ res = dkim.verify(sig + self.message, dnsfunc=self.dnsfuncRSA)
166+ self.assertTrue(res)
167+
168+
169+ def test_generate_verifies_Ed25519_key(self):
170+ #Create temporary dir
171+ tmpdir = tempfile.TemporaryDirectory()
172+ keydir = tmpdir.name
173+ ed25519_key_file = os.path.join(keydir, "dkim.ed25519.key")
174+ self.ed25519_dns_key_file = os.path.join(keydir, "dkim.ed25519.key.pub.txt")
175+ #Generate a ed25519 key
176+ pkt = dknewkey.GenEd25519Keys(ed25519_key_file, False)
177+ dknewkey.ExtractEd25519PublicKey(self.ed25519_dns_key_file, pkt, False)
178+ #Load the key
179+ ed25519key = read_data(ed25519_key_file)
180+ #Test signature with the newely generated key
181+ for header_algo in (b"simple", b"relaxed"):
182+ for body_algo in (b"simple", b"relaxed"):
183+ sig = dkim.sign(
184+ self.message, b"test1", b"example.com", ed25519key,
185+ signature_algorithm=b'ed25519-sha256',
186+ canonicalize=(header_algo, body_algo))
187+ res = dkim.verify(sig + self.message, dnsfunc=self.dnsfuncED25519)
188+ self.assertTrue(res)
189+
190+
191+ def dnsfuncRSA(self, domain, timeout=5):
192+ _dns_responses = {
193+ 'test._domainkey.example.com.': read_data(self.rsa_dns_key_file),
194+ }
195+ try:
196+ domain = domain.decode('ascii')
197+ except UnicodeDecodeError:
198+ return None
199+ self.assertTrue(domain in _dns_responses,domain)
200+ return _dns_responses[domain]
201+
202+ def dnsfuncED25519(self, domain, timeout=5):
203+ _dns_responses = {
204+ 'test1._domainkey.example.com.': read_data(self.ed25519_dns_key_file),
205+ }
206+ try:
207+ domain = domain.decode('ascii')
208+ except UnicodeDecodeError:
209+ return None
210+ self.assertTrue(domain in _dns_responses,domain)
211+ return _dns_responses[domain]
212+
213+
214+
215+def test_suite():
216+ from unittest import TestLoader
217+ return TestLoader().loadTestsFromName(__name__)

Subscribers

People subscribed via source and target branches

to all changes: