1
# Copyright 2014-2015 Canonical Limited.
3
# This file is part of charm-helpers.
5
# charm-helpers is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License version 3 as
7
# published by the Free Software Foundation.
9
# charm-helpers is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU Lesser General Public License for more details.
14
# You should have received a copy of the GNU Lesser General Public License
15
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
18
from os.path import join as path_join
19
from os.path import exists
22
from charmhelpers.core.hookenv import log, DEBUG
26
# Mysql server is fairly picky about cert creation
27
# and types, spec its creation separately for now.
31
class ServiceCA(object):
33
default_expiry = str(365 * 2)
34
default_ca_expiry = str(365 * 6)
36
def __init__(self, name, ca_dir, cert_type=STD_CERT):
39
self.cert_type = cert_type
44
def get_ca(type=STD_CERT):
45
service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
46
ca_path = os.path.join(os.environ['CHARM_DIR'], 'ca')
47
ca = ServiceCA(service_name, ca_path, type)
52
def get_service_cert(cls, type=STD_CERT):
53
service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
55
crt, key = ca.get_or_create_cert(service_name)
56
return crt, key, ca.get_ca_bundle()
61
log("initializing service ca", level=DEBUG)
62
if not exists(self.ca_dir):
63
self._init_ca_dir(self.ca_dir)
68
return path_join(self.ca_dir, 'private', 'cacert.key')
72
return path_join(self.ca_dir, 'cacert.pem')
76
return path_join(self.ca_dir, 'ca.cnf')
79
def signing_conf(self):
80
return path_join(self.ca_dir, 'signing.cnf')
82
def _init_ca_dir(self, ca_dir):
84
for i in ['certs', 'crl', 'newcerts', 'private']:
85
sd = path_join(ca_dir, i)
89
if not exists(path_join(ca_dir, 'serial')):
90
with open(path_join(ca_dir, 'serial'), 'w') as fh:
93
if not exists(path_join(ca_dir, 'index.txt')):
94
with open(path_join(ca_dir, 'index.txt'), 'w') as fh:
98
"""Generate the root ca's cert and key.
100
if not exists(path_join(self.ca_dir, 'ca.cnf')):
101
with open(path_join(self.ca_dir, 'ca.cnf'), 'w') as fh:
103
CA_CONF_TEMPLATE % (self.get_conf_variables()))
105
if not exists(path_join(self.ca_dir, 'signing.cnf')):
106
with open(path_join(self.ca_dir, 'signing.cnf'), 'w') as fh:
108
SIGNING_CONF_TEMPLATE % (self.get_conf_variables()))
110
if exists(self.ca_cert) or exists(self.ca_key):
111
raise RuntimeError("Initialized called when CA already exists")
112
cmd = ['openssl', 'req', '-config', self.ca_conf,
113
'-x509', '-nodes', '-newkey', 'rsa',
114
'-days', self.default_ca_expiry,
115
'-keyout', self.ca_key, '-out', self.ca_cert,
117
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
118
log("CA Init:\n %s" % output, level=DEBUG)
120
def get_conf_variables(self):
123
org_unit_name="%s service" % self.name,
124
common_name=self.name,
127
def get_or_create_cert(self, common_name):
128
if common_name in self:
129
return self.get_certificate(common_name)
130
return self.create_certificate(common_name)
132
def create_certificate(self, common_name):
133
if common_name in self:
134
return self.get_certificate(common_name)
135
key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
136
crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
137
csr_p = path_join(self.ca_dir, "certs", "%s.csr" % common_name)
138
self._create_certificate(common_name, key_p, csr_p, crt_p)
139
return self.get_certificate(common_name)
141
def get_certificate(self, common_name):
142
if common_name not in self:
143
raise ValueError("No certificate for %s" % common_name)
144
key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
145
crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
146
with open(crt_p) as fh:
148
with open(key_p) as fh:
152
def __contains__(self, common_name):
153
crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
156
def _create_certificate(self, common_name, key_p, csr_p, crt_p):
157
template_vars = self.get_conf_variables()
158
template_vars['common_name'] = common_name
159
subj = '/O=%(org_name)s/OU=%(org_unit_name)s/CN=%(common_name)s' % (
162
log("CA Create Cert %s" % common_name, level=DEBUG)
163
cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa:2048',
164
'-nodes', '-days', self.default_expiry,
165
'-keyout', key_p, '-out', csr_p, '-subj', subj]
166
subprocess.check_call(cmd, stderr=subprocess.PIPE)
167
cmd = ['openssl', 'rsa', '-in', key_p, '-out', key_p]
168
subprocess.check_call(cmd, stderr=subprocess.PIPE)
170
log("CA Sign Cert %s" % common_name, level=DEBUG)
171
if self.cert_type == MYSQL_CERT:
172
cmd = ['openssl', 'x509', '-req',
173
'-in', csr_p, '-days', self.default_expiry,
174
'-CA', self.ca_cert, '-CAkey', self.ca_key,
175
'-set_serial', '01', '-out', crt_p]
177
cmd = ['openssl', 'ca', '-config', self.signing_conf,
178
'-extensions', 'req_extensions',
179
'-days', self.default_expiry, '-notext',
180
'-in', csr_p, '-out', crt_p, '-subj', subj, '-batch']
181
log("running %s" % " ".join(cmd), level=DEBUG)
182
subprocess.check_call(cmd, stderr=subprocess.PIPE)
184
def get_ca_bundle(self):
185
with open(self.ca_cert) as fh:
189
CA_CONF_TEMPLATE = """
191
default_ca = CA_default
195
policy = policy_match
196
database = $dir/index.txt
200
new_certs_dir = $dir/newcerts
201
certificate = $dir/cacert.pem
202
private_key = $dir/private/cacert.key
203
RANDFILE = $dir/private/.rand
211
distinguished_name = ca_distinguished_name
213
x509_extensions = ca_extensions
215
[ ca_distinguished_name ]
216
organizationName = %(org_name)s
217
organizationalUnitName = %(org_unit_name)s Certificate Authority
221
countryName = optional
222
stateOrProvinceName = optional
223
organizationName = match
224
organizationalUnitName = optional
225
commonName = supplied
228
basicConstraints = critical,CA:true
229
subjectKeyIdentifier = hash
230
authorityKeyIdentifier = keyid:always, issuer
231
keyUsage = cRLSign, keyCertSign
235
SIGNING_CONF_TEMPLATE = """
237
default_ca = CA_default
241
policy = policy_match
242
database = $dir/index.txt
246
new_certs_dir = $dir/newcerts
247
certificate = $dir/cacert.pem
248
private_key = $dir/private/cacert.key
249
RANDFILE = $dir/private/.rand
257
distinguished_name = req_distinguished_name
259
x509_extensions = req_extensions
261
[ req_distinguished_name ]
262
organizationName = %(org_name)s
263
organizationalUnitName = %(org_unit_name)s machine resources
264
commonName = %(common_name)s
267
countryName = optional
268
stateOrProvinceName = optional
269
organizationName = match
270
organizationalUnitName = optional
271
commonName = supplied
274
basicConstraints = CA:false
275
subjectKeyIdentifier = hash
276
authorityKeyIdentifier = keyid:always, issuer
277
keyUsage = digitalSignature, keyEncipherment, keyAgreement
278
extendedKeyUsage = serverAuth, clientAuth