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
diff --git a/dkim/dknewkey.py b/dkim/dknewkey.py
index 4750619..d189ede 100644
--- a/dkim/dknewkey.py
+++ b/dkim/dknewkey.py
@@ -43,15 +43,17 @@ OPENSSL_BINARY = '/usr/bin/openssl'
43def eprint(*args, **kwargs):43def eprint(*args, **kwargs):
44 print(*args, file=sys.stderr, **kwargs)44 print(*args, file=sys.stderr, **kwargs)
4545
46def GenRSAKeys(private_key_file):46def GenRSAKeys(private_key_file, verbose=True):
47 """ Generates a suitable private key. Output is unprotected.47 """ Generates a suitable private key. Output is unprotected.
48 You should encrypt your keys.48 You should encrypt your keys.
49 """49 """
50 eprint('generating ' + private_key_file)50 if verbose:
51 eprint('generating ' + private_key_file)
51 subprocess.check_call([OPENSSL_BINARY, 'genrsa', '-out', private_key_file,52 subprocess.check_call([OPENSSL_BINARY, 'genrsa', '-out', private_key_file,
52 str(BITS_REQUIRED)])53 str(BITS_REQUIRED)],
54 stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
5355
54def GenEd25519Keys(private_key_file):56def GenEd25519Keys(private_key_file, verbose=True):
55 """Generates a base64 encoded private key for ed25519 DKIM signing.57 """Generates a base64 encoded private key for ed25519 DKIM signing.
56 Output is unprotected. You should protect your keys.58 Output is unprotected. You should protect your keys.
57 """59 """
@@ -59,7 +61,8 @@ def GenEd25519Keys(private_key_file):
59 import nacl.encoding61 import nacl.encoding
60 import os62 import os
61 skg = nacl.signing.SigningKey(seed=os.urandom(32))63 skg = nacl.signing.SigningKey(seed=os.urandom(32))
62 eprint('generating ' + private_key_file)64 if verbose:
65 eprint('generating ' + private_key_file)
63 priv_key = skg.generate()66 priv_key = skg.generate()
64 with open(private_key_file, 'w') as pkf:67 with open(private_key_file, 'w') as pkf:
65 pkf.write(priv_key.encode(encoder=nacl.encoding.Base64Encoder).decode("utf-8"))68 pkf.write(priv_key.encode(encoder=nacl.encoding.Base64Encoder).decode("utf-8"))
@@ -67,13 +70,15 @@ def GenEd25519Keys(private_key_file):
67 os.chmod(private_key_file, 0o600)70 os.chmod(private_key_file, 0o600)
68 return(priv_key)71 return(priv_key)
6972
70def ExtractRSADnsPublicKey(private_key_file, dns_file):73def ExtractRSADnsPublicKey(private_key_file, dns_file, verbose=True):
71 """ Given a key, extract the bit we should place in DNS.74 """ Given a key, extract the bit we should place in DNS.
72 """75 """
73 eprint('extracting ' + private_key_file)76 if verbose:
77 eprint('extracting ' + private_key_file)
74 working_file = tempfile.NamedTemporaryFile(delete=False).name78 working_file = tempfile.NamedTemporaryFile(delete=False).name
75 subprocess.check_call([OPENSSL_BINARY, 'rsa', '-in', private_key_file,79 subprocess.check_call([OPENSSL_BINARY, 'rsa', '-in', private_key_file,
76 '-out', working_file, '-pubout', '-outform', 'PEM'])80 '-out', working_file, '-pubout', '-outform', 'PEM'],
81 stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
77 try:82 try:
78 with open(working_file) as wf:83 with open(working_file) as wf:
79 y = ''84 y = ''
@@ -84,17 +89,19 @@ def ExtractRSADnsPublicKey(private_key_file, dns_file):
84 finally:89 finally:
85 os.unlink(working_file)90 os.unlink(working_file)
86 with open(dns_file, 'w') as dns_fp:91 with open(dns_file, 'w') as dns_fp:
87 eprint('writing ' + dns_file)92 if verbose:
93 eprint('writing ' + dns_file)
88 dns_fp.write("v=DKIM1; k=rsa; h=sha256; p={0}".format(output))94 dns_fp.write("v=DKIM1; k=rsa; h=sha256; p={0}".format(output))
8995
90def ExtractEd25519PublicKey(dns_file, priv_key):96def ExtractEd25519PublicKey(dns_file, priv_key, verbose=True):
91 """ Given a ed25519 key, extract the bit we should place in DNS.97 """ Given a ed25519 key, extract the bit we should place in DNS.
92 """98 """
93 import nacl.encoding # Yes, pep-8, but let's not make everyone install nacl99 import nacl.encoding # Yes, pep-8, but let's not make everyone install nacl
94 pubkey = priv_key.verify_key100 pubkey = priv_key.verify_key
95 output = pubkey.encode(encoder=nacl.encoding.Base64Encoder).decode("utf-8")101 output = pubkey.encode(encoder=nacl.encoding.Base64Encoder).decode("utf-8")
96 with open(dns_file, 'w') as dns_fp:102 with open(dns_file, 'w') as dns_fp:
97 eprint('writing ' + dns_file)103 if verbose:
104 eprint('writing ' + dns_file)
98 dns_fp.write("v=DKIM1; k=ed25519; p={0}".format(output))105 dns_fp.write("v=DKIM1; k=ed25519; p={0}".format(output))
99106
100def main():107def main():
diff --git a/dkim/tests/__init__.py b/dkim/tests/__init__.py
index 9bc88c8..4fb2926 100644
--- a/dkim/tests/__init__.py
+++ b/dkim/tests/__init__.py
@@ -35,6 +35,7 @@ def test_suite():
35 test_util,35 test_util,
36 test_arc,36 test_arc,
37 test_dnsplug,37 test_dnsplug,
38 test_dkim_generate,
38 )39 )
39 modules = [40 modules = [
40 test_canonicalization,41 test_canonicalization,
@@ -46,6 +47,7 @@ def test_suite():
46 test_util,47 test_util,
47 test_arc,48 test_arc,
48 test_dnsplug,49 test_dnsplug,
50 test_dkim_generate,
49 ]51 ]
50 suites = [x.test_suite() for x in modules]52 suites = [x.test_suite() for x in modules]
51 return unittest.TestSuite(suites)53 return unittest.TestSuite(suites)
diff --git a/dkim/tests/test_dkim_generate.py b/dkim/tests/test_dkim_generate.py
52new file mode 10064454new file mode 100644
index 0000000..1649ab5
--- /dev/null
+++ b/dkim/tests/test_dkim_generate.py
@@ -0,0 +1,113 @@
1# This software is provided 'as-is', without any express or implied
2# warranty. In no event will the author be held liable for any damages
3# arising from the use of this software.
4#
5# Permission is granted to anyone to use this software for any purpose,
6# including commercial applications, and to alter it and redistribute it
7# freely, subject to the following restrictions:
8#
9# 1. The origin of this software must not be misrepresented; you must not
10# claim that you wrote the original software. If you use this software
11# in a product, an acknowledgment in the product documentation would be
12# appreciated but is not required.
13# 2. Altered source versions must be plainly marked as such, and must not be
14# misrepresented as being the original software.
15# 3. This notice may not be removed or altered from any source distribution.
16#
17# Copyright (c) 2011 William Grant <me@williamgrant.id.au>
18# Copyright (c) 2022 Adrien Precigout <dev@asdrip.fr>
19
20import os.path
21import tempfile
22import unittest
23
24import dkim
25import dknewkey
26
27def read_data(path):
28 """Get the content of the given test data file."""
29
30 with open(path, 'rb') as f:
31 return f.read()
32
33
34class TestSignAndVerify(unittest.TestCase):
35 """End-to-end signature and verification tests with a generated key."""
36
37 def setUp(self):
38 message_dir = os.path.join(os.path.dirname(__file__), 'data', "test.message")
39 self.message = read_data(message_dir)
40 self.ed25519_dns_key_file = ""
41 self.rsa_dns_key_file = ""
42
43
44 def test_generate_verifies_new_RSA_key(self):
45 #Create temporary dir
46 tmpdir = tempfile.TemporaryDirectory()
47 keydir = tmpdir.name
48 rsa_key_file = os.path.join(keydir, "dkim.rsa.key")
49 self.rsa_dns_key_file = os.path.join(keydir, "dkim.rsa.key.pub.txt")
50 #Generate a rsa key
51 dknewkey.GenRSAKeys(rsa_key_file, False)
52 dknewkey.ExtractRSADnsPublicKey(rsa_key_file, self.rsa_dns_key_file, False)
53 #Load the key
54 rsakey = read_data(rsa_key_file)
55 #Test signature with the newely generated key
56 for header_algo in (b"simple", b"relaxed"):
57 for body_algo in (b"simple", b"relaxed"):
58 sig = dkim.sign(
59 self.message, b"test", b"example.com", rsakey,
60 canonicalize=(header_algo, body_algo))
61 res = dkim.verify(sig + self.message, dnsfunc=self.dnsfuncRSA)
62 self.assertTrue(res)
63
64
65 def test_generate_verifies_Ed25519_key(self):
66 #Create temporary dir
67 tmpdir = tempfile.TemporaryDirectory()
68 keydir = tmpdir.name
69 ed25519_key_file = os.path.join(keydir, "dkim.ed25519.key")
70 self.ed25519_dns_key_file = os.path.join(keydir, "dkim.ed25519.key.pub.txt")
71 #Generate a ed25519 key
72 pkt = dknewkey.GenEd25519Keys(ed25519_key_file, False)
73 dknewkey.ExtractEd25519PublicKey(self.ed25519_dns_key_file, pkt, False)
74 #Load the key
75 ed25519key = read_data(ed25519_key_file)
76 #Test signature with the newely generated key
77 for header_algo in (b"simple", b"relaxed"):
78 for body_algo in (b"simple", b"relaxed"):
79 sig = dkim.sign(
80 self.message, b"test1", b"example.com", ed25519key,
81 signature_algorithm=b'ed25519-sha256',
82 canonicalize=(header_algo, body_algo))
83 res = dkim.verify(sig + self.message, dnsfunc=self.dnsfuncED25519)
84 self.assertTrue(res)
85
86
87 def dnsfuncRSA(self, domain, timeout=5):
88 _dns_responses = {
89 'test._domainkey.example.com.': read_data(self.rsa_dns_key_file),
90 }
91 try:
92 domain = domain.decode('ascii')
93 except UnicodeDecodeError:
94 return None
95 self.assertTrue(domain in _dns_responses,domain)
96 return _dns_responses[domain]
97
98 def dnsfuncED25519(self, domain, timeout=5):
99 _dns_responses = {
100 'test1._domainkey.example.com.': read_data(self.ed25519_dns_key_file),
101 }
102 try:
103 domain = domain.decode('ascii')
104 except UnicodeDecodeError:
105 return None
106 self.assertTrue(domain in _dns_responses,domain)
107 return _dns_responses[domain]
108
109
110
111def test_suite():
112 from unittest import TestLoader
113 return TestLoader().loadTestsFromName(__name__)

Subscribers

People subscribed via source and target branches

to all changes: