From a0a81bf53398a93ecdf112d09766e18d5b00d490 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 10 Dec 2014 17:07:12 +0100 Subject: [PATCH] More coverage for crypto_util --- letsencrypt/client/crypto_util.py | 68 ++++++----- letsencrypt/client/tests/crypto_util_test.py | 114 ++++++++++++++++++ letsencrypt/client/tests/testdata/csr-san.der | Bin 0 -> 370 bytes letsencrypt/client/tests/testdata/csr-san.pem | 10 ++ letsencrypt/client/tests/testdata/csr.der | Bin 0 -> 353 bytes letsencrypt/client/tests/testdata/csr.pem | 10 ++ .../client/tests/testdata/rsa512_key.pem | 9 ++ 7 files changed, 178 insertions(+), 33 deletions(-) create mode 100644 letsencrypt/client/tests/testdata/csr-san.der create mode 100644 letsencrypt/client/tests/testdata/csr-san.pem create mode 100644 letsencrypt/client/tests/testdata/csr.der create mode 100644 letsencrypt/client/tests/testdata/csr.pem create mode 100644 letsencrypt/client/tests/testdata/rsa512_key.pem diff --git a/letsencrypt/client/crypto_util.py b/letsencrypt/client/crypto_util.py index d19cbc0da..754557326 100644 --- a/letsencrypt/client/crypto_util.py +++ b/letsencrypt/client/crypto_util.py @@ -1,6 +1,5 @@ """Let's Encrypt client crypto utility functions""" import binascii -import hashlib import logging import time @@ -15,8 +14,6 @@ from letsencrypt.client import CONFIG from letsencrypt.client import le_util -# TODO: All of these functions need unit tests - def b64_cert_to_pem(b64_der_cert): return M2Crypto.X509.load_cert_der_string( le_util.jose_b64decode(b64_der_cert)).as_pem() @@ -76,27 +73,32 @@ def leading_zeros(arg): return arg -def sha256(arg): - return hashlib.sha256(arg).hexdigest() - - # based on M2Crypto unit test written by Toby Allsopp def make_key(bits=CONFIG.RSA_KEY_SIZE): + """Generate PEM encoded RSA key. + + :param int bits: Number of bits. + + :returns: new RSA key in PEM form with specified number of bits + :rtype: str + """ - Returns new RSA key in PEM form with specified bits - """ - # Python Crypto module doesn't produce any stdout - key = Crypto.PublicKey.RSA.generate(bits) # rsa = M2Crypto.RSA.gen_key(bits, 65537) # key_pem = rsa.as_pem(cipher=None) # rsa = None # should not be freed here - - return key.exportKey(format='PEM') + # Python Crypto module doesn't produce any stdout + return Crypto.PublicKey.RSA.generate(bits).exportKey(format='PEM') def make_csr(key_str, domains): - """ - Returns new CSR in PEM and DER form using key_file containing all domains + """Generate a CSR. + + :param str key_str: RSA key. + :param list domains: Domains included in the certificate. + + :returns: new CSR in PEM and DER form containing all domains + :rtype: tuple + """ assert domains, "Must provide one or more hostnames for the CSR." rsa_key = M2Crypto.RSA.load_key_string(key_str) @@ -115,7 +117,7 @@ def make_csr(key_str, domains): extstack = M2Crypto.X509.X509_Extension_Stack() ext = M2Crypto.X509.new_extension( - 'subjectAltName', ", ".join(["DNS:%s" % d for d in domains])) + 'subjectAltName', ", ".join("DNS:%s" % d for d in domains)) extstack.push(ext) csr.add_extensions(extstack) @@ -210,7 +212,7 @@ def valid_csr(csr): Check if `csr` is a valid CSR for the given domains. - :param str csr: CSR file contents + :param str csr: CSR in PEM. :returns: Validity of CSR. :rtype: bool @@ -229,7 +231,7 @@ def csr_matches_names(csr, domains): M2Crypto currently does not expose the OpenSSL interface to also check the SAN extension. This is insufficient for full testing - :param str csr: CSR file contents + :param str csr: CSR in DER. :param list domains: Domains the CSR should contain. @@ -244,6 +246,21 @@ def csr_matches_names(csr, domains): return False +def csr_matches_pubkey(csr, privkey): + """Does private key correspond to the subject public key in the CSR? + + :param str csr: CSR in PEM. + :param str privkey: Private key file contents + + :returns: Correspondence of private key to CSR subject public key. + :rtype: bool + + """ + csr_obj = M2Crypto.X509.load_request_string(csr) + privkey_obj = M2Crypto.RSA.load_key_string(privkey) + return csr_obj.get_pubkey().get_rsa().pub() == privkey_obj.pub() + + def valid_privkey(privkey): """Is valid RSA private key? @@ -257,18 +274,3 @@ def valid_privkey(privkey): return bool(M2Crypto.RSA.load_key_string(privkey).check_key()) except M2Crypto.RSA.RSAError: return False - - -def csr_matches_pubkey(csr, privkey): - """Does private key correspond to the subject public key in the CSR? - - :param str csr: CSR file contents - :param str privkey: Private key file contents - - :returns: Correspondence of private key to CSR subject public key. - :rtype: bool - - """ - csr_obj = M2Crypto.X509.load_request_string(csr) - privkey_obj = M2Crypto.RSA.load_key_string(privkey) - return csr_obj.get_pubkey().get_rsa().pub() == privkey_obj.pub() diff --git a/letsencrypt/client/tests/crypto_util_test.py b/letsencrypt/client/tests/crypto_util_test.py index 65b730df0..aad8ba1cf 100644 --- a/letsencrypt/client/tests/crypto_util_test.py +++ b/letsencrypt/client/tests/crypto_util_test.py @@ -1,5 +1,7 @@ """Tests for letsencrypt.client.crypto_util.""" +import os import pkg_resources +import tempfile import unittest @@ -40,5 +42,117 @@ class CreateSigTest(unittest.TestCase): del self.signature['nonce'] self.assertEqual(signature, self.signature) + +class MakeCSRTest(unittest.TestCase): + """Tests for letsencrypt.client.crypto_util.make_csr.""" + + def setUp(self): + self.key = pkg_resources.resource_string( + __name__, 'testdata/rsa256_key.pem') + + def test_single_domain(self): + from letsencrypt.client.crypto_util import make_csr + pem, der = make_csr(self.key, ['example.com']) + self.assertEqual(pem, pkg_resources.resource_string( + __name__, 'testdata/csr.pem')) + self.assertEqual(der, pkg_resources.resource_string( + __name__, 'testdata/csr.der')) + + def test_san(self): + from letsencrypt.client.crypto_util import make_csr + pem, der = make_csr(self.key, ['example.com', 'www.example.com']) + self.assertEqual(pem, pkg_resources.resource_string( + __name__, 'testdata/csr-san.pem')) + self.assertEqual(der, pkg_resources.resource_string( + __name__, 'testdata/csr-san.der')) + + +class ValidCSRTest(unittest.TestCase): + """Tests for letsencrypt.client.crypto_util.valid_csr.""" + + def _call(self, csr): + from letsencrypt.client.crypto_util import valid_csr + return valid_csr(csr) + + def _call_testdata(self, name): + return self._call(pkg_resources.resource_string( + __name__, os.path.join('testdata', name))) + + def test_valid_pem_true(self): + self.assertTrue(self._call_testdata('csr.pem')) + + def test_valid_pem_san_true(self): + self.assertTrue(self._call_testdata('csr-san.pem')) + + def test_valid_der_false(self): + self.assertFalse(self._call_testdata('csr.der')) + + def test_valid_der_san_false(self): + self.assertFalse(self._call_testdata('csr-san.der')) + + def test_empty_false(self): + self.assertFalse(self._call('')) + + def test_rubbis_false(self): + self.assertFalse(self._call('foo bar')) + + +class CSRMatchesNamesTest(unittest.TestCase): + """Tests for letsencrypt.client.crypto_util.csr_matches_names.""" + + def _call(self, csr, domains): + from letsencrypt.client.crypto_util import csr_matches_names + return csr_matches_names(csr, domains) + + def _call_testdata(self, name, domains): + return self._call(pkg_resources.resource_string( + __name__, os.path.join('testdata', name)), domains) + + def test_it(self): + self.assertTrue(self._call_testdata('csr.der', ['example.com'])) + self.assertFalse(self._call_testdata('csr.der', ['www.example.com'])) + self.assertFalse(self._call_testdata('csr.der', ['example'])) + + def test_san(self): + self.assertTrue(self._call_testdata('csr-san.der', ['example.com'])) + self.assertTrue(self._call_testdata('csr-san.der', ['www.example.com'])) + self.assertFalse(self._call_testdata('csr-san.der', ['example'])) + + +class CSRMatchesPubkeyTest(unittest.TestCase): + """Tests for letsencrypt.client.crypto_util.csr_matches_pubkey.""" + + def _call_testdata(self, name, privkey): + from letsencrypt.client.crypto_util import csr_matches_pubkey + return csr_matches_pubkey(pkg_resources.resource_string( + __name__, os.path.join('testdata', name)), privkey) + + def test_valid_true(self): + key = pkg_resources.resource_string(__name__, 'testdata/rsa256_key.pem') + self.assertTrue(self._call_testdata('csr.pem', key)) + + def test_invalid_false(self): + key = pkg_resources.resource_string(__name__, 'testdata/rsa512_key.pem') + self.assertFalse(self._call_testdata('csr.pem', key)) + + +class ValidPrivkeyTest(unittest.TestCase): + """Tests fro letsencrypt.client.crypto_util.valid_privkey.""" + + def _call(self, privkey): + from letsencrypt.client.crypto_util import valid_privkey + return valid_privkey(privkey) + + def test_valid_true(self): + self.assertTrue(self._call(pkg_resources.resource_string( + __name__, 'testdata/rsa256_key.pem'))) + + def test_empty_false(self): + self.assertFalse(self._call('')) + + def test_rubbish_false(self): + self.assertFalse(self._call('foo bar')) + + if __name__ == '__main__': unittest.main() diff --git a/letsencrypt/client/tests/testdata/csr-san.der b/letsencrypt/client/tests/testdata/csr-san.der new file mode 100644 index 0000000000000000000000000000000000000000..68fd38723ddd62a2d60fd6c27e67925d273d51f8 GIT binary patch literal 370 zcmXqLV$3sWVw7NFWH6{S`GxkDEsS*Ho7m>sw0v z-yV9(#LURRxWLN50&Y4dpP{yarhz)p5we0T3I=jb$PQ`ZFE20GLv|UGySRbwa%4zJ z@cA$|+wI(oIT5`GxkDEsS*Ho7m>sw0v z-yV9(#LURRxIoiD9d0@&pP`h2gn<~)5we0T{06*DC=Nk#1~