From 092c3c1f6b372f1ac6d870e0fd285e08e8171163 Mon Sep 17 00:00:00 2001 From: Frode Nordahl Date: Tue, 1 May 2018 14:09:12 +0200 Subject: [PATCH] Add utility helper function for generating X.509 certs --- setup.py | 1 + .../utilitites/test_zaza_utilitites_cert.py | 102 +++++++++++++++ zaza/utilities/cert.py | 123 ++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 unit_tests/utilitites/test_zaza_utilitites_cert.py create mode 100644 zaza/utilities/cert.py diff --git a/setup.py b/setup.py index a2a39f8..e1689f3 100644 --- a/setup.py +++ b/setup.py @@ -9,6 +9,7 @@ from setuptools.command.test import test as TestCommand version = "0.0.1.dev1" install_require = [ 'async_generator', + 'cryptography', 'hvac', 'jinja2', 'juju', diff --git a/unit_tests/utilitites/test_zaza_utilitites_cert.py b/unit_tests/utilitites/test_zaza_utilitites_cert.py new file mode 100644 index 0000000..876c299 --- /dev/null +++ b/unit_tests/utilitites/test_zaza_utilitites_cert.py @@ -0,0 +1,102 @@ +import unit_tests.utils as ut_utils +import zaza.utilities.cert as cert + + +class TestUtilitiesCert(ut_utils.BaseTestCase): + + def test_generate_cert(self): + self.patch_object(cert, 'serialization') + self.patch_object(cert, 'rsa') + self.patch_object(cert, 'cryptography') + cert.generate_cert('unit_test.ci.local') + self.assertTrue(self.serialization.NoEncryption.called) + self.cryptography.x509.NameAttribute.assert_called_with( + self.cryptography.x509.oid.NameOID.COMMON_NAME, + 'unit_test.ci.local', + ) + self.cryptography.x509.BasicConstraints.assert_called_with( + ca=False, path_length=None + ) + + def test_generate_cert_password(self): + self.patch_object(cert, 'serialization') + self.patch_object(cert, 'rsa') + self.patch_object(cert, 'cryptography') + cert.generate_cert('unit_test.ci.local', password='secret') + self.serialization.BestAvailableEncryption.assert_called_with('secret') + self.cryptography.x509.NameAttribute.assert_called_with( + self.cryptography.x509.oid.NameOID.COMMON_NAME, + 'unit_test.ci.local', + ) + self.cryptography.x509.BasicConstraints.assert_called_with( + ca=False, path_length=None + ) + + def test_generate_cert_issuer_name(self): + self.patch_object(cert, 'serialization') + self.patch_object(cert, 'rsa') + self.patch_object(cert, 'cryptography') + cert.generate_cert('unit_test.ci.local', issuer_name='issuer') + self.cryptography.x509.NameAttribute.assert_called_with( + self.cryptography.x509.oid.NameOID.COMMON_NAME, + 'issuer', + ) + self.cryptography.x509.BasicConstraints.assert_called_with( + ca=False, path_length=None + ) + + def test_generate_cert_signing_key(self): + self.patch_object(cert, 'serialization') + self.patch_object(cert, 'rsa') + self.patch_object(cert, 'cryptography') + cert.generate_cert('unit_test.ci.local', signing_key='signing_key') + self.assertTrue(self.serialization.NoEncryption.called) + self.serialization.load_pem_private_key.assert_called_with( + 'signing_key', + password=None, + backend=self.cryptography.hazmat.backends.default_backend(), + ) + self.cryptography.x509.NameAttribute.assert_called_with( + self.cryptography.x509.oid.NameOID.COMMON_NAME, + 'unit_test.ci.local', + ) + self.cryptography.x509.BasicConstraints.assert_called_with( + ca=False, path_length=None + ) + + def test_generate_cert_signing_key_signing_key_password(self): + self.patch_object(cert, 'serialization') + self.patch_object(cert, 'rsa') + self.patch_object(cert, 'cryptography') + cert.generate_cert( + 'unit_test.ci.local', + signing_key='signing_key', + signing_key_password='signing_key_password', + ) + self.assertTrue(self.serialization.NoEncryption.called) + self.serialization.load_pem_private_key.assert_called_with( + 'signing_key', + password='signing_key_password', + backend=self.cryptography.hazmat.backends.default_backend(), + ) + self.cryptography.x509.NameAttribute.assert_called_with( + self.cryptography.x509.oid.NameOID.COMMON_NAME, + 'unit_test.ci.local', + ) + self.cryptography.x509.BasicConstraints.assert_called_with( + ca=False, path_length=None + ) + + def test_generate_cert_generate_ca(self): + self.patch_object(cert, 'serialization') + self.patch_object(cert, 'rsa') + self.patch_object(cert, 'cryptography') + cert.generate_cert('unit_test.ci.local', generate_ca=True) + self.assertTrue(self.serialization.NoEncryption.called) + self.cryptography.x509.NameAttribute.assert_called_with( + self.cryptography.x509.oid.NameOID.COMMON_NAME, + 'unit_test.ci.local', + ) + self.cryptography.x509.BasicConstraints.assert_called_with( + ca=True, path_length=None + ) diff --git a/zaza/utilities/cert.py b/zaza/utilities/cert.py new file mode 100644 index 0000000..753499b --- /dev/null +++ b/zaza/utilities/cert.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# +# Copyright 2018 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cryptography +from cryptography.hazmat.primitives.asymmetric import rsa +import cryptography.hazmat.primitives.serialization as serialization +import datetime + + +def generate_cert(common_name, + password=None, + issuer_name=None, + signing_key=None, + signing_key_password=None, + generate_ca=False): + """ + Generate x.509 certificate + + Example of how to create a certificate chain: + (cakey, cacert) = generate_cert('DivineAuthority', generate_ca=True) + (crkey, crcert) = generate_cert('test.com', + issuer_name='DivineAuthority', + signing_key=cakey) + + :param common_name: Common Name to use in generated certificate + :type common_name: str + :param password: Password to protect encrypted private key with + :type password: Optional[str] + :param issuer_name: Issuer name, must match provided_private_key issuer + :type issuer_name: Optional[str] + :param signing_key: PEM encoded PKCS8 formatted private key + :type signing_key: Optional[str] + :param signing_key_password: Password to decrypt private key + :type signing_key_password: Optional[str] + :param generate_ca: Generate a certificate usable as a CA certificate + :type generate_ca: bool + :returns: x.509 certificate + :rtype: cryptography.x509.Certificate + """ + if password is not None: + encryption_algorithm = serialization.BestAvailableEncryption(password) + else: + encryption_algorithm = serialization.NoEncryption() + + if signing_key: + _signing_key = serialization.load_pem_private_key( + signing_key, + password=signing_key_password, + backend=cryptography.hazmat.backends.default_backend(), + ) + + private_key = rsa.generate_private_key( + public_exponent=65537, # per RFC 5280 Appendix C + key_size=2048, + backend=cryptography.hazmat.backends.default_backend() + ) + + public_key = private_key.public_key() + + builder = cryptography.x509.CertificateBuilder() + builder = builder.subject_name(cryptography.x509.Name([ + cryptography.x509.NameAttribute( + cryptography.x509.oid.NameOID.COMMON_NAME, common_name), + ])) + + if issuer_name is None: + issuer_name = common_name + + builder = builder.issuer_name(cryptography.x509.Name([ + cryptography.x509.NameAttribute( + cryptography.x509.oid.NameOID.COMMON_NAME, issuer_name), + ])) + builder = builder.not_valid_before( + datetime.datetime.today() - datetime.timedelta(1, 0, 0), + ) + builder = builder.not_valid_after( + datetime.datetime.today() + datetime.timedelta(1, 0, 0), + ) + builder = builder.serial_number(cryptography.x509.random_serial_number()) + builder = builder.public_key(public_key) + builder = builder.add_extension( + cryptography.x509.SubjectAlternativeName( + [cryptography.x509.DNSName(common_name)], + ), + critical=False, + ) + builder = builder.add_extension( + cryptography.x509.BasicConstraints(ca=generate_ca, path_length=None), + critical=True, + ) + + if signing_key: + sign_key = _signing_key + else: + sign_key = private_key + + certificate = builder.sign( + private_key=sign_key, + algorithm=cryptography.hazmat.primitives.hashes.SHA256(), + backend=cryptography.hazmat.backends.default_backend(), + ) + + return ( + private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=encryption_algorithm), + certificate.public_bytes( + serialization.Encoding.PEM) + )