Skip to content

Commit

Permalink
keystone_manage certificate generation
Browse files Browse the repository at this point in the history
Bug 1017554

paths now correspond with SSL
unit test for cert generation
Added mode config values
Explict about umask

replace string concat for paths with proper use of os.path.join
Change-Id: I8b3bec82d7b72993aa69653f63ff64c3f675f716
  • Loading branch information
Adam Young committed Jul 2, 2012
1 parent b45c252 commit 5ad8086
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 1 deletion.
9 changes: 8 additions & 1 deletion etc/keystone.conf.sample
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,18 @@
#ca_certs = /etc/keystone/ssl/certs/ca.pem
#cert_required = True

[signing]
#certfile = /etc/keystone/ssl/certs/signing_cert.pem
#keyfile = /etc/keystone/ssl/private/signing_key.pem
#ca_certs = /etc/keystone/ssl/certs/ca.pem
#key_size = 2048
#valid_days = 3650
#ca_password = None

[ldap]
# url = ldap://localhost
# user = dc=Manager,dc=example,dc=com
# password = freeipa4all
# password = None
# suffix = cn=example,cn=com
# use_dumb_member = False

Expand Down
15 changes: 15 additions & 0 deletions keystone/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import textwrap

from keystone import config
from keystone.common import openssl
from keystone.openstack.common import importutils
from keystone.openstack.common import jsonutils

Expand Down Expand Up @@ -55,6 +56,19 @@ def main(self):
driver.db_sync()


class PKISetup(BaseApp):
"""Set up Key pairs and certificates for token signing and verification."""

name = 'pki_setup'

def __init__(self, *args, **kw):
super(PKISetup, self).__init__(*args, **kw)

def main(self):
conf_ssl = openssl.ConfigurePKI()
conf_ssl.run()


class ImportLegacy(BaseApp):
"""Import a legacy database."""

Expand Down Expand Up @@ -110,6 +124,7 @@ def main(self):
'import_legacy': ImportLegacy,
'export_legacy_catalog': ExportLegacyCatalog,
'import_nova_auth': ImportNovaAuth,
'pki_setup': PKISetup,
}


Expand Down
214 changes: 214 additions & 0 deletions keystone/common/openssl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright 2012 OpenStack LLC
#
# 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
#

import os
import stat
import subprocess
import sys
import stat

from keystone import config


CONF = config.CONF
DIR_PERMS = stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | \
stat.S_IRGRP | stat.S_IXGRP | \
stat.S_IROTH | stat.S_IXOTH
CERT_PERMS = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH
PRIV_PERMS = stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
DEFAULT_SUBJECT = "/C=US/ST=Unset/L=Unset/O=Unset/CN=www.example.com"


def file_exists(file_path):
sys.stdout.write("Looking for %s:\t" % file_path)
if os.path.exists(file_path):
print("[FOUND]")
return True
else:
print("[NOT FOUND]")
return False


def make_dirs(file_name):
dir = os.path.dirname(file_name)
if not file_exists(dir):
os.makedirs(dir, DIR_PERMS)


class ConfigurePKI(object):
"""Generate files for PKI siginging using OpenSSL
Signed tokens require a private key and signing certificate which itself
must be signed by a CA. This class generates them with workable defaults
if each of the files are not present
"""
def __init__(self, *args, **kw):
self.conf_dir = os.path.dirname(CONF.signing.ca_certs)
self.ssl_config_file_name = os.path.join(self.conf_dir, "openssl.conf")
self.ca_key_file = os.path.join(self.conf_dir, "cakey.pem")
self.request_file_name = os.path.join(self.conf_dir, "req.pem")
self.ssl_dictionary = \
{
'conf_dir': self.conf_dir,
"ca_cert": CONF.signing.ca_certs,
"ssl_config": self.ssl_config_file_name,
"ca_private_key": self.ca_key_file,
"ca_cert_cn": "hostname",
"request_file": self.request_file_name,
"signing_key": CONF.signing.keyfile,
"signing_cert": CONF.signing.certfile,
"default_subject": DEFAULT_SUBJECT,
"key_size": int(CONF.signing.key_size),
"valid_days": int(CONF.signing.valid_days),
"ca_password": CONF.signing.ca_password
}

def exec_command(self, command):
to_exec = command % self.ssl_dictionary
print (to_exec)
subprocess.check_call(to_exec.rsplit(" "))

def build_ssl_config_file(self):
if not file_exists(self.ssl_config_file_name):
make_dirs(self.ssl_config_file_name)
ssl_config_file = open(self.ssl_config_file_name, 'w')
ssl_config_file.write(self.sslconfig % self.ssl_dictionary)
ssl_config_file.close()
os.chmod(self.ssl_config_file_name, CERT_PERMS)

index_file_name = os.path.join(self.conf_dir, "index.txt")
if not file_exists(index_file_name):
index_file = open(index_file_name, 'w')
index_file.write("")
index_file.close()
os.chmod(self.ssl_config_file_name, PRIV_PERMS)

serial_file_name = os.path.join(self.conf_dir, "serial")
if not file_exists(serial_file_name):
index_file = open(serial_file_name, 'w')
index_file.write("01")
index_file.close()
os.chmod(self.ssl_config_file_name, PRIV_PERMS)

def build_ca_cert(self):
if not file_exists(CONF.signing.ca_certs):
if not os.path.exists(self.ca_key_file):
make_dirs(self.ca_key_file)
self.exec_command("openssl genrsa -out %(ca_private_key)s "\
"%(key_size)d -config %(ssl_config)s")
os.chmod(self.ssl_dictionary["ca_private_key"], stat.S_IRUSR)
print("Generating CA certificate")
self.exec_command('openssl req -new -x509 -extensions v3_ca ' \
'-passin pass:%(ca_password)s ' \
'-key %(ca_private_key)s -out %(ca_cert)s '\
'-days %(valid_days)d ' \
'-config %(ssl_config)s ' \
'-subj %(default_subject)s')
os.chmod(self.ssl_dictionary["ca_cert"], CERT_PERMS)

def build_private_key(self):
if not file_exists(CONF.signing.keyfile):
make_dirs(CONF.signing.keyfile)

self.exec_command("openssl genrsa -out %(signing_key)s "\
"%(key_size)d "\
"-config %(ssl_config)s")
os.chmod(os.path.dirname(self.ssl_dictionary["signing_key"]),
PRIV_PERMS)
os.chmod(self.ssl_dictionary["signing_key"], stat.S_IRUSR)

def build_signing_cert(self):
if not file_exists(CONF.signing.certfile):
make_dirs(CONF.signing.certfile)
self.exec_command("openssl req -key %(signing_key)s -new -nodes "\
"-out %(request_file)s -config %(ssl_config)s "\
"-subj %(default_subject)s")
self.exec_command("openssl ca -batch -out %(signing_cert)s "\
"-config %(ssl_config)s "\
"-infiles %(request_file)s")

def run(self):
self.build_ssl_config_file()
self.build_ca_cert()
self.build_private_key()
self.build_signing_cert()

sslconfig = """
# OpenSSL configuration file.
#
# Establish working directory.
dir = %(conf_dir)s
[ ca ]
default_ca = CA_default
[ CA_default ]
new_certs_dir = $dir
serial = $dir/serial
database = $dir/index.txt
certificate = %(ca_cert)s
private_key = %(ca_private_key)s
default_days = 365
default_md = md5
preserve = no
email_in_dn = no
nameopt = default_ca
certopt = default_ca
policy = policy_match
[ policy_match ]
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
default_bits = 1024 # Size of keys
default_keyfile = key.pem # name of generated keys
default_md = md5 # message digest algorithm
string_mask = nombstr # permitted characters
distinguished_name = req_distinguished_name
req_extensions = v3_req
[ req_distinguished_name ]
0.organizationName = Organization Name (company)
organizationalUnitName = Organizational Unit Name (department, division)
emailAddress = Email Address
emailAddress_max = 40
localityName = Locality Name (city, district)
stateOrProvinceName = State or Province Name (full name)
countryName = Country Name (2 letter code)
countryName_min = 2
countryName_max = 2
commonName = Common Name (hostname, IP, or your name)
commonName_max = 64
# Default values for the above, for consistency and less typing.
0.organizationName_default = Openstack, Inc
localityName_default = Undefined
stateOrProvinceName_default = Undefined
countryName_default = US
commonName_default = %(ca_cert_cn)s
[ v3_ca ]
basicConstraints = CA:TRUE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always
[ v3_req ]
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash"""
10 changes: 10 additions & 0 deletions keystone/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@ def register_cli_int(*args, **kw):
register_str('keyfile', group='ssl', default=None)
register_str('ca_certs', group='ssl', default=None)
register_bool('cert_required', group='ssl', default=False)
#signing options
register_str('certfile', group='signing',
default="/etc/keystone/ssl/certs/signing_cert.pem")
register_str('keyfile', group='signing',
default="/etc/keystone/ssl/private/signing_key.pem")
register_str('ca_certs', group='signing',
default="/etc/keystone/ssl/certs/ca.pem")
register_int('key_size', group='signing', default=2048)
register_int('valid_days', group='signing', default=3650)
register_str('ca_password', group='signing', default=None)

# sql options
register_str('connection', group='sql', default='sqlite:///keystone.db')
Expand Down
52 changes: 52 additions & 0 deletions tests/test_cert_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright 2012 OpenStack LLC
#
# 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 os
import unittest2 as test
import shutil

from keystone import config
from keystone.common import openssl

ROOTDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SSLDIR = "%s/tests/ssl/" % ROOTDIR
CONF = config.CONF


def rootdir(*p):
return os.path.join(SSLDIR, *p)


CERTDIR = rootdir("certs")
KEYDIR = rootdir("private")

CONF.signing.certfile = os.path.join(CERTDIR, 'signing_cert.pem')
CONF.signing.ca_certs = os.path.join(CERTDIR, "ca.pem")
CONF.signing.keyfile = os.path.join(KEYDIR, "signing_key.pem")


class CertSetupTestCase(test.TestCase):

def test_create_certs(self):
ssl = openssl.ConfigurePKI()
ssl.run()
self.assertTrue(os.path.exists(CONF.signing.certfile))
self.assertTrue(os.path.exists(CONF.signing.ca_certs))
self.assertTrue(os.path.exists(CONF.signing.keyfile))

def tearDown(self):
shutil.rmtree(rootdir(SSLDIR))

0 comments on commit 5ad8086

Please sign in to comment.