Files
charm-keystone/hooks/keystone_ssl.py
Alex Kavanagh b3a6fdf5b5 Fix dangling file open() commands with no corresponding close
The code relies on a undocumented (and probably unstable) feature
of CPython to close a file when the reference is GCed.  However,
it's pretty poor practice to do so, so this patchset replaces them
with "with ..." statements to ensure that the files are closed
when no longer being used.

Change-Id: I6f24bc042a820ddd0147247267ee159753cfc1fb
2017-08-18 10:40:26 +01:00

359 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):
with open(bundle_file, 'r') as f:
current = f.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):
keypath = self.get_key_path(common_name)
crtpath = self.get_cert_path(common_name)
if not os.path.isfile(crtpath):
log("Creating certificate and key for {}.".format(common_name),
level=DEBUG)
crtpath, keypath = self._create_certificate(common_name,
common_name)
with open(crtpath, 'r') as f:
crt = f.read()
with open(keypath, 'r') as f:
key = f.read()
return crt, key
@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):
with open(self.ca_cert_path) as f:
int_cert = f.read()
with open(self.root_ca_cert_path) as f:
root_cert = f.read()
# NOTE: ordering of certs in bundle matters!
return int_cert + root_cert