PKI key/cert generation for use with Keystone
Allow generating CA key + certificate and a signing key + certificate. This will be generated on seed/undercloud and Heat + os-apply-config will be used to get the files into place on undercloud/overcloud. Partially implements: blueprint tripleo-keystone-cloud-config Change-Id: Ide692f372faa0907447fcb9971a8bdeec86130dd
This commit is contained in:
parent
8d281a9e2c
commit
11b0d9ebe8
48
os_cloud_config/cmd/generate_keystone_pki.py
Normal file
48
os_cloud_config/cmd/generate_keystone_pki.py
Normal file
@ -0,0 +1,48 @@
|
||||
# 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 keystone_pki
|
||||
|
||||
|
||||
def parse_args():
|
||||
description = textwrap.dedent("""
|
||||
Generate 4 files inside <directory> for use with Keystone PKI
|
||||
token signing:
|
||||
|
||||
ca_key.pem - certificate authority key
|
||||
ca_cert.pem - self-signed certificate authority certificate
|
||||
signing_key.pem - key for signing tokens
|
||||
signing_cert.pem - certificate for verifying token validity, the
|
||||
certificate itself is verifiable by ca_cert.pem
|
||||
|
||||
ca_key.pem doesn't have to (shouldn't) be uploaded to Keystone nodes.
|
||||
""")
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=description,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
'directory',
|
||||
metavar='<directory>',
|
||||
help='directory where keys/certs will be generated',
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
keystone_pki.create_and_write_ca_and_signing_pairs(args.directory)
|
143
os_cloud_config/keystone_pki.py
Normal file
143
os_cloud_config/keystone_pki.py
Normal file
@ -0,0 +1,143 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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 logging
|
||||
import os
|
||||
from os import path
|
||||
import stat
|
||||
|
||||
from OpenSSL import crypto
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CA_KEY_SIZE = 2048
|
||||
CA_CERT_DAYS = 10 * 365
|
||||
SIGNING_KEY_SIZE = 2048
|
||||
SIGNING_CERT_DAYS = 10 * 365
|
||||
X509_VERSION = 2
|
||||
|
||||
|
||||
def create_ca_pair(cert_serial=1):
|
||||
"""Create CA private key and self-signed certificate.
|
||||
|
||||
CA generation is mostly meant for proof-of-concept
|
||||
deployments. For real deployments it is suggested to use an
|
||||
external CA (separate from deployment tools).
|
||||
|
||||
:param cert_serial: serial number of the generated certificate
|
||||
:type cert_serial: integer
|
||||
:return: (ca_key_pem, ca_cert_pem) tuple of base64 encoded CA
|
||||
private key and CA certificate (PEM format)
|
||||
:rtype: (string, string)
|
||||
"""
|
||||
ca_key = crypto.PKey()
|
||||
ca_key.generate_key(crypto.TYPE_RSA, CA_KEY_SIZE)
|
||||
LOG.debug('Generated CA key.')
|
||||
|
||||
ca_cert = crypto.X509()
|
||||
ca_cert.set_version(X509_VERSION)
|
||||
ca_cert.set_serial_number(cert_serial)
|
||||
subject = ca_cert.get_subject()
|
||||
subject.C = 'XX'
|
||||
subject.ST = 'Unset'
|
||||
subject.L = 'Unset'
|
||||
subject.O = 'Unset'
|
||||
subject.CN = 'Keystone CA'
|
||||
ca_cert.gmtime_adj_notBefore(0)
|
||||
ca_cert.gmtime_adj_notAfter(60 * 60 * 24 * CA_CERT_DAYS)
|
||||
ca_cert.set_issuer(subject)
|
||||
ca_cert.set_pubkey(ca_key)
|
||||
ca_cert.add_extensions([
|
||||
crypto.X509Extension("basicConstraints", True, "CA:TRUE, pathlen:0"),
|
||||
])
|
||||
ca_cert.sign(ca_key, 'sha1')
|
||||
LOG.debug('Generated CA certificate.')
|
||||
|
||||
return (crypto.dump_privatekey(crypto.FILETYPE_PEM, ca_key),
|
||||
crypto.dump_certificate(crypto.FILETYPE_PEM, ca_cert))
|
||||
|
||||
|
||||
def create_signing_pair(ca_key_pem, ca_cert_pem, cert_serial=2):
|
||||
"""Create signing private key and certificate.
|
||||
|
||||
Os-cloud-config key generation and certificate signing is mostly
|
||||
meant for proof-of-concept deployments. For real deployments it is
|
||||
suggested to use certificates signed by an external CA.
|
||||
|
||||
:param ca_key_pem: CA private key to sign the signing certificate,
|
||||
base64 encoded (PEM format)
|
||||
:type ca_key_pem: string
|
||||
:param ca_cert_pem: CA certificate, base64 encoded (PEM format)
|
||||
:type ca_cert_pem: string
|
||||
:param cert_serial: serial number of the generated certificate
|
||||
:type cert_serial: integer
|
||||
:return: (signing_key_pem, signing_cert_pem) tuple of base64
|
||||
encoded signing private key and signing certificate
|
||||
(PEM format)
|
||||
:rtype: (string, string)
|
||||
"""
|
||||
ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, ca_key_pem)
|
||||
ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, ca_cert_pem)
|
||||
|
||||
signing_key = crypto.PKey()
|
||||
signing_key.generate_key(crypto.TYPE_RSA, CA_KEY_SIZE)
|
||||
LOG.debug('Generated signing key.')
|
||||
|
||||
signing_cert = crypto.X509()
|
||||
signing_cert.set_version(X509_VERSION)
|
||||
signing_cert.set_serial_number(cert_serial)
|
||||
subject = signing_cert.get_subject()
|
||||
subject.C = 'XX'
|
||||
subject.ST = 'Unset'
|
||||
subject.L = 'Unset'
|
||||
subject.O = 'Unset'
|
||||
subject.CN = 'Keystone 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())
|
||||
signing_cert.set_pubkey(signing_key)
|
||||
signing_cert.sign(ca_key, 'sha1')
|
||||
LOG.debug('Generated signing certificate.')
|
||||
|
||||
return (crypto.dump_privatekey(crypto.FILETYPE_PEM, signing_key),
|
||||
crypto.dump_certificate(crypto.FILETYPE_PEM, signing_cert))
|
||||
|
||||
|
||||
def create_and_write_ca_and_signing_pairs(directory):
|
||||
"""Create and write out CA and signing keys and certificates.
|
||||
|
||||
Generate ca_key.pem, ca_cert.pem, signing_key.pem,
|
||||
signing_cert.pem and write them out to a directory.
|
||||
|
||||
:param directory: directory where keys and certs will be written
|
||||
:type directory: string
|
||||
"""
|
||||
if not path.isdir(directory):
|
||||
os.mkdir(directory)
|
||||
|
||||
ca_key_pem, ca_cert_pem = create_ca_pair()
|
||||
signing_key_pem, signing_cert_pem = create_signing_pair(ca_key_pem,
|
||||
ca_cert_pem)
|
||||
|
||||
_write_pki_file(path.join(directory, 'ca_key.pem'), ca_key_pem)
|
||||
_write_pki_file(path.join(directory, 'ca_cert.pem'), ca_cert_pem)
|
||||
_write_pki_file(path.join(directory, 'signing_key.pem'), signing_key_pem)
|
||||
_write_pki_file(path.join(directory, 'signing_cert.pem'), signing_cert_pem)
|
||||
|
||||
|
||||
def _write_pki_file(file_path, contents):
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(contents)
|
||||
os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR)
|
||||
LOG.debug("Wrote '%s'.", path.abspath(file_path))
|
100
os_cloud_config/tests/test_keystone_pki.py
Normal file
100
os_cloud_config/tests/test_keystone_pki.py
Normal file
@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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 stat
|
||||
|
||||
import mock
|
||||
from OpenSSL import crypto
|
||||
|
||||
from os_cloud_config import keystone_pki
|
||||
from os_cloud_config.tests import base
|
||||
|
||||
|
||||
class KeystonePKITest(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 = crypto.load_privatekey(crypto.FILETYPE_PEM, ca_key_pem)
|
||||
ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, ca_cert_pem)
|
||||
|
||||
# check CA key properties
|
||||
self.assertTrue(ca_key.check())
|
||||
self.assertEqual(2048, ca_key.bits())
|
||||
|
||||
# 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)
|
||||
|
||||
# create signing pair
|
||||
signing_key_pem, signing_cert_pem = keystone_pki.create_signing_pair(
|
||||
ca_key_pem, ca_cert_pem)
|
||||
signing_key = crypto.load_privatekey(crypto.FILETYPE_PEM,
|
||||
signing_key_pem)
|
||||
signing_cert = crypto.load_certificate(crypto.FILETYPE_PEM,
|
||||
signing_cert_pem)
|
||||
|
||||
# check signing key properties
|
||||
self.assertTrue(signing_key.check())
|
||||
self.assertEqual(2048, signing_key.bits())
|
||||
|
||||
# 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)
|
||||
# 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)
|
||||
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')
|
||||
|
||||
mkdir.assert_called_with('/fake_dir')
|
||||
chmod.assert_has_calls([
|
||||
mock.call('/fake_dir/ca_key.pem',
|
||||
stat.S_IRUSR | stat.S_IWUSR),
|
||||
mock.call('/fake_dir/ca_cert.pem',
|
||||
stat.S_IRUSR | stat.S_IWUSR),
|
||||
mock.call('/fake_dir/signing_key.pem',
|
||||
stat.S_IRUSR | stat.S_IWUSR),
|
||||
mock.call('/fake_dir/signing_cert.pem',
|
||||
stat.S_IRUSR | stat.S_IWUSR),
|
||||
])
|
||||
# need any_order param, there are open().__enter__()
|
||||
# etc. called in between
|
||||
open_.assert_has_calls([
|
||||
mock.call('/fake_dir/ca_key.pem', 'w'),
|
||||
mock.call('/fake_dir/ca_cert.pem', 'w'),
|
||||
mock.call('/fake_dir/signing_key.pem', 'w'),
|
||||
mock.call('/fake_dir/signing_cert.pem', 'w'),
|
||||
], any_order=True)
|
||||
cert_files = open_.return_value.__enter__.return_value
|
||||
cert_files.write.assert_has_calls([
|
||||
mock.call('mock_ca_key'),
|
||||
mock.call('mock_ca_cert'),
|
||||
mock.call('mock_signing_key'),
|
||||
mock.call('mock_signing_cert'),
|
||||
])
|
@ -5,4 +5,5 @@ Babel>=1.3
|
||||
python-ironicclient
|
||||
python-keystoneclient>=0.6.0
|
||||
oslo.config>=1.2.0
|
||||
pyOpenSSL>=0.11
|
||||
simplejson>=2.0.9
|
||||
|
Loading…
Reference in New Issue
Block a user