~canonical-ci-engineering/charms/trusty/core-image-publisher/trunk

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/contrib/ssl/service.py

  • Committer: Celso Providelo
  • Date: 2015-03-25 04:13:43 UTC
  • Revision ID: celso.providelo@canonical.com-20150325041343-jw05jaz6jscs3c8f
fork of core-image-watcher

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014-2015 Canonical Limited.
 
2
#
 
3
# This file is part of charm-helpers.
 
4
#
 
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.
 
8
#
 
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.
 
13
#
 
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/>.
 
16
 
 
17
import logging
 
18
import os
 
19
from os.path import join as path_join
 
20
from os.path import exists
 
21
import subprocess
 
22
 
 
23
 
 
24
log = logging.getLogger("service_ca")
 
25
 
 
26
logging.basicConfig(level=logging.DEBUG)
 
27
 
 
28
STD_CERT = "standard"
 
29
 
 
30
# Mysql server is fairly picky about cert creation
 
31
# and types, spec its creation separately for now.
 
32
MYSQL_CERT = "mysql"
 
33
 
 
34
 
 
35
class ServiceCA(object):
 
36
 
 
37
    default_expiry = str(365 * 2)
 
38
    default_ca_expiry = str(365 * 6)
 
39
 
 
40
    def __init__(self, name, ca_dir, cert_type=STD_CERT):
 
41
        self.name = name
 
42
        self.ca_dir = ca_dir
 
43
        self.cert_type = cert_type
 
44
 
 
45
    ###############
 
46
    # Hook Helper API
 
47
    @staticmethod
 
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)
 
52
        ca.init()
 
53
        return ca
 
54
 
 
55
    @classmethod
 
56
    def get_service_cert(cls, type=STD_CERT):
 
57
        service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
 
58
        ca = cls.get_ca()
 
59
        crt, key = ca.get_or_create_cert(service_name)
 
60
        return crt, key, ca.get_ca_bundle()
 
61
 
 
62
    ###############
 
63
 
 
64
    def init(self):
 
65
        log.debug("initializing service ca")
 
66
        if not exists(self.ca_dir):
 
67
            self._init_ca_dir(self.ca_dir)
 
68
            self._init_ca()
 
69
 
 
70
    @property
 
71
    def ca_key(self):
 
72
        return path_join(self.ca_dir, 'private', 'cacert.key')
 
73
 
 
74
    @property
 
75
    def ca_cert(self):
 
76
        return path_join(self.ca_dir, 'cacert.pem')
 
77
 
 
78
    @property
 
79
    def ca_conf(self):
 
80
        return path_join(self.ca_dir, 'ca.cnf')
 
81
 
 
82
    @property
 
83
    def signing_conf(self):
 
84
        return path_join(self.ca_dir, 'signing.cnf')
 
85
 
 
86
    def _init_ca_dir(self, ca_dir):
 
87
        os.mkdir(ca_dir)
 
88
        for i in ['certs', 'crl', 'newcerts', 'private']:
 
89
            sd = path_join(ca_dir, i)
 
90
            if not exists(sd):
 
91
                os.mkdir(sd)
 
92
 
 
93
        if not exists(path_join(ca_dir, 'serial')):
 
94
            with open(path_join(ca_dir, 'serial'), 'wb') as fh:
 
95
                fh.write('02\n')
 
96
 
 
97
        if not exists(path_join(ca_dir, 'index.txt')):
 
98
            with open(path_join(ca_dir, 'index.txt'), 'wb') as fh:
 
99
                fh.write('')
 
100
 
 
101
    def _init_ca(self):
 
102
        """Generate the root ca's cert and key.
 
103
        """
 
104
        if not exists(path_join(self.ca_dir, 'ca.cnf')):
 
105
            with open(path_join(self.ca_dir, 'ca.cnf'), 'wb') as fh:
 
106
                fh.write(
 
107
                    CA_CONF_TEMPLATE % (self.get_conf_variables()))
 
108
 
 
109
        if not exists(path_join(self.ca_dir, 'signing.cnf')):
 
110
            with open(path_join(self.ca_dir, 'signing.cnf'), 'wb') as fh:
 
111
                fh.write(
 
112
                    SIGNING_CONF_TEMPLATE % (self.get_conf_variables()))
 
113
 
 
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,
 
120
               '-outform', 'PEM']
 
121
        output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
 
122
        log.debug("CA Init:\n %s", output)
 
123
 
 
124
    def get_conf_variables(self):
 
125
        return dict(
 
126
            org_name="juju",
 
127
            org_unit_name="%s service" % self.name,
 
128
            common_name=self.name,
 
129
            ca_dir=self.ca_dir)
 
130
 
 
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)
 
135
 
 
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)
 
144
 
 
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:
 
151
            crt = fh.read()
 
152
        with open(key_p) as fh:
 
153
            key = fh.read()
 
154
        return crt, key
 
155
 
 
156
    def __contains__(self, common_name):
 
157
        crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
 
158
        return exists(crt_p)
 
159
 
 
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' % (
 
164
            template_vars)
 
165
 
 
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)
 
173
 
 
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]
 
180
        else:
 
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)
 
187
 
 
188
    def get_ca_bundle(self):
 
189
        with open(self.ca_cert) as fh:
 
190
            return fh.read()
 
191
 
 
192
 
 
193
CA_CONF_TEMPLATE = """
 
194
[ ca ]
 
195
default_ca = CA_default
 
196
 
 
197
[ CA_default ]
 
198
dir                     = %(ca_dir)s
 
199
policy                  = policy_match
 
200
database                = $dir/index.txt
 
201
serial                  = $dir/serial
 
202
certs                   = $dir/certs
 
203
crl_dir                 = $dir/crl
 
204
new_certs_dir           = $dir/newcerts
 
205
certificate             = $dir/cacert.pem
 
206
private_key             = $dir/private/cacert.key
 
207
RANDFILE                = $dir/private/.rand
 
208
default_md              = default
 
209
 
 
210
[ req ]
 
211
default_bits            = 1024
 
212
default_md              = sha1
 
213
 
 
214
prompt                  = no
 
215
distinguished_name      = ca_distinguished_name
 
216
 
 
217
x509_extensions         = ca_extensions
 
218
 
 
219
[ ca_distinguished_name ]
 
220
organizationName        = %(org_name)s
 
221
organizationalUnitName  = %(org_unit_name)s Certificate Authority
 
222
 
 
223
 
 
224
[ policy_match ]
 
225
countryName             = optional
 
226
stateOrProvinceName     = optional
 
227
organizationName        = match
 
228
organizationalUnitName  = optional
 
229
commonName              = supplied
 
230
 
 
231
[ ca_extensions ]
 
232
basicConstraints        = critical,CA:true
 
233
subjectKeyIdentifier    = hash
 
234
authorityKeyIdentifier  = keyid:always, issuer
 
235
keyUsage                = cRLSign, keyCertSign
 
236
"""
 
237
 
 
238
 
 
239
SIGNING_CONF_TEMPLATE = """
 
240
[ ca ]
 
241
default_ca = CA_default
 
242
 
 
243
[ CA_default ]
 
244
dir                     = %(ca_dir)s
 
245
policy                  = policy_match
 
246
database                = $dir/index.txt
 
247
serial                  = $dir/serial
 
248
certs                   = $dir/certs
 
249
crl_dir                 = $dir/crl
 
250
new_certs_dir           = $dir/newcerts
 
251
certificate             = $dir/cacert.pem
 
252
private_key             = $dir/private/cacert.key
 
253
RANDFILE                = $dir/private/.rand
 
254
default_md              = default
 
255
 
 
256
[ req ]
 
257
default_bits            = 1024
 
258
default_md              = sha1
 
259
 
 
260
prompt                  = no
 
261
distinguished_name      = req_distinguished_name
 
262
 
 
263
x509_extensions         = req_extensions
 
264
 
 
265
[ req_distinguished_name ]
 
266
organizationName        = %(org_name)s
 
267
organizationalUnitName  = %(org_unit_name)s machine resources
 
268
commonName              = %(common_name)s
 
269
 
 
270
[ policy_match ]
 
271
countryName             = optional
 
272
stateOrProvinceName     = optional
 
273
organizationName        = match
 
274
organizationalUnitName  = optional
 
275
commonName              = supplied
 
276
 
 
277
[ req_extensions ]
 
278
basicConstraints        = CA:false
 
279
subjectKeyIdentifier    = hash
 
280
authorityKeyIdentifier  = keyid:always, issuer
 
281
keyUsage                = digitalSignature, keyEncipherment, keyAgreement
 
282
extendedKeyUsage        = serverAuth, clientAuth
 
283
"""