From fab181ae34a01428869eb6099be0f5645373ffdb Mon Sep 17 00:00:00 2001 From: Spitap Date: Mon, 1 Aug 2022 09:41:59 +0200 Subject: [PATCH] Add key generation unit test --- dkim/dknewkey.py | 29 +++++--- dkim/tests/__init__.py | 2 + dkim/tests/test_dkim_generate.py | 115 +++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 dkim/tests/test_dkim_generate.py 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' def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) -def GenRSAKeys(private_key_file): +def GenRSAKeys(private_key_file, verbose=True): """ Generates a suitable private key. Output is unprotected. You should encrypt your keys. """ - eprint('generating ' + private_key_file) + if verbose: + eprint('generating ' + private_key_file) subprocess.check_call([OPENSSL_BINARY, 'genrsa', '-out', private_key_file, - str(BITS_REQUIRED)]) + str(BITS_REQUIRED)], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) -def GenEd25519Keys(private_key_file): +def GenEd25519Keys(private_key_file, verbose=True): """Generates a base64 encoded private key for ed25519 DKIM signing. Output is unprotected. You should protect your keys. """ @@ -59,7 +61,8 @@ def GenEd25519Keys(private_key_file): import nacl.encoding import os skg = nacl.signing.SigningKey(seed=os.urandom(32)) - eprint('generating ' + private_key_file) + if verbose: + eprint('generating ' + private_key_file) priv_key = skg.generate() with open(private_key_file, 'w') as pkf: pkf.write(priv_key.encode(encoder=nacl.encoding.Base64Encoder).decode("utf-8")) @@ -67,13 +70,15 @@ def GenEd25519Keys(private_key_file): os.chmod(private_key_file, 0o600) return(priv_key) -def ExtractRSADnsPublicKey(private_key_file, dns_file): +def ExtractRSADnsPublicKey(private_key_file, dns_file, verbose=True): """ Given a key, extract the bit we should place in DNS. """ - eprint('extracting ' + private_key_file) + if verbose: + eprint('extracting ' + private_key_file) working_file = tempfile.NamedTemporaryFile(delete=False).name subprocess.check_call([OPENSSL_BINARY, 'rsa', '-in', private_key_file, - '-out', working_file, '-pubout', '-outform', 'PEM']) + '-out', working_file, '-pubout', '-outform', 'PEM'], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) try: with open(working_file) as wf: y = '' @@ -84,17 +89,19 @@ def ExtractRSADnsPublicKey(private_key_file, dns_file): finally: os.unlink(working_file) with open(dns_file, 'w') as dns_fp: - eprint('writing ' + dns_file) + if verbose: + eprint('writing ' + dns_file) dns_fp.write("v=DKIM1; k=rsa; h=sha256; p={0}".format(output)) -def ExtractEd25519PublicKey(dns_file, priv_key): +def ExtractEd25519PublicKey(dns_file, priv_key, verbose=True): """ Given a ed25519 key, extract the bit we should place in DNS. """ import nacl.encoding # Yes, pep-8, but let's not make everyone install nacl pubkey = priv_key.verify_key output = pubkey.encode(encoder=nacl.encoding.Base64Encoder).decode("utf-8") with open(dns_file, 'w') as dns_fp: - eprint('writing ' + dns_file) + if verbose: + eprint('writing ' + dns_file) dns_fp.write("v=DKIM1; k=ed25519; p={0}".format(output)) def 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(): test_util, test_arc, test_dnsplug, + test_dkim_generate, ) modules = [ test_canonicalization, @@ -46,6 +47,7 @@ def test_suite(): test_util, test_arc, test_dnsplug, + test_dkim_generate, ] suites = [x.test_suite() for x in modules] return unittest.TestSuite(suites) diff --git a/dkim/tests/test_dkim_generate.py b/dkim/tests/test_dkim_generate.py new file mode 100644 index 0000000..855928a --- /dev/null +++ b/dkim/tests/test_dkim_generate.py @@ -0,0 +1,115 @@ +# 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 +# Copyright (c) 2022 Adrien Precigout + +import os.path +import tempfile +import unittest + +import dkim +import dknewkey + +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 TestSignAndVerify(unittest.TestCase): + """End-to-end signature and verification tests with a generated key.""" + + def setUp(self): + self.message = read_test_data("test.message") + self.ed25519_dns_key_file = "" + self.rsa_dns_key_file = "" + + + def test_generate_verifies_new_RSA_key(self): + #Create temporary dir + tmpdir = tempfile.TemporaryDirectory() + keydir = tmpdir.name + rsa_key_file = os.path.join(keydir, "dkim.rsa.key") + self.rsa_dns_key_file = os.path.join(keydir, "dkim.rsa.key.pub.txt") + #Generate a rsa key + dknewkey.GenRSAKeys(rsa_key_file, False) + dknewkey.ExtractRSADnsPublicKey(rsa_key_file, self.rsa_dns_key_file, False) + #Load the key + rsakey = read_test_data(rsa_key_file) + #Test signature with the newely generated key + 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.com", rsakey, + canonicalize=(header_algo, body_algo)) + res = dkim.verify(sig + self.message, dnsfunc=self.dnsfuncRSA) + self.assertTrue(res) + + + def test_generate_verifies_Ed25519_key(self): + #Create temporary dir + tmpdir = tempfile.TemporaryDirectory() + keydir = tmpdir.name + ed25519_key_file = os.path.join(keydir, "dkim.ed25519.key") + self.ed25519_dns_key_file = os.path.join(keydir, "dkim.ed25519.key.pub.txt") + #Generate a ed25519 key + pkt = dknewkey.GenEd25519Keys(ed25519_key_file, False) + dknewkey.ExtractEd25519PublicKey(self.ed25519_dns_key_file, pkt, False) + #Load the key + ed25519key = read_test_data(ed25519_key_file) + #Test signature with the newely generated key + for header_algo in (b"simple", b"relaxed"): + for body_algo in (b"simple", b"relaxed"): + sig = dkim.sign( + self.message, b"test1", b"example.com", ed25519key, + signature_algorithm=b'ed25519-sha256', + canonicalize=(header_algo, body_algo)) + res = dkim.verify(sig + self.message, dnsfunc=self.dnsfuncED25519) + self.assertTrue(res) + + + def dnsfuncRSA(self, domain, timeout=5): + _dns_responses = { + 'test._domainkey.example.com.': read_test_data(self.rsa_dns_key_file), + } + try: + domain = domain.decode('ascii') + except UnicodeDecodeError: + return None + self.assertTrue(domain in _dns_responses,domain) + return _dns_responses[domain] + + def dnsfuncED25519(self, domain, timeout=5): + _dns_responses = { + 'test1._domainkey.example.com.': read_test_data(self.ed25519_dns_key_file), + } + try: + domain = domain.decode('ascii') + except UnicodeDecodeError: + return None + self.assertTrue(domain in _dns_responses,domain) + return _dns_responses[domain] + + + +def test_suite(): + from unittest import TestLoader + return TestLoader().loadTestsFromName(__name__)