Merge lp:~josvaz/charms/trusty/bip/client_side_ssl-with_helper-lp1604894 into lp:charms/trusty/bip
- Trusty Tahr (14.04)
- client_side_ssl-with_helper-lp1604894
- Merge into trunk
Status: | Needs review | ||||
---|---|---|---|---|---|
Proposed branch: | lp:~josvaz/charms/trusty/bip/client_side_ssl-with_helper-lp1604894 | ||||
Merge into: | lp:charms/trusty/bip | ||||
Prerequisite: | lp:~josvaz/charms/trusty/bip/charmhelpers-cleanup | ||||
Diff against target: |
618 lines (+496/-7) 8 files modified
charm-helpers.yaml (+1/-0) config.yaml (+4/-0) hooks/hooks.py (+30/-6) lib/charmhelpers/contrib/__init__.py (+13/-0) lib/charmhelpers/contrib/ssl/__init__.py (+92/-0) lib/charmhelpers/contrib/ssl/service.py (+277/-0) templates/bip_conf.template (+1/-1) tests/11-deploy-ssl (+78/-0) |
||||
To merge this branch: | bzr merge lp:~josvaz/charms/trusty/bip/client_side_ssl-with_helper-lp1604894 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Review Queue (community) | automated testing | Approve | |
Brad Marshall (community) | Approve | ||
Pen Gale (community) | Approve | ||
charmers | Pending | ||
Review via email: mp+301802@code.launchpad.net |
Commit message
Description of the change
This should replace https:/
It goes after this MP:
https:/
And fixes:
https:/
X-CPC-Summary-Skip: 1
Pen Gale (pengale) wrote : | # |
Code looks sensible, and does not break existing tests, but I would like to see unit or integration tests that cover the self signing code.
Jose L. VG (josvaz) wrote : | # |
Would a test that calls gen_certs() and verifies the generated pem files are for a self signed cert & key suffice?
Jose L. VG (josvaz) wrote : | # |
Also this MP depends on this other to be done first:
https:/
Do I need to fix anything in that one?
- 32. By Jose L. VG
-
Adding a bip ssl deploy test (validates cert & handshake)
Jose L. VG (josvaz) wrote : | # |
Added a 11-deploy-ssl test that deploys a ssl enabled bip on port 6697, it retrieves the server certificate and then validates against it a SSL connection.
I guess this covers all the new feature code.
Please review!
Pen Gale (pengale) wrote : | # |
Hello Jose,
Thank you for all your work on this. The new test looks great, and passes when I run bundletester. :-)
I am +1 on this.
There is an outstanding issue with merging and promulgating this, which I noted in the related PR: the bip charm is currently own by ~charmers, rather than a specific maintainer. I can't fix that myself, but I am poking people about it; I'll ping this ticket when the issue is fixed.
Pen Gale (pengale) wrote : | # |
Dropping in an update here. The next steps for this change need to be:
@bradm needs to update his branch of this charm with the latest from ~charmers, then merge this PR into it. Then he needs to promulgate a charm from his namespace, and request that a charmer merge it.
That should update the charm store so that bip gets pointed at bradm's namespace (since he's the maintainer), and should allow bradm to more easily maintain the charm going forward.
@josvaz: you most likely don't need to do anything else with this PR; the maintainer (bradm) just needs to do some housekeeping to get things merged.
Jose L. VG (josvaz) wrote : | # |
Thanks fo rthe update Pete!
Jose
On Thu, Aug 25, 2016 at 3:43 PM, petevg <email address hidden>
wrote:
> Dropping in an update here. The next steps for this change need to be:
>
> @bradm needs to update his branch of this charm with the latest from
> ~charmers, then merge this PR into it. Then he needs to promulgate a charm
> from his namespace, and request that a charmer merge it.
>
> That should update the charm store so that bip gets pointed at bradm's
> namespace (since he's the maintainer), and should allow bradm to more
> easily maintain the charm going forward.
>
> @josvaz: you most likely don't need to do anything else with this PR; the
> maintainer (bradm) just needs to do some housekeeping to get things merged.
> --
> https:/
> client_
> You are the owner of lp:~josvaz/charms/trusty/bip/
> client_
>
Pen Gale (pengale) wrote : | # |
Poked bradm about confirming that bip is moved and closing this review out.
Brad Marshall (brad-marshall) wrote : | # |
Looks good to me, merged into my own branch and am working on getting it updated in the charmstore.
Jose L. VG (josvaz) wrote : | # |
Thanks Brad, let me know if/when this MP needs to be marked Merged manually.
Review Queue (review-queue) wrote : | # |
The results (PASS) are in and available here: http://
Review Queue (review-queue) wrote : | # |
The results (PASS) are in and available here: http://
Unmerged revisions
- 32. By Jose L. VG
-
Adding a bip ssl deploy test (validates cert & handshake)
- 31. By Jose L. VG
-
Added support to client_side_ssl with charmhelper contrib.ssl
- 30. By Jose L. VG
-
Got charmhelpers for fetch as apt_install is now there
- 29. By Jose L. VG
-
Moved from charm-helpers/
charmhelpers to just charmhelpers (plus clean-up) - 28. By Jose L. VG
-
Result of removing charmhelpers and re-syncing it
- 27. By Jose L. VG
-
Added charm-helpers.yaml to manage helpers with charm_helpers_sync tool
- 26. By Jose L. VG
-
Flake8 suggested fixes, including unused imports
Preview Diff
1 | === modified file 'charm-helpers.yaml' |
2 | --- charm-helpers.yaml 2016-08-19 17:00:35 +0000 |
3 | +++ charm-helpers.yaml 2016-08-19 17:00:35 +0000 |
4 | @@ -3,3 +3,4 @@ |
5 | include: |
6 | - core |
7 | - fetch |
8 | + - contrib.ssl |
9 | |
10 | === modified file 'config.yaml' |
11 | --- config.yaml 2016-07-20 10:26:23 +0000 |
12 | +++ config.yaml 2016-08-19 17:00:35 +0000 |
13 | @@ -35,6 +35,10 @@ |
14 | type: string |
15 | default: '{"oftc": {host: "irc.oftc.net", port: 6667}, "freenode": {host: "irc.freenode.net", port: 6667}}' |
16 | description: IRC Networks defined |
17 | + client_side_ssl: |
18 | + type: string |
19 | + default: "false" |
20 | + description: Use SSL to connect to bip from your client |
21 | user: |
22 | type: string |
23 | description: User definitions |
24 | |
25 | === modified file 'hooks/hooks.py' |
26 | --- hooks/hooks.py 2016-08-19 17:00:35 +0000 |
27 | +++ hooks/hooks.py 2016-08-19 17:00:35 +0000 |
28 | @@ -3,10 +3,13 @@ |
29 | # Copyright 2013 Canonical Ltd. All rights reserved. |
30 | # Author: Brad Marshall <brad.marshall@canonical.com> |
31 | |
32 | +import glob |
33 | +import pwd |
34 | import os |
35 | import sys |
36 | import json |
37 | import yaml |
38 | +import subprocess |
39 | |
40 | local_copy = os.path.join( |
41 | os.path.dirname(os.path.abspath(os.path.dirname(__file__))), |
42 | @@ -29,6 +32,8 @@ |
43 | open_port, |
44 | ) |
45 | |
46 | +from charmhelpers.contrib import ssl |
47 | + |
48 | hook_dir = os.path.abspath(os.path.dirname(__file__)) |
49 | charm_dir = os.path.dirname(hook_dir) |
50 | |
51 | @@ -41,6 +46,7 @@ |
52 | user_config = config().get('user') |
53 | backlog_lines = config().get('backlog_lines') |
54 | backlog_msg_only = config().get('backlog_msg_only') |
55 | +client_side_ssl = config().get('client_side_ssl') |
56 | backlog_always = config().get('backlog_always') |
57 | bip_conf = '/etc/bip.conf' |
58 | bip_defaults = '/etc/default/bip' |
59 | @@ -48,27 +54,44 @@ |
60 | required_pkgs = [ |
61 | 'bip', |
62 | 'python-jinja2', |
63 | + 'openssl', |
64 | ] |
65 | |
66 | hooks = Hooks() |
67 | |
68 | |
69 | +def gen_certs(): |
70 | + """gen_certs generates bip SSL certificates with openssl CLI""" |
71 | + bip_dir = "/var/lib/bip" |
72 | + pem = "%s/bip.pem" % bip_dir |
73 | + ssl.generate_selfsigned(keyfile=pem, certfile=pem, keysize=4096, |
74 | + cn="BipServerSert") |
75 | + os.chmod('%s/bip.pem' % bip_dir, 0600) |
76 | + bip_uid = pwd.getpwnam("bip").pw_uid |
77 | + os.chown('%s/bip.pem' % bip_dir, bip_uid, -1) |
78 | + |
79 | +def setup(): |
80 | + """setup executes post configuration actions, like certificate generation""" |
81 | + if client_side_ssl.lower() == 'true': |
82 | + gen_certs() |
83 | + |
84 | + |
85 | def update_config(): |
86 | from jinja2 import Environment, FileSystemLoader |
87 | if network_config: |
88 | - # Try originally supported JSON formatted config |
89 | + ## Try originally supported JSON formatted config |
90 | try: |
91 | network = json.loads(network_config) |
92 | - # else use YAML (current): |
93 | + ## else use YAML (current): |
94 | except ValueError: |
95 | network = yaml.load(network_config) |
96 | else: |
97 | network = {} |
98 | if user_config: |
99 | - # Try originally supported JSON formatted config |
100 | + ## Try originally supported JSON formatted config |
101 | try: |
102 | user = json.loads(user_config) |
103 | - # else use YAML (current): |
104 | + ## else use YAML (current): |
105 | except ValueError: |
106 | user = yaml.load(user_config) |
107 | else: |
108 | @@ -87,12 +110,13 @@ |
109 | 'user': user, |
110 | 'backlog_lines': backlog_lines, |
111 | 'backlog_msg_only': backlog_msg_only, |
112 | + 'client_side_ssl': client_side_ssl, |
113 | 'backlog_always': backlog_always, |
114 | } |
115 | - template = template_env.get_template('bip_conf.template').render( |
116 | - templ_vars) |
117 | + template = template_env.get_template('bip_conf.template').render(templ_vars) |
118 | with open(bip_conf, 'w') as bip_conf_config: |
119 | bip_conf_config.write(str(template)) |
120 | + setup() |
121 | |
122 | |
123 | @hooks.hook() |
124 | |
125 | === added directory 'lib/charmhelpers/contrib' |
126 | === added file 'lib/charmhelpers/contrib/__init__.py' |
127 | --- lib/charmhelpers/contrib/__init__.py 1970-01-01 00:00:00 +0000 |
128 | +++ lib/charmhelpers/contrib/__init__.py 2016-08-19 17:00:35 +0000 |
129 | @@ -0,0 +1,13 @@ |
130 | +# Copyright 2014-2015 Canonical Limited. |
131 | +# |
132 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
133 | +# you may not use this file except in compliance with the License. |
134 | +# You may obtain a copy of the License at |
135 | +# |
136 | +# http://www.apache.org/licenses/LICENSE-2.0 |
137 | +# |
138 | +# Unless required by applicable law or agreed to in writing, software |
139 | +# distributed under the License is distributed on an "AS IS" BASIS, |
140 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
141 | +# See the License for the specific language governing permissions and |
142 | +# limitations under the License. |
143 | |
144 | === added directory 'lib/charmhelpers/contrib/ssl' |
145 | === added file 'lib/charmhelpers/contrib/ssl/__init__.py' |
146 | --- lib/charmhelpers/contrib/ssl/__init__.py 1970-01-01 00:00:00 +0000 |
147 | +++ lib/charmhelpers/contrib/ssl/__init__.py 2016-08-19 17:00:35 +0000 |
148 | @@ -0,0 +1,92 @@ |
149 | +# Copyright 2014-2015 Canonical Limited. |
150 | +# |
151 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
152 | +# you may not use this file except in compliance with the License. |
153 | +# You may obtain a copy of the License at |
154 | +# |
155 | +# http://www.apache.org/licenses/LICENSE-2.0 |
156 | +# |
157 | +# Unless required by applicable law or agreed to in writing, software |
158 | +# distributed under the License is distributed on an "AS IS" BASIS, |
159 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
160 | +# See the License for the specific language governing permissions and |
161 | +# limitations under the License. |
162 | + |
163 | +import subprocess |
164 | +from charmhelpers.core import hookenv |
165 | + |
166 | + |
167 | +def generate_selfsigned(keyfile, certfile, keysize="1024", config=None, subject=None, cn=None): |
168 | + """Generate selfsigned SSL keypair |
169 | + |
170 | + You must provide one of the 3 optional arguments: |
171 | + config, subject or cn |
172 | + If more than one is provided the leftmost will be used |
173 | + |
174 | + Arguments: |
175 | + keyfile -- (required) full path to the keyfile to be created |
176 | + certfile -- (required) full path to the certfile to be created |
177 | + keysize -- (optional) SSL key length |
178 | + config -- (optional) openssl configuration file |
179 | + subject -- (optional) dictionary with SSL subject variables |
180 | + cn -- (optional) cerfificate common name |
181 | + |
182 | + Required keys in subject dict: |
183 | + cn -- Common name (eq. FQDN) |
184 | + |
185 | + Optional keys in subject dict |
186 | + country -- Country Name (2 letter code) |
187 | + state -- State or Province Name (full name) |
188 | + locality -- Locality Name (eg, city) |
189 | + organization -- Organization Name (eg, company) |
190 | + organizational_unit -- Organizational Unit Name (eg, section) |
191 | + email -- Email Address |
192 | + """ |
193 | + |
194 | + cmd = [] |
195 | + if config: |
196 | + cmd = ["/usr/bin/openssl", "req", "-new", "-newkey", |
197 | + "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509", |
198 | + "-keyout", keyfile, |
199 | + "-out", certfile, "-config", config] |
200 | + elif subject: |
201 | + ssl_subject = "" |
202 | + if "country" in subject: |
203 | + ssl_subject = ssl_subject + "/C={}".format(subject["country"]) |
204 | + if "state" in subject: |
205 | + ssl_subject = ssl_subject + "/ST={}".format(subject["state"]) |
206 | + if "locality" in subject: |
207 | + ssl_subject = ssl_subject + "/L={}".format(subject["locality"]) |
208 | + if "organization" in subject: |
209 | + ssl_subject = ssl_subject + "/O={}".format(subject["organization"]) |
210 | + if "organizational_unit" in subject: |
211 | + ssl_subject = ssl_subject + "/OU={}".format(subject["organizational_unit"]) |
212 | + if "cn" in subject: |
213 | + ssl_subject = ssl_subject + "/CN={}".format(subject["cn"]) |
214 | + else: |
215 | + hookenv.log("When using \"subject\" argument you must " |
216 | + "provide \"cn\" field at very least") |
217 | + return False |
218 | + if "email" in subject: |
219 | + ssl_subject = ssl_subject + "/emailAddress={}".format(subject["email"]) |
220 | + |
221 | + cmd = ["/usr/bin/openssl", "req", "-new", "-newkey", |
222 | + "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509", |
223 | + "-keyout", keyfile, |
224 | + "-out", certfile, "-subj", ssl_subject] |
225 | + elif cn: |
226 | + cmd = ["/usr/bin/openssl", "req", "-new", "-newkey", |
227 | + "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509", |
228 | + "-keyout", keyfile, |
229 | + "-out", certfile, "-subj", "/CN={}".format(cn)] |
230 | + |
231 | + if not cmd: |
232 | + hookenv.log("No config, subject or cn provided," |
233 | + "unable to generate self signed SSL certificates") |
234 | + return False |
235 | + try: |
236 | + subprocess.check_call(cmd) |
237 | + return True |
238 | + except Exception as e: |
239 | + print("Execution of openssl command failed:\n{}".format(e)) |
240 | + return False |
241 | |
242 | === added file 'lib/charmhelpers/contrib/ssl/service.py' |
243 | --- lib/charmhelpers/contrib/ssl/service.py 1970-01-01 00:00:00 +0000 |
244 | +++ lib/charmhelpers/contrib/ssl/service.py 2016-08-19 17:00:35 +0000 |
245 | @@ -0,0 +1,277 @@ |
246 | +# Copyright 2014-2015 Canonical Limited. |
247 | +# |
248 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
249 | +# you may not use this file except in compliance with the License. |
250 | +# You may obtain a copy of the License at |
251 | +# |
252 | +# http://www.apache.org/licenses/LICENSE-2.0 |
253 | +# |
254 | +# Unless required by applicable law or agreed to in writing, software |
255 | +# distributed under the License is distributed on an "AS IS" BASIS, |
256 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
257 | +# See the License for the specific language governing permissions and |
258 | +# limitations under the License. |
259 | + |
260 | +import os |
261 | +from os.path import join as path_join |
262 | +from os.path import exists |
263 | +import subprocess |
264 | + |
265 | +from charmhelpers.core.hookenv import log, DEBUG |
266 | + |
267 | +STD_CERT = "standard" |
268 | + |
269 | +# Mysql server is fairly picky about cert creation |
270 | +# and types, spec its creation separately for now. |
271 | +MYSQL_CERT = "mysql" |
272 | + |
273 | + |
274 | +class ServiceCA(object): |
275 | + |
276 | + default_expiry = str(365 * 2) |
277 | + default_ca_expiry = str(365 * 6) |
278 | + |
279 | + def __init__(self, name, ca_dir, cert_type=STD_CERT): |
280 | + self.name = name |
281 | + self.ca_dir = ca_dir |
282 | + self.cert_type = cert_type |
283 | + |
284 | + ############### |
285 | + # Hook Helper API |
286 | + @staticmethod |
287 | + def get_ca(type=STD_CERT): |
288 | + service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0] |
289 | + ca_path = os.path.join(os.environ['CHARM_DIR'], 'ca') |
290 | + ca = ServiceCA(service_name, ca_path, type) |
291 | + ca.init() |
292 | + return ca |
293 | + |
294 | + @classmethod |
295 | + def get_service_cert(cls, type=STD_CERT): |
296 | + service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0] |
297 | + ca = cls.get_ca() |
298 | + crt, key = ca.get_or_create_cert(service_name) |
299 | + return crt, key, ca.get_ca_bundle() |
300 | + |
301 | + ############### |
302 | + |
303 | + def init(self): |
304 | + log("initializing service ca", level=DEBUG) |
305 | + if not exists(self.ca_dir): |
306 | + self._init_ca_dir(self.ca_dir) |
307 | + self._init_ca() |
308 | + |
309 | + @property |
310 | + def ca_key(self): |
311 | + return path_join(self.ca_dir, 'private', 'cacert.key') |
312 | + |
313 | + @property |
314 | + def ca_cert(self): |
315 | + return path_join(self.ca_dir, 'cacert.pem') |
316 | + |
317 | + @property |
318 | + def ca_conf(self): |
319 | + return path_join(self.ca_dir, 'ca.cnf') |
320 | + |
321 | + @property |
322 | + def signing_conf(self): |
323 | + return path_join(self.ca_dir, 'signing.cnf') |
324 | + |
325 | + def _init_ca_dir(self, ca_dir): |
326 | + os.mkdir(ca_dir) |
327 | + for i in ['certs', 'crl', 'newcerts', 'private']: |
328 | + sd = path_join(ca_dir, i) |
329 | + if not exists(sd): |
330 | + os.mkdir(sd) |
331 | + |
332 | + if not exists(path_join(ca_dir, 'serial')): |
333 | + with open(path_join(ca_dir, 'serial'), 'w') as fh: |
334 | + fh.write('02\n') |
335 | + |
336 | + if not exists(path_join(ca_dir, 'index.txt')): |
337 | + with open(path_join(ca_dir, 'index.txt'), 'w') as fh: |
338 | + fh.write('') |
339 | + |
340 | + def _init_ca(self): |
341 | + """Generate the root ca's cert and key. |
342 | + """ |
343 | + if not exists(path_join(self.ca_dir, 'ca.cnf')): |
344 | + with open(path_join(self.ca_dir, 'ca.cnf'), 'w') as fh: |
345 | + fh.write( |
346 | + CA_CONF_TEMPLATE % (self.get_conf_variables())) |
347 | + |
348 | + if not exists(path_join(self.ca_dir, 'signing.cnf')): |
349 | + with open(path_join(self.ca_dir, 'signing.cnf'), 'w') as fh: |
350 | + fh.write( |
351 | + SIGNING_CONF_TEMPLATE % (self.get_conf_variables())) |
352 | + |
353 | + if exists(self.ca_cert) or exists(self.ca_key): |
354 | + raise RuntimeError("Initialized called when CA already exists") |
355 | + cmd = ['openssl', 'req', '-config', self.ca_conf, |
356 | + '-x509', '-nodes', '-newkey', 'rsa', |
357 | + '-days', self.default_ca_expiry, |
358 | + '-keyout', self.ca_key, '-out', self.ca_cert, |
359 | + '-outform', 'PEM'] |
360 | + output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) |
361 | + log("CA Init:\n %s" % output, level=DEBUG) |
362 | + |
363 | + def get_conf_variables(self): |
364 | + return dict( |
365 | + org_name="juju", |
366 | + org_unit_name="%s service" % self.name, |
367 | + common_name=self.name, |
368 | + ca_dir=self.ca_dir) |
369 | + |
370 | + def get_or_create_cert(self, common_name): |
371 | + if common_name in self: |
372 | + return self.get_certificate(common_name) |
373 | + return self.create_certificate(common_name) |
374 | + |
375 | + def create_certificate(self, common_name): |
376 | + if common_name in self: |
377 | + return self.get_certificate(common_name) |
378 | + key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name) |
379 | + crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name) |
380 | + csr_p = path_join(self.ca_dir, "certs", "%s.csr" % common_name) |
381 | + self._create_certificate(common_name, key_p, csr_p, crt_p) |
382 | + return self.get_certificate(common_name) |
383 | + |
384 | + def get_certificate(self, common_name): |
385 | + if common_name not in self: |
386 | + raise ValueError("No certificate for %s" % common_name) |
387 | + key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name) |
388 | + crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name) |
389 | + with open(crt_p) as fh: |
390 | + crt = fh.read() |
391 | + with open(key_p) as fh: |
392 | + key = fh.read() |
393 | + return crt, key |
394 | + |
395 | + def __contains__(self, common_name): |
396 | + crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name) |
397 | + return exists(crt_p) |
398 | + |
399 | + def _create_certificate(self, common_name, key_p, csr_p, crt_p): |
400 | + template_vars = self.get_conf_variables() |
401 | + template_vars['common_name'] = common_name |
402 | + subj = '/O=%(org_name)s/OU=%(org_unit_name)s/CN=%(common_name)s' % ( |
403 | + template_vars) |
404 | + |
405 | + log("CA Create Cert %s" % common_name, level=DEBUG) |
406 | + cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa:2048', |
407 | + '-nodes', '-days', self.default_expiry, |
408 | + '-keyout', key_p, '-out', csr_p, '-subj', subj] |
409 | + subprocess.check_call(cmd, stderr=subprocess.PIPE) |
410 | + cmd = ['openssl', 'rsa', '-in', key_p, '-out', key_p] |
411 | + subprocess.check_call(cmd, stderr=subprocess.PIPE) |
412 | + |
413 | + log("CA Sign Cert %s" % common_name, level=DEBUG) |
414 | + if self.cert_type == MYSQL_CERT: |
415 | + cmd = ['openssl', 'x509', '-req', |
416 | + '-in', csr_p, '-days', self.default_expiry, |
417 | + '-CA', self.ca_cert, '-CAkey', self.ca_key, |
418 | + '-set_serial', '01', '-out', crt_p] |
419 | + else: |
420 | + cmd = ['openssl', 'ca', '-config', self.signing_conf, |
421 | + '-extensions', 'req_extensions', |
422 | + '-days', self.default_expiry, '-notext', |
423 | + '-in', csr_p, '-out', crt_p, '-subj', subj, '-batch'] |
424 | + log("running %s" % " ".join(cmd), level=DEBUG) |
425 | + subprocess.check_call(cmd, stderr=subprocess.PIPE) |
426 | + |
427 | + def get_ca_bundle(self): |
428 | + with open(self.ca_cert) as fh: |
429 | + return fh.read() |
430 | + |
431 | + |
432 | +CA_CONF_TEMPLATE = """ |
433 | +[ ca ] |
434 | +default_ca = CA_default |
435 | + |
436 | +[ CA_default ] |
437 | +dir = %(ca_dir)s |
438 | +policy = policy_match |
439 | +database = $dir/index.txt |
440 | +serial = $dir/serial |
441 | +certs = $dir/certs |
442 | +crl_dir = $dir/crl |
443 | +new_certs_dir = $dir/newcerts |
444 | +certificate = $dir/cacert.pem |
445 | +private_key = $dir/private/cacert.key |
446 | +RANDFILE = $dir/private/.rand |
447 | +default_md = default |
448 | + |
449 | +[ req ] |
450 | +default_bits = 1024 |
451 | +default_md = sha1 |
452 | + |
453 | +prompt = no |
454 | +distinguished_name = ca_distinguished_name |
455 | + |
456 | +x509_extensions = ca_extensions |
457 | + |
458 | +[ ca_distinguished_name ] |
459 | +organizationName = %(org_name)s |
460 | +organizationalUnitName = %(org_unit_name)s Certificate Authority |
461 | + |
462 | + |
463 | +[ policy_match ] |
464 | +countryName = optional |
465 | +stateOrProvinceName = optional |
466 | +organizationName = match |
467 | +organizationalUnitName = optional |
468 | +commonName = supplied |
469 | + |
470 | +[ ca_extensions ] |
471 | +basicConstraints = critical,CA:true |
472 | +subjectKeyIdentifier = hash |
473 | +authorityKeyIdentifier = keyid:always, issuer |
474 | +keyUsage = cRLSign, keyCertSign |
475 | +""" |
476 | + |
477 | + |
478 | +SIGNING_CONF_TEMPLATE = """ |
479 | +[ ca ] |
480 | +default_ca = CA_default |
481 | + |
482 | +[ CA_default ] |
483 | +dir = %(ca_dir)s |
484 | +policy = policy_match |
485 | +database = $dir/index.txt |
486 | +serial = $dir/serial |
487 | +certs = $dir/certs |
488 | +crl_dir = $dir/crl |
489 | +new_certs_dir = $dir/newcerts |
490 | +certificate = $dir/cacert.pem |
491 | +private_key = $dir/private/cacert.key |
492 | +RANDFILE = $dir/private/.rand |
493 | +default_md = default |
494 | + |
495 | +[ req ] |
496 | +default_bits = 1024 |
497 | +default_md = sha1 |
498 | + |
499 | +prompt = no |
500 | +distinguished_name = req_distinguished_name |
501 | + |
502 | +x509_extensions = req_extensions |
503 | + |
504 | +[ req_distinguished_name ] |
505 | +organizationName = %(org_name)s |
506 | +organizationalUnitName = %(org_unit_name)s machine resources |
507 | +commonName = %(common_name)s |
508 | + |
509 | +[ policy_match ] |
510 | +countryName = optional |
511 | +stateOrProvinceName = optional |
512 | +organizationName = match |
513 | +organizationalUnitName = optional |
514 | +commonName = supplied |
515 | + |
516 | +[ req_extensions ] |
517 | +basicConstraints = CA:false |
518 | +subjectKeyIdentifier = hash |
519 | +authorityKeyIdentifier = keyid:always, issuer |
520 | +keyUsage = digitalSignature, keyEncipherment, keyAgreement |
521 | +extendedKeyUsage = serverAuth, clientAuth |
522 | +""" |
523 | |
524 | === modified file 'templates/bip_conf.template' |
525 | --- templates/bip_conf.template 2016-07-20 10:26:23 +0000 |
526 | +++ templates/bip_conf.template 2016-08-19 17:00:35 +0000 |
527 | @@ -2,7 +2,7 @@ |
528 | |
529 | port = {{ listen_port }}; |
530 | |
531 | -client_side_ssl = false; |
532 | +client_side_ssl = {{ client_side_ssl }}; |
533 | |
534 | log_level = 3; |
535 | |
536 | |
537 | === added file 'tests/11-deploy-ssl' |
538 | --- tests/11-deploy-ssl 1970-01-01 00:00:00 +0000 |
539 | +++ tests/11-deploy-ssl 2016-08-19 17:00:35 +0000 |
540 | @@ -0,0 +1,78 @@ |
541 | +#!/usr/bin/env python3 |
542 | + |
543 | +import amulet |
544 | +import os |
545 | +import pprint |
546 | +import ssl |
547 | +import socket |
548 | +import unittest |
549 | + |
550 | +class TestSSLDeployment(unittest.TestCase): |
551 | + @classmethod |
552 | + def setUpClass(cls): |
553 | + cls.deployment = amulet.Deployment() |
554 | + |
555 | + cls.deployment.add('bip-ssl', charm='bip') |
556 | + |
557 | + # configure ssl and new port |
558 | + cls.deployment.configure('bip-ssl', { |
559 | + 'client_side_ssl': 'true', |
560 | + 'listen_port': '6697' |
561 | + }) |
562 | + |
563 | + cls.deployment.expose('bip-ssl') |
564 | + |
565 | + try: |
566 | + cls.deployment.setup(timeout=900, cleanup=False) |
567 | + cls.deployment.sentry.wait() |
568 | + except amulet.helpers.TimeoutError: |
569 | + amulet.raise_status(amulet.SKIP, |
570 | + msg="Environment wasn't stood up in time") |
571 | + except: |
572 | + raise |
573 | + |
574 | + cls.unit = cls.deployment.sentry['bip-ssl'][0] |
575 | + cls.ipaddr = cls.unit.info['public-address'] |
576 | + cls.ports = cls.unit.info['open-ports'] |
577 | + |
578 | + def test_1_check_running(self): |
579 | + output = self.unit.run('service bip status') |
580 | + service_active = 'bip is running' in str(output) |
581 | + if service_active: |
582 | + print("Found running bip on %s" % self.ipaddr) |
583 | + #amulet.raise_status(amulet.PASS, msg=message) |
584 | + else: |
585 | + message = "Failed to find running bip" |
586 | + amulet.raise_status(amulet.FAIL, msg=message) |
587 | + |
588 | + def test_2_check_ssl(self): |
589 | + #print("bip.conf: {}".format(self.unit.run('cat /etc/bip.conf'))) |
590 | + s = socket.socket() |
591 | + for p in self.ports: |
592 | + (port, proto) = p.split("/") |
593 | + crt_pem = ssl.get_server_certificate((self.ipaddr, int(port))) |
594 | + #print(crt_pem) |
595 | + cert_file = "_test-self-signed-cert.pem" |
596 | + with open(cert_file, 'wt', encoding='utf-8') as f: |
597 | + f.write(crt_pem) |
598 | + sslsock = ssl.wrap_socket(s, |
599 | + ca_certs=cert_file, |
600 | + cert_reqs=ssl.CERT_REQUIRED) |
601 | + try: |
602 | + sslsock.connect((self.ipaddr, int(port))) |
603 | + #print(repr(sslsock.getpeername())) |
604 | + #print(sslsock.cipher()) |
605 | + #print(pprint.pformat(sslsock.getpeercert())) |
606 | + message = "Connect to %s on port %s succeeded" % ( |
607 | + self.ipaddr, port) |
608 | + #amulet.raise_status(amulet.PASS, msg=message) |
609 | + print(message) |
610 | + sslsock.close() |
611 | + except socket.error as e: |
612 | + message = "Connection to %s on port %s failed: %s" % ( |
613 | + self.ipaddr, port, e) |
614 | + amulet.raise_status(amulet.FAIL, msg=message) |
615 | + os.remove(cert_file) |
616 | + |
617 | +if __name__ == '__main__': |
618 | + unittest.main() |
This has been tested on a juju setup on GCE