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
This commit is contained in:
parent
db8317c6aa
commit
e5f844d126
@ -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)
|
||||
|
49
os_cloud_config/cmd/generate_ssl_cert.py
Normal file
49
os_cloud_config/cmd/generate_ssl_cert.py
Normal file
@ -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='<heat_env>',
|
||||
help='path to JSON heat environment file'
|
||||
)
|
||||
parser.add_argument(
|
||||
'name',
|
||||
metavar='<name>',
|
||||
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)
|
@ -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):
|
||||
|
@ -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 <jsonfile> which is a heat JSON environment. If the parameters
|
||||
property exists in destination, then the following properties are added:
|
||||
{
|
||||
"parameters": {
|
||||
"<name>SslCertificate": PEM_DATA,
|
||||
"<name>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:
|
||||
|
||||
{
|
||||
"<name>": {
|
||||
"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))
|
@ -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()
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user