Merge pull request #44 from openstack-charmers/add-utilities-generate-cert

Add utility helper function for generating X.509 certs
This commit is contained in:
Liam Young
2018-05-03 12:03:03 +01:00
committed by GitHub
3 changed files with 226 additions and 0 deletions

View File

@@ -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',

View File

@@ -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
)

123
zaza/utilities/cert.py Normal file
View File

@@ -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)
)