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/>.
19
from os.path import join as path_join
20
from os.path import exists
24
log = logging.getLogger("service_ca")
26
logging.basicConfig(level=logging.DEBUG)
30
# Mysql server is fairly picky about cert creation
31
# and types, spec its creation separately for now.
35
class ServiceCA(object):
37
default_expiry = str(365 * 2)
38
default_ca_expiry = str(365 * 6)
40
def __init__(self, name, ca_dir, cert_type=STD_CERT):
43
self.cert_type = cert_type
48
def get_ca(type=STD_CERT):
49
service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
50
ca_path = os.path.join(os.environ['CHARM_DIR'], 'ca')
51
ca = ServiceCA(service_name, ca_path, type)
56
def get_service_cert(cls, type=STD_CERT):
57
service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
59
crt, key = ca.get_or_create_cert(service_name)
60
return crt, key, ca.get_ca_bundle()
65
log.debug("initializing service ca")
66
if not exists(self.ca_dir):
67
self._init_ca_dir(self.ca_dir)
72
return path_join(self.ca_dir, 'private', 'cacert.key')
76
return path_join(self.ca_dir, 'cacert.pem')
80
return path_join(self.ca_dir, 'ca.cnf')
83
def signing_conf(self):
84
return path_join(self.ca_dir, 'signing.cnf')
86
def _init_ca_dir(self, ca_dir):
88
for i in ['certs', 'crl', 'newcerts', 'private']:
89
sd = path_join(ca_dir, i)
93
if not exists(path_join(ca_dir, 'serial')):
94
with open(path_join(ca_dir, 'serial'), 'wb') as fh:
97
if not exists(path_join(ca_dir, 'index.txt')):
98
with open(path_join(ca_dir, 'index.txt'), 'wb') as fh:
102
"""Generate the root ca's cert and key.
104
if not exists(path_join(self.ca_dir, 'ca.cnf')):
105
with open(path_join(self.ca_dir, 'ca.cnf'), 'wb') as fh:
107
CA_CONF_TEMPLATE % (self.get_conf_variables()))
109
if not exists(path_join(self.ca_dir, 'signing.cnf')):
110
with open(path_join(self.ca_dir, 'signing.cnf'), 'wb') as fh:
112
SIGNING_CONF_TEMPLATE % (self.get_conf_variables()))
114
if exists(self.ca_cert) or exists(self.ca_key):
115
raise RuntimeError("Initialized called when CA already exists")
116
cmd = ['openssl', 'req', '-config', self.ca_conf,
117
'-x509', '-nodes', '-newkey', 'rsa',
118
'-days', self.default_ca_expiry,
119
'-keyout', self.ca_key, '-out', self.ca_cert,
121
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
122
log.debug("CA Init:\n %s", output)
124
def get_conf_variables(self):
127
org_unit_name="%s service" % self.name,
128
common_name=self.name,
131
def get_or_create_cert(self, common_name):
132
if common_name in self:
133
return self.get_certificate(common_name)
134
return self.create_certificate(common_name)
136
def create_certificate(self, common_name):
137
if common_name in self:
138
return self.get_certificate(common_name)
139
key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
140
crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
141
csr_p = path_join(self.ca_dir, "certs", "%s.csr" % common_name)
142
self._create_certificate(common_name, key_p, csr_p, crt_p)
143
return self.get_certificate(common_name)
145
def get_certificate(self, common_name):
146
if common_name not in self:
147
raise ValueError("No certificate for %s" % common_name)
148
key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
149
crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
150
with open(crt_p) as fh:
152
with open(key_p) as fh:
156
def __contains__(self, common_name):
157
crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
160
def _create_certificate(self, common_name, key_p, csr_p, crt_p):
161
template_vars = self.get_conf_variables()
162
template_vars['common_name'] = common_name
163
subj = '/O=%(org_name)s/OU=%(org_unit_name)s/CN=%(common_name)s' % (
166
log.debug("CA Create Cert %s", common_name)
167
cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa:2048',
168
'-nodes', '-days', self.default_expiry,
169
'-keyout', key_p, '-out', csr_p, '-subj', subj]
170
subprocess.check_call(cmd)
171
cmd = ['openssl', 'rsa', '-in', key_p, '-out', key_p]
172
subprocess.check_call(cmd)
174
log.debug("CA Sign Cert %s", common_name)
175
if self.cert_type == MYSQL_CERT:
176
cmd = ['openssl', 'x509', '-req',
177
'-in', csr_p, '-days', self.default_expiry,
178
'-CA', self.ca_cert, '-CAkey', self.ca_key,
179
'-set_serial', '01', '-out', crt_p]
181
cmd = ['openssl', 'ca', '-config', self.signing_conf,
182
'-extensions', 'req_extensions',
183
'-days', self.default_expiry, '-notext',
184
'-in', csr_p, '-out', crt_p, '-subj', subj, '-batch']
185
log.debug("running %s", " ".join(cmd))
186
subprocess.check_call(cmd)
188
def get_ca_bundle(self):
189
with open(self.ca_cert) as fh:
193
CA_CONF_TEMPLATE = """
195
default_ca = CA_default
199
policy = policy_match
200
database = $dir/index.txt
204
new_certs_dir = $dir/newcerts
205
certificate = $dir/cacert.pem
206
private_key = $dir/private/cacert.key
207
RANDFILE = $dir/private/.rand
215
distinguished_name = ca_distinguished_name
217
x509_extensions = ca_extensions
219
[ ca_distinguished_name ]
220
organizationName = %(org_name)s
221
organizationalUnitName = %(org_unit_name)s Certificate Authority
225
countryName = optional
226
stateOrProvinceName = optional
227
organizationName = match
228
organizationalUnitName = optional
229
commonName = supplied
232
basicConstraints = critical,CA:true
233
subjectKeyIdentifier = hash
234
authorityKeyIdentifier = keyid:always, issuer
235
keyUsage = cRLSign, keyCertSign
239
SIGNING_CONF_TEMPLATE = """
241
default_ca = CA_default
245
policy = policy_match
246
database = $dir/index.txt
250
new_certs_dir = $dir/newcerts
251
certificate = $dir/cacert.pem
252
private_key = $dir/private/cacert.key
253
RANDFILE = $dir/private/.rand
261
distinguished_name = req_distinguished_name
263
x509_extensions = req_extensions
265
[ req_distinguished_name ]
266
organizationName = %(org_name)s
267
organizationalUnitName = %(org_unit_name)s machine resources
268
commonName = %(common_name)s
271
countryName = optional
272
stateOrProvinceName = optional
273
organizationName = match
274
organizationalUnitName = optional
275
commonName = supplied
278
basicConstraints = CA:false
279
subjectKeyIdentifier = hash
280
authorityKeyIdentifier = keyid:always, issuer
281
keyUsage = digitalSignature, keyEncipherment, keyAgreement
282
extendedKeyUsage = serverAuth, clientAuth