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 000000000..68fd38723 Binary files /dev/null and b/letsencrypt/client/tests/testdata/csr-san.der differ diff --git a/letsencrypt/client/tests/testdata/csr-san.pem b/letsencrypt/client/tests/testdata/csr-san.pem new file mode 100644 index 000000000..a7128e35c --- /dev/null +++ b/letsencrypt/client/tests/testdata/csr-san.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw +EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy +c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG +9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f +p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN +AQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t +MA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy +tmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A== +-----END CERTIFICATE REQUEST----- diff --git a/letsencrypt/client/tests/testdata/csr.der b/letsencrypt/client/tests/testdata/csr.der new file mode 100644 index 000000000..22900a612 Binary files /dev/null and b/letsencrypt/client/tests/testdata/csr.der differ diff --git a/letsencrypt/client/tests/testdata/csr.pem b/letsencrypt/client/tests/testdata/csr.pem new file mode 100644 index 000000000..b6818e39d --- /dev/null +++ b/letsencrypt/client/tests/testdata/csr.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw +EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy +c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG +9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f +p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoCkwJwYJKoZIhvcN +AQkOMRowGDAWBgNVHREEDzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAANB +AHJH/O6BtC9aGzEVCMGOZ7z9iIRHWSzr9x/bOzn7hLwsbXPAgO1QxEwL+X+4g20G +n9XBE1N9W6HCIEut2d8wACg= +-----END CERTIFICATE REQUEST----- diff --git a/letsencrypt/client/tests/testdata/rsa512_key.pem b/letsencrypt/client/tests/testdata/rsa512_key.pem new file mode 100644 index 000000000..709b6d8e3 --- /dev/null +++ b/letsencrypt/client/tests/testdata/rsa512_key.pem @@ -0,0 +1,9 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIBOwIBAAJBAPS2EXFRNza/qpXnnBHF/CcFQ543htV+7nLAmrLrmTNHtPXJmLlM +8SJDIzv/ceAFXL110VzxFfi81lpH5E5c0TMCAwEAAQJBALmppYQ/JVARjWBcsEm/ +1/bXBJ127YLv4gQIY5baL4r6IdEE33OXMTTmD9wf+ajuq1eaH0htHkwhOvREu0sz +bskCIQD/Cg+xhEVLcwK3pFp3afPIhj1IPFiL3Uy/nqyMZ6O/RQIhAPWiDBofp7Cp +J4dGZs+hkRySq/IOeeRJlNK1Pq64nToXAiBZ7+te1100YSd5KT051SRB94zO13EG +SZESFduVW8rz3QIgK+tLiqg6TYYRQUi/PUTAM4GuKNuZw828RGiPyqHLywUCIQCd +pkZrNphL/y0D7HSbPIfZzD90M2V8tUjlK0BTqk1bHA== +-----END RSA PRIVATE KEY-----