charm-keystone/hooks/keystone_ssl.py

355 lines
12 KiB
Python

#!/usr/bin/python
#
# Copyright 2016 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 os
import shutil
import subprocess
import tarfile
import tempfile
from charmhelpers.core.hookenv import (
log,
DEBUG,
)
CA_EXPIRY = '365'
ORG_NAME = 'Ubuntu'
ORG_UNIT = 'Ubuntu Cloud'
CA_BUNDLE = '/usr/local/share/ca-certificates/juju_ca_cert.crt'
CA_CONFIG = """
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = %(ca_dir)s
policy = policy_match
database = $dir/index.txt
serial = $dir/serial
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
certificate = $dir/cacert.pem
private_key = $dir/private/cacert.key
RANDFILE = $dir/private/.rand
default_md = default
[ req ]
default_bits = 1024
default_md = sha1
prompt = no
distinguished_name = ca_distinguished_name
x509_extensions = ca_extensions
[ ca_distinguished_name ]
organizationName = %(org_name)s
organizationalUnitName = %(org_unit_name)s Certificate Authority
commonName = %(common_name)s
[ policy_match ]
countryName = optional
stateOrProvinceName = optional
organizationName = match
organizationalUnitName = optional
commonName = supplied
[ ca_extensions ]
basicConstraints = critical,CA:true
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer
keyUsage = cRLSign, keyCertSign
"""
SIGNING_CONFIG = """
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = %(ca_dir)s
policy = policy_match
database = $dir/index.txt
serial = $dir/serial
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
certificate = $dir/cacert.pem
private_key = $dir/private/cacert.key
RANDFILE = $dir/private/.rand
default_md = default
[ req ]
default_bits = 1024
default_md = sha1
prompt = no
distinguished_name = req_distinguished_name
x509_extensions = req_extensions
[ req_distinguished_name ]
organizationName = %(org_name)s
organizationalUnitName = %(org_unit_name)s Server Farm
[ policy_match ]
countryName = optional
stateOrProvinceName = optional
organizationName = match
organizationalUnitName = optional
commonName = supplied
[ req_extensions ]
basicConstraints = CA:false
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer
keyUsage = digitalSignature, keyEncipherment, keyAgreement
extendedKeyUsage = serverAuth, clientAuth
"""
# Instance can be appended to this list to represent a singleton
CA_SINGLETON = []
def init_ca(ca_dir, common_name, org_name=ORG_NAME, org_unit_name=ORG_UNIT):
log('Ensuring certificate authority exists at %s.' % ca_dir, level=DEBUG)
if not os.path.exists(ca_dir):
log('Initializing new certificate authority at %s' % ca_dir,
level=DEBUG)
os.mkdir(ca_dir)
for i in ['certs', 'crl', 'newcerts', 'private']:
d = os.path.join(ca_dir, i)
if not os.path.exists(d):
log('Creating %s.' % d, level=DEBUG)
os.mkdir(d)
os.chmod(os.path.join(ca_dir, 'private'), 0o710)
if not os.path.isfile(os.path.join(ca_dir, 'serial')):
with open(os.path.join(ca_dir, 'serial'), 'wb') as out:
out.write('01\n')
if not os.path.isfile(os.path.join(ca_dir, 'index.txt')):
with open(os.path.join(ca_dir, 'index.txt'), 'wb') as out:
out.write('')
conf = os.path.join(ca_dir, 'ca.cnf')
if not os.path.isfile(conf):
log('Creating new CA config in %s' % ca_dir, level=DEBUG)
with open(conf, 'wb') as out:
out.write(CA_CONFIG % locals())
def root_ca_crt_key(ca_dir):
init = False
crt = os.path.join(ca_dir, 'cacert.pem')
key = os.path.join(ca_dir, 'private', 'cacert.key')
for f in [crt, key]:
if not os.path.isfile(f):
log('Missing %s, will re-initialize cert+key.' % f, level=DEBUG)
init = True
else:
log('Found %s.' % f, level=DEBUG)
if init:
conf = os.path.join(ca_dir, 'ca.cnf')
cmd = ['openssl', 'req', '-config', conf,
'-x509', '-nodes', '-newkey', 'rsa', '-days', '21360',
'-keyout', key, '-out', crt, '-outform', 'PEM']
subprocess.check_call(cmd)
return crt, key
def intermediate_ca_csr_key(ca_dir):
log('Creating new intermediate CSR.', level=DEBUG)
key = os.path.join(ca_dir, 'private', 'cacert.key')
csr = os.path.join(ca_dir, 'cacert.csr')
conf = os.path.join(ca_dir, 'ca.cnf')
cmd = ['openssl', 'req', '-config', conf, '-sha1', '-newkey', 'rsa',
'-nodes', '-keyout', key, '-out', csr, '-outform', 'PEM']
subprocess.check_call(cmd)
return csr, key
def sign_int_csr(ca_dir, csr, common_name):
log('Signing certificate request %s.' % csr, level=DEBUG)
crt_name = os.path.basename(csr).split('.')[0]
crt = os.path.join(ca_dir, 'certs', '%s.crt' % crt_name)
subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name)
conf = os.path.join(ca_dir, 'ca.cnf')
cmd = ['openssl', 'ca', '-batch', '-config', conf, '-extensions',
'ca_extensions', '-days', CA_EXPIRY, '-notext', '-in', csr, '-out',
crt, '-subj', subj, '-batch']
log("Executing: %s" % ' '.join(cmd), level=DEBUG)
subprocess.check_call(cmd)
return crt
def init_root_ca(ca_dir, common_name):
init_ca(ca_dir, common_name)
return root_ca_crt_key(ca_dir)
def init_intermediate_ca(ca_dir, common_name, root_ca_dir, org_name=ORG_NAME,
org_unit_name=ORG_UNIT):
init_ca(ca_dir, common_name)
if not os.path.isfile(os.path.join(ca_dir, 'cacert.pem')):
csr, key = intermediate_ca_csr_key(ca_dir)
crt = sign_int_csr(root_ca_dir, csr, common_name)
shutil.copy(crt, os.path.join(ca_dir, 'cacert.pem'))
else:
log('Intermediate CA certificate already exists.', level=DEBUG)
conf = os.path.join(ca_dir, 'signing.cnf')
if not os.path.isfile(conf):
log('Creating new signing config in %s' % ca_dir, level=DEBUG)
with open(conf, 'wb') as out:
out.write(SIGNING_CONFIG % locals())
def create_certificate(ca_dir, service):
common_name = service
subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name)
csr = os.path.join(ca_dir, 'certs', '%s.csr' % service)
key = os.path.join(ca_dir, 'certs', '%s.key' % service)
cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa', '-nodes', '-keyout',
key, '-out', csr, '-subj', subj]
subprocess.check_call(cmd)
crt = sign_int_csr(ca_dir, csr, common_name)
log('Signed new CSR, crt @ %s' % crt, level=DEBUG)
return
def update_bundle(bundle_file, new_bundle):
return
if os.path.isfile(bundle_file):
current = open(bundle_file, 'r').read().strip()
if new_bundle == current:
log('CA Bundle @ %s is up to date.' % bundle_file, level=DEBUG)
return
log('Updating CA bundle @ %s.' % bundle_file, level=DEBUG)
with open(bundle_file, 'wb') as out:
out.write(new_bundle)
subprocess.check_call(['update-ca-certificates'])
def tar_directory(path):
cwd = os.getcwd()
parent = os.path.dirname(path)
directory = os.path.basename(path)
tmp = tempfile.TemporaryFile()
os.chdir(parent)
tarball = tarfile.TarFile(fileobj=tmp, mode='w')
tarball.add(directory)
tarball.close()
tmp.seek(0)
out = tmp.read()
tmp.close()
os.chdir(cwd)
return out
class JujuCA(object):
def __init__(self, name, ca_dir, root_ca_dir, user, group):
# Root CA
cn = '%s Certificate Authority' % name
root_crt, root_key = init_root_ca(root_ca_dir, cn)
# Intermediate CA
cn = '%s Intermediate Certificate Authority' % name
init_intermediate_ca(ca_dir, cn, root_ca_dir)
# Create dirs
cmd = ['chown', '-R', '%s.%s' % (user, group), ca_dir]
subprocess.check_call(cmd)
cmd = ['chown', '-R', '%s.%s' % (user, group), root_ca_dir]
subprocess.check_call(cmd)
self.ca_dir = ca_dir
self.root_ca_dir = root_ca_dir
self.user = user
self.group = group
update_bundle(CA_BUNDLE, self.get_ca_bundle())
def _sign_csr(self, csr, service, common_name):
subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name)
crt = os.path.join(self.ca_dir, 'certs', '%s.crt' % common_name)
conf = os.path.join(self.ca_dir, 'signing.cnf')
cmd = ['openssl', 'ca', '-config', conf, '-extensions',
'req_extensions', '-days', '365', '-notext', '-in', csr,
'-out', crt, '-batch', '-subj', subj]
subprocess.check_call(cmd)
return crt
def _create_certificate(self, service, common_name):
subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name)
csr = os.path.join(self.ca_dir, 'certs', '%s.csr' % service)
key = os.path.join(self.ca_dir, 'certs', '%s.key' % service)
cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa', '-nodes',
'-keyout', key, '-out', csr, '-subj', subj]
subprocess.check_call(cmd)
crt = self._sign_csr(csr, service, common_name)
cmd = ['chown', '-R', '%s.%s' % (self.user, self.group), self.ca_dir]
subprocess.check_call(cmd)
log('Signed new CSR, crt @ %s' % crt, level=DEBUG)
return crt, key
def get_key_path(self, cn):
return os.path.join(self.ca_dir, 'certs', '%s.key' % cn)
def get_cert_path(self, cn):
return os.path.join(self.ca_dir, 'certs', '%s.crt' % cn)
def get_cert_and_key(self, common_name):
log('Getting certificate and key for %s.' % common_name, level=DEBUG)
keypath = self.get_key_path(common_name)
crtpath = self.get_cert_path(common_name)
if os.path.isfile(crtpath):
log('Found existing certificate for %s.' % common_name,
level=DEBUG)
crt = open(crtpath, 'r').read()
key = open(keypath, 'r').read()
return crt, key
crt, key = self._create_certificate(common_name, common_name)
return open(crt, 'r').read(), open(key, 'r').read()
@property
def ca_cert_path(self):
return os.path.join(self.ca_dir, 'cacert.pem')
@property
def ca_key_path(self):
return os.path.join(self.ca_dir, 'private', 'cacert.key')
@property
def root_ca_cert_path(self):
return os.path.join(self.root_ca_dir, 'cacert.pem')
@property
def root_ca_key_path(self):
return os.path.join(self.root_ca_dir, 'private', 'cacert.key')
def get_ca_bundle(self):
int_cert = open(self.ca_cert_path).read()
root_cert = open(self.root_ca_cert_path).read()
# NOTE: ordering of certs in bundle matters!
return int_cert + root_cert