
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
359 lines
12 KiB
Python
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
|