From e5f844d126d6dbe6ad0b8a4dae5c2b314f5918a0 Mon Sep 17 00:00:00 2001 From: Gregory Haynes Date: Mon, 30 Jun 2014 12:26:51 -0700 Subject: [PATCH] Make keystone_pki less keystone specific We need to create ssl certs for services other than Keystone. Renaming keystone_pki to ssl_pki, and making the routines less keystone specific so we can create certs for other services. Also creating generate_ssl_cert command which can generate ssl certificates for multiple services using a single CA. Change-Id: I4d65cceee9e15ce0ca8ef309127a587135355810 --- os_cloud_config/cmd/generate_keystone_pki.py | 6 +- os_cloud_config/cmd/generate_ssl_cert.py | 49 +++++++ .../cmd/tests/test_generate_keystone_pki.py | 6 +- .../{keystone_pki.py => ssl_pki.py} | 130 +++++++++++++----- .../{test_keystone_pki.py => test_ssl_pki.py} | 112 ++++++++++----- setup.cfg | 1 + 6 files changed, 229 insertions(+), 75 deletions(-) create mode 100644 os_cloud_config/cmd/generate_ssl_cert.py rename os_cloud_config/{keystone_pki.py => ssl_pki.py} (61%) rename os_cloud_config/tests/{test_keystone_pki.py => test_ssl_pki.py} (50%) diff --git a/os_cloud_config/cmd/generate_keystone_pki.py b/os_cloud_config/cmd/generate_keystone_pki.py index db7ac16..c406599 100644 --- a/os_cloud_config/cmd/generate_keystone_pki.py +++ b/os_cloud_config/cmd/generate_keystone_pki.py @@ -15,7 +15,7 @@ import argparse import textwrap from os_cloud_config.cmd.utils import environment -from os_cloud_config import keystone_pki +from os_cloud_config import ssl_pki def parse_args(): @@ -60,6 +60,6 @@ def main(): environment._configure_logging(args) if args.heatenv: - keystone_pki.generate_certs_into_json(args.heatenv, args.seed) + ssl_pki.generate_cert_into_json(args.heatenv, "keystone") else: - keystone_pki.create_and_write_ca_and_signing_pairs(args.directory) + ssl_pki.create_and_write_ca_and_signing_pairs(args.directory) diff --git a/os_cloud_config/cmd/generate_ssl_cert.py b/os_cloud_config/cmd/generate_ssl_cert.py new file mode 100644 index 0000000..66ef916 --- /dev/null +++ b/os_cloud_config/cmd/generate_ssl_cert.py @@ -0,0 +1,49 @@ +# 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 argparse +import textwrap + +from os_cloud_config import ssl_pki + + +def parse_args(): + description = textwrap.dedent(""" + Generate and sign certificate with CA + + This script generates a certificate and signes certificate using the CA + in the heat environment. If no CA is in the heat environment then a new + CA will be generated. The resulting certificate and CA (if one is made) + are added to the heat environment. + """) + + parser = argparse.ArgumentParser( + description=description, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + 'heat_env', + metavar='', + help='path to JSON heat environment file' + ) + parser.add_argument( + 'name', + metavar='', + help='name for key/certificate pair', + ) + return parser.parse_args() + + +def main(): + args = parse_args() + ssl_pki.generate_cert_into_json(args.heat_env, args.name, args.overwrite) diff --git a/os_cloud_config/cmd/tests/test_generate_keystone_pki.py b/os_cloud_config/cmd/tests/test_generate_keystone_pki.py index f49d9bd..910b09b 100644 --- a/os_cloud_config/cmd/tests/test_generate_keystone_pki.py +++ b/os_cloud_config/cmd/tests/test_generate_keystone_pki.py @@ -23,14 +23,14 @@ from os_cloud_config.tests import base class GenerateKeystonePKITest(base.TestCase): - @mock.patch('os_cloud_config.keystone_pki.generate_certs_into_json') + @mock.patch('os_cloud_config.ssl_pki.generate_cert_into_json') @mock.patch.object(sys, 'argv', ['generate-keystone-pki', '-j', 'foo.json', '-s']) def test_with_heatenv(self, generate_mock): generate_keystone_pki.main() - generate_mock.assert_called_once_with('foo.json', True) + generate_mock.assert_called_once_with('foo.json', 'keystone') - @mock.patch('os_cloud_config.keystone_pki.create_and_write_ca_' + @mock.patch('os_cloud_config.ssl_pki.create_and_write_ca_' 'and_signing_pairs') @mock.patch.object(sys, 'argv', ['generate-keystone-pki', '-d', 'bar']) def test_without_heatenv(self, create_mock): diff --git a/os_cloud_config/keystone_pki.py b/os_cloud_config/ssl_pki.py similarity index 61% rename from os_cloud_config/keystone_pki.py rename to os_cloud_config/ssl_pki.py index c3d31a5..fa9b632 100644 --- a/os_cloud_config/keystone_pki.py +++ b/os_cloud_config/ssl_pki.py @@ -54,7 +54,7 @@ def create_ca_pair(cert_serial=1): subject.ST = 'Unset' subject.L = 'Unset' subject.O = 'Unset' - subject.CN = 'Keystone CA' + subject.CN = 'os-cloud-config CA' ca_cert.gmtime_adj_notBefore(0) ca_cert.gmtime_adj_notAfter(60 * 60 * 24 * CA_CERT_DAYS) ca_cert.set_issuer(subject) @@ -103,7 +103,7 @@ def create_signing_pair(ca_key_pem, ca_cert_pem, cert_serial=2): subject.ST = 'Unset' subject.L = 'Unset' subject.O = 'Unset' - subject.CN = 'Keystone Signing' + subject.CN = 'os-cloud-config Signing' signing_cert.gmtime_adj_notBefore(0) signing_cert.gmtime_adj_notAfter(60 * 60 * 24 * SIGNING_CERT_DAYS) signing_cert.set_issuer(ca_cert.get_subject()) @@ -137,18 +137,54 @@ def create_and_write_ca_and_signing_pairs(directory): _write_pki_file(path.join(directory, 'signing_cert.pem'), signing_cert_pem) -def generate_certs_into_json(jsonfile, seed): - """Create and write out CA certificate and signing certificate/key. +def generate_cert_into_json(jsonfile, name, auto_gen_ca=True, + overwrite=False): + """Create and write out an SSL certificate. - Generate CA certificate, signing certificate and signing key and - add them into a JSON file. If key/certs already exist in JSON file, no - change is done. + Create an ssl certificate, sign it with a CA, and output the certificate + to which is a heat JSON environment. If the parameters + property exists in destination, then the following properties are added: + { + "parameters": { + "SslCertificate": PEM_DATA, + "SslCertificateKey": PEM_DATA + } + } - :param jsonfile: JSON file where certs and key will be written - :type jsonfile: string - :param seed: JSON file for seed machine has different structure. Different - key/certs names and different parent node are used - :type seed: boolean + The CA certificate and key lives in the parameters "CaSslCertificate" and + "CaSslCertificateKey". If these parameters are not defined then a new CA + is created and these properties are added. + + If no "parameters" property exists then the following properties are + added: + + { + "": { + "ssl" : { + "certificate": PEM DATA, + "certificate_key: PEM_DATA + } + } + } + + The CA certificate and key in this case are: + + { + "ssl": { + "ca_certificate": PEM_DATA, + "ca_certificate_key": PEM_DATA + } + } + + :param jsonfile: Destination to write certificate and possible CA to. + :type jsonfile: string + :param cert_serial: Serial for the certificate. If None then this is + automatically determined. + :type cert_serial: integer + :param name: name for the certificate. + :type name: string + :param overwrite: overwrite certificate if it already exists + :type overwrite: boolean """ if os.path.isfile(jsonfile): with open(jsonfile) as json_fd: @@ -156,30 +192,60 @@ def generate_certs_into_json(jsonfile, seed): else: all_data = {} - if seed: - parent = 'keystone' - ca_cert_name = 'ca_certificate' - signing_key_name = 'signing_key' - signing_cert_name = 'signing_certificate' + parent = all_data.get("parameters") + ca_parent = None + if parent is not None: + cert_dest = "%sSslCertificate" % name + cert_key_dest = "%sSslCertificateKey" % name + ca_dest = "CaSslCertificate" + ca_key_dest = "CaSslCertificateKey" + cert_count_dest = "SslCertificatCount" + ca_parent = parent else: - parent = 'parameters' - ca_cert_name = 'KeystoneCACertificate' - signing_key_name = 'KeystoneSigningKey' - signing_cert_name = 'KeystoneSigningCertificate' + cert_dest = "certificate" + cert_key_dest = "certificate_key" + ca_dest = "ca_certificate" + ca_key_dest = "ca_certificate_key" + cert_count_dest = "certificate_count" - if parent not in all_data: - all_data[parent] = {} - parent_node = all_data[parent] + # Make parent be a node in all_data + svc = all_data.get(name, {}) + parent = all_data.get("ssl", {}) + svc["ssl"] = parent + ca_parent = all_data.get("ssl", {}) - if not (ca_cert_name in parent_node and - signing_key_name in parent_node and - signing_cert_name in parent_node): - ca_key_pem, ca_cert_pem = create_ca_pair() + all_data[name] = svc + all_data["ssl"] = ca_parent + + # If we only have one of cert or key this is an error if not overwriting + if (cert_dest not in parent and cert_key_dest in parent or + cert_dest in parent and cert_key_dest not in parent) and \ + not overwrite: + raise ValueError("Only one of certificate or key defined.") + + if cert_dest not in parent or overwrite: + # Check that we have both a CA cert and key or neither + if (ca_dest not in parent and ca_key_dest in parent) or \ + (ca_dest in parent and ca_key_dest not in parent): + raise ValueError("Only one of CA certificate or key defined.") + + # Gen CA + if ca_dest not in parent: + ca_key_pem, ca_cert_pem = create_ca_pair() + ca_parent[ca_key_dest] = ca_key_pem + ca_parent[ca_dest] = ca_cert_pem + + # Gen cert + cert_serial = ca_parent.get(cert_count_dest, 0) + cert_serial += 1 signing_key_pem, signing_cert_pem = create_signing_pair(ca_key_pem, - ca_cert_pem) - parent_node.update({ca_cert_name: ca_cert_pem, - signing_key_name: signing_key_pem, - signing_cert_name: signing_cert_pem}) + ca_cert_pem, + cert_serial) + ca_parent[cert_count_dest] = cert_serial + parent[cert_key_dest] = signing_key_pem + parent[cert_dest] = signing_cert_pem + + # Write out env with open(jsonfile, 'w') as json_fd: json.dump(all_data, json_fd, sort_keys=True) LOG.debug("Wrote key/certs into '%s'.", path.abspath(jsonfile)) diff --git a/os_cloud_config/tests/test_keystone_pki.py b/os_cloud_config/tests/test_ssl_pki.py similarity index 50% rename from os_cloud_config/tests/test_keystone_pki.py rename to os_cloud_config/tests/test_ssl_pki.py index 5f81a67..0808902 100644 --- a/os_cloud_config/tests/test_keystone_pki.py +++ b/os_cloud_config/tests/test_ssl_pki.py @@ -17,18 +17,18 @@ import stat import mock from OpenSSL import crypto -from os_cloud_config import keystone_pki +from os_cloud_config import ssl_pki from os_cloud_config.tests import base -class KeystonePKITest(base.TestCase): +class SslPKITest(base.TestCase): def test_create_ca_and_signing_pairs(self): # use one common test to avoid generating CA pair twice # do not mock out pyOpenSSL, test generated keys/certs # create CA pair - ca_key_pem, ca_cert_pem = keystone_pki.create_ca_pair() + ca_key_pem, ca_cert_pem = ssl_pki.create_ca_pair() ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, ca_key_pem) ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, ca_cert_pem) @@ -38,11 +38,11 @@ class KeystonePKITest(base.TestCase): # check CA cert properties self.assertFalse(ca_cert.has_expired()) - self.assertEqual('Keystone CA', ca_cert.get_issuer().CN) - self.assertEqual('Keystone CA', ca_cert.get_subject().CN) + self.assertEqual('os-cloud-config CA', ca_cert.get_issuer().CN) + self.assertEqual('os-cloud-config CA', ca_cert.get_subject().CN) # create signing pair - signing_key_pem, signing_cert_pem = keystone_pki.create_signing_pair( + signing_key_pem, signing_cert_pem = ssl_pki.create_signing_pair( ca_key_pem, ca_cert_pem) signing_key = crypto.load_privatekey(crypto.FILETYPE_PEM, signing_key_pem) @@ -55,22 +55,23 @@ class KeystonePKITest(base.TestCase): # check signing cert properties self.assertFalse(signing_cert.has_expired()) - self.assertEqual('Keystone CA', signing_cert.get_issuer().CN) - self.assertEqual('Keystone Signing', signing_cert.get_subject().CN) + self.assertEqual('os-cloud-config CA', signing_cert.get_issuer().CN) + self.assertEqual('os-cloud-config Signing', + signing_cert.get_subject().CN) # pyOpenSSL currenty cannot verify a cert against a CA cert - @mock.patch('os_cloud_config.keystone_pki.os.chmod', create=True) - @mock.patch('os_cloud_config.keystone_pki.os.mkdir', create=True) - @mock.patch('os_cloud_config.keystone_pki.path.isdir', create=True) - @mock.patch('os_cloud_config.keystone_pki.create_ca_pair') - @mock.patch('os_cloud_config.keystone_pki.create_signing_pair') - @mock.patch('os_cloud_config.keystone_pki.open', create=True) + @mock.patch('os_cloud_config.ssl_pki.os.chmod', create=True) + @mock.patch('os_cloud_config.ssl_pki.os.mkdir', create=True) + @mock.patch('os_cloud_config.ssl_pki.path.isdir', create=True) + @mock.patch('os_cloud_config.ssl_pki.create_ca_pair') + @mock.patch('os_cloud_config.ssl_pki.create_signing_pair') + @mock.patch('os_cloud_config.ssl_pki.open', create=True) def test_create_and_write_ca_and_signing_pairs( self, open_, create_signing, create_ca, isdir, mkdir, chmod): create_ca.return_value = ('mock_ca_key', 'mock_ca_cert') create_signing.return_value = ('mock_signing_key', 'mock_signing_cert') isdir.return_value = False - keystone_pki.create_and_write_ca_and_signing_pairs('/fake_dir') + ssl_pki.create_and_write_ca_and_signing_pairs('/fake_dir') mkdir.assert_called_with('/fake_dir') chmod.assert_has_calls([ @@ -99,31 +100,66 @@ class KeystonePKITest(base.TestCase): mock.call('mock_signing_cert'), ]) - @mock.patch('os_cloud_config.keystone_pki.path.isfile', create=True) - @mock.patch('os_cloud_config.keystone_pki.create_ca_pair') - @mock.patch('os_cloud_config.keystone_pki.create_signing_pair') - @mock.patch('os_cloud_config.keystone_pki.open', create=True) - @mock.patch('os_cloud_config.keystone_pki.json.dump') + @mock.patch('os_cloud_config.ssl_pki.path.isfile', create=True) + @mock.patch('os_cloud_config.ssl_pki.create_ca_pair') + @mock.patch('os_cloud_config.ssl_pki.create_signing_pair') + @mock.patch('os_cloud_config.ssl_pki.open', create=True) + @mock.patch('os_cloud_config.ssl_pki.json.load') + @mock.patch('os_cloud_config.ssl_pki.json.dump') def test_generate_certs_into_json( - self, mock_json, open_, create_signing, create_ca, isfile): + self, mock_json_dump, mock_json_load, open_, create_signing, + create_ca, isfile): create_ca.return_value = ('mock_ca_key', 'mock_ca_cert') create_signing.return_value = ('mock_signing_key', 'mock_signing_cert') - isfile.return_value = False + isfile.return_value = True + mock_json_load.return_value = { + "Keystone": { + "mock_property": "mock_value" + } + } - keystone_pki.generate_certs_into_json('/jsonfile', False) + ssl_pki.generate_cert_into_json('/jsonfile', "Keystone") - params = mock_json.call_args[0][0]['parameters'] - self.assertEqual(params['KeystoneCACertificate'], 'mock_ca_cert') - self.assertEqual(params['KeystoneSigningKey'], 'mock_signing_key') - self.assertEqual(params['KeystoneSigningCertificate'], + ssl = mock_json_dump.call_args[0][0]["ssl"] + keystone = mock_json_dump.call_args[0][0]["Keystone"] + self.assertEqual(keystone["mock_property"], "mock_value") + self.assertEqual(keystone["ssl"]["certificate"], "mock_signing_cert") + self.assertEqual(keystone["ssl"]["certificate_key"], + "mock_signing_key") + self.assertEqual(ssl["ca_certificate"], "mock_ca_cert") + self.assertEqual(ssl["ca_certificate_key"], "mock_ca_key") + + @mock.patch('os_cloud_config.ssl_pki.path.isfile', create=True) + @mock.patch('os_cloud_config.ssl_pki.create_ca_pair') + @mock.patch('os_cloud_config.ssl_pki.create_signing_pair') + @mock.patch('os_cloud_config.ssl_pki.open', create=True) + @mock.patch('os_cloud_config.ssl_pki.json.load') + @mock.patch('os_cloud_config.ssl_pki.json.dump') + def test_generate_cert_into_json_notseed( + self, mock_json_dump, mock_json_load, open_, create_signing, + create_ca, isfile): + create_ca.return_value = ('mock_ca_key', 'mock_ca_cert') + create_signing.return_value = ('mock_signing_key', 'mock_signing_cert') + isfile.return_value = True + mock_json_load.return_value = { + "parameters": {} + } + + ssl_pki.generate_cert_into_json('/jsonfile', "Keystone") + + params = mock_json_dump.call_args[0][0]['parameters'] + self.assertEqual(params['CaSslCertificate'], 'mock_ca_cert') + self.assertEqual(params['KeystoneSslCertificateKey'], + 'mock_signing_key') + self.assertEqual(params['KeystoneSslCertificate'], 'mock_signing_cert') - @mock.patch('os_cloud_config.keystone_pki.path.isfile', create=True) - @mock.patch('os_cloud_config.keystone_pki.create_ca_pair') - @mock.patch('os_cloud_config.keystone_pki.create_signing_pair') - @mock.patch('os_cloud_config.keystone_pki.open', create=True) - @mock.patch('os_cloud_config.keystone_pki.json.load') - @mock.patch('os_cloud_config.keystone_pki.json.dump') + @mock.patch('os_cloud_config.ssl_pki.path.isfile', create=True) + @mock.patch('os_cloud_config.ssl_pki.create_ca_pair') + @mock.patch('os_cloud_config.ssl_pki.create_signing_pair') + @mock.patch('os_cloud_config.ssl_pki.open', create=True) + @mock.patch('os_cloud_config.ssl_pki.json.load') + @mock.patch('os_cloud_config.ssl_pki.json.dump') def test_generate_certs_into_json_with_existing_certs( self, mock_json_dump, mock_json_load, open_, create_signing, create_ca, isfile): @@ -131,10 +167,12 @@ class KeystonePKITest(base.TestCase): create_signing.return_value = ('mock_signing_key', 'mock_signing_cert') isfile.return_value = True mock_json_load.return_value = { - 'KeystoneCACertificate': 'mock_ca_cert', - 'KeystoneSigningKey': 'mock_signing_key', - 'KeystoneSigningCertificate': 'mock_signing_cert' + "parameters": { + 'KeystoneCACertificate': 'mock_ca_cert', + 'KeystoneSigningKey': 'mock_signing_key', + 'KeystoneSigningCertificate': 'mock_signing_cert' + } } - keystone_pki.generate_certs_into_json('/jsonfile', False) + ssl_pki.generate_cert_into_json('/jsonfile', "keystone") mock_json_dump.assert_not_called() diff --git a/setup.cfg b/setup.cfg index 3b3d441..34cc21e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ packages = [entry_points] console_scripts = generate-keystone-pki = os_cloud_config.cmd.generate_keystone_pki:main + generate-ssl-cert = os_cloud_config.cmd.generate_ssl_cert:main init-keystone = os_cloud_config.cmd.init_keystone:main register-nodes = os_cloud_config.cmd.register_nodes:main setup-endpoints = os_cloud_config.cmd.setup_endpoints:main