blob: 0107080a0b213defa530b80ba400f4660b9ea7c3 [file] [log] [blame]
Piotr Dobrowolski598a0792019-04-09 13:28:46 +02001# encoding: utf-8
Sergiusz Bazanski0dcc7022020-03-28 17:58:19 +01002from datetime import datetime, timezone
Sergiusz Bazanski73cef112019-04-07 00:06:23 +02003import json
4import logging
5import os
6from six import StringIO
7import subprocess
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +02008import tempfile
Sergiusz Bazanski73cef112019-04-07 00:06:23 +02009
10
11logger = logging.getLogger(__name__)
12
13
14_std_subj = {
15 "C": "PL",
16 "ST": "Mazowieckie",
17 "L": "Warsaw",
18 "O": "Warsaw Hackerspace",
19 "OU": "clustercfg",
20}
21
22_ca_csr = {
23 "CN": "Prototype Test Certificate Authority",
24 "key": {
25 "algo": "rsa",
26 "size": 2048
27 },
28 "names": [ _std_subj ],
29}
30
31_ca_config = {
32 "signing": {
33 "default": {
34 "expiry": "168h"
35 },
36 "profiles": {
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +020037 "intermediate": {
38 "expiry": "8760h",
39 "usages": [
40 "signing",
41 "key encipherment",
42 "cert sign",
43 "crl sign",
44 "server auth",
45 "client auth",
46 ],
47 "ca_constraint": {
48 "is_ca": True,
49 },
50 },
Sergiusz Bazanski73cef112019-04-07 00:06:23 +020051 "server": {
52 "expiry": "8760h",
53 "usages": [
54 "signing",
55 "key encipherment",
56 "server auth"
57 ]
58 },
59 "client": {
60 "expiry": "8760h",
61 "usages": [
62 "signing",
63 "key encipherment",
64 "client auth"
65 ]
66 },
67 "client-server": {
68 "expiry": "8760h",
69 "usages": [
70 "signing",
71 "key encipherment",
72 "server auth",
73 "client auth"
74 ]
75 }
76 }
77 }
78}
79
80
81class CAException(Exception):
82 pass
83
84
85class CA(object):
86 def __init__(self, secretstore, certdir, short, cn):
87 self.ss = secretstore
88 self.cdir = certdir
89 self.short = short
90 self.cn = cn
91 self._init_ca()
92
93 def __str__(self):
94 return 'CN={} ({})'.format(self.cn, self.short)
95
96 @property
97 def _secret_key(self):
98 return 'ca-{}.key'.format(self.short)
99
100 @property
101 def _cert(self):
102 return os.path.join(self.cdir, 'ca-{}.crt'.format(self.short))
103
104 @property
105 def cert_data(self):
106 with open(self._cert) as f:
107 return f.read()
108
Piotr Dobrowolski598a0792019-04-09 13:28:46 +0200109 def _cfssl_call(self, args, obj=None, stdin=None):
110 p = subprocess.Popen(['cfssl'] + args,
111 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
112 stderr=subprocess.PIPE)
Piotr Dobrowolskie24ccd62019-04-09 13:43:54 +0200113 if obj is not None:
114 stdin = json.dumps(obj)
Piotr Dobrowolski598a0792019-04-09 13:28:46 +0200115
116 outs, errs = p.communicate(stdin.encode())
117 if p.returncode != 0:
118 raise Exception(
119 'cfssl failed. stderr: %r, stdout: %r, code: %r' % (
120 errs, outs, p.returncode))
121
122 out = json.loads(outs)
123 return out
124
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200125 def _init_ca(self):
126 if self.ss.exists(self._secret_key):
127 return
128
129 ca_csr = dict(_ca_csr)
130 ca_csr['CN'] = self.cn
131
132 logger.info("{}: Generating CA...".format(self))
Piotr Dobrowolski598a0792019-04-09 13:28:46 +0200133 out = self._cfssl_call(['gencert', '-initca', '-'], obj=ca_csr)
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200134
135 f = self.ss.open(self._secret_key, 'w')
136 f.write(out['key'])
137 f.close()
138
139 f = open(self._cert, 'w')
140 f.write(out['cert'])
141 f.close()
142
143 def gen_key(self, hosts, o=_std_subj['O'], ou=_std_subj['OU'], save=None):
144 """お元気ですか?"""
145 cfg = {
146 "CN": hosts[0],
147 "hosts": hosts,
148 "key": {
149 "algo": "rsa",
150 "size": 4096,
151 },
152 "names": [
153 {
154 "C": _std_subj["C"],
155 "ST": _std_subj["ST"],
156 "L": _std_subj["L"],
157 "O": o,
158 "OU": ou,
159 },
160 ],
161 }
162 cfg.update(_ca_config)
163 logger.info("{}: Generating key/CSR for {}".format(self, hosts))
Piotr Dobrowolski598a0792019-04-09 13:28:46 +0200164 out = self._cfssl_call(['genkey', '-'], obj=cfg)
165
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200166 key, csr = out['key'], out['csr']
167 if save is not None:
168 logging.info("{}: Saving new key to secret {}".format(self, save))
169 f = self.ss.open(save, 'w')
170 f.write(key)
171 f.close()
172
173 return key, csr
174
Sergiusz Bazanski0dcc7022020-03-28 17:58:19 +0100175 def gen_csr(self, key, hosts, o=_std_subj['O'], ou=_std_subj['OU']):
176 """
177 Generate a CSR while already having a private key - for renewals, etc.
178
179 TODO(q3k): this shouldn't be a CA method, but a cert method.
180 """
181 cfg = {
182 "CN": hosts[0],
183 "hosts": hosts,
184 "key": {
185 "algo": "rsa",
186 "size": 4096,
187 },
188 "names": [
189 {
190 "C": _std_subj["C"],
191 "ST": _std_subj["ST"],
192 "L": _std_subj["L"],
193 "O": o,
194 "OU": ou,
195 },
196 ],
197 }
198 cfg.update(_ca_config)
199 logger.info("{}: Generating CSR for {}".format(self, hosts))
200 out = self._cfssl_call(['gencsr', '-key', key, '-'], obj=cfg)
201
202 return out['csr']
203
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +0200204 def sign(self, csr, save=None, profile='client-server'):
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200205 logging.info("{}: Signing CSR".format(self))
206 ca = self._cert
207 cakey = self.ss.plaintext(self._secret_key)
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +0200208
209 config = tempfile.NamedTemporaryFile(mode='w')
210 json.dump(_ca_config, config)
211 config.flush()
212
Piotr Dobrowolski598a0792019-04-09 13:28:46 +0200213 out = self._cfssl_call(['sign', '-ca=' + ca, '-ca-key=' + cakey,
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +0200214 '-profile='+profile, '-config='+config.name, '-'], stdin=csr)
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200215 cert = out['cert']
216 if save is not None:
217 name = os.path.join(self.cdir, save)
218 logging.info("{}: Saving new certificate to {}".format(self, name))
219 f = open(name, 'w')
220 f.write(cert)
221 f.close()
222
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +0200223 config.close()
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200224 return cert
225
226 def upload(self, c, remote_cert):
227 logger.info("Uploading CA {} to {}".format(self, remote_cert))
228 c.put(local=self._cert, remote=remote_cert)
229
230 def make_cert(self, *a, **kw):
231 return ManagedCertificate(self, *a, **kw)
232
233
234class ManagedCertificate(object):
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +0200235 def __init__(self, ca, name, hosts, o=None, ou=None, profile='client-server'):
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200236 self.ca = ca
237
238 self.hosts = hosts
239 self.name = name
240 self.key = '{}.key'.format(name)
241 self.cert = '{}.cert'.format(name)
242 self.o = o
243 self.ou = ou
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +0200244 self.profile = profile
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200245
246 self.ensure()
247
248 def __str__(self):
249 return '{}'.format(self.name)
250
251 @property
252 def key_exists(self):
253 return self.ca.ss.exists(self.key)
254
255 @property
256 def key_data(self):
257 f = open(self.ca.ss.open(self.key))
258 d = f.read()
259 f.close()
260 return d
261
262 @property
263 def key_path(self):
264 return self.ca.ss.plaintext(self.key)
265
266 @property
267 def cert_path(self):
268 return os.path.join(self.ca.cdir, self.cert)
269
270 @property
271 def cert_exists(self):
272 return os.path.exists(self.cert_path)
273
274 @property
275 def cert_data(self):
276 with open(self.cert_path) as f:
277 return f.read()
278
Sergiusz Bazanski0dcc7022020-03-28 17:58:19 +0100279 @property
280 def cert_expires_soon(self):
281 if not self.cert_exists:
282 return False
283
284 out = self.ca._cfssl_call(['certinfo', '-cert', self.cert_path], stdin="")
285 not_after = datetime.strptime(out['not_after'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc)
286 until = not_after - datetime.now(timezone.utc)
287 if until.days < 30:
288 return True
289 return False
290
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200291 def ensure(self):
Sergiusz Bazanski0dcc7022020-03-28 17:58:19 +0100292 if self.key_exists and self.cert_exists and not self.cert_expires_soon:
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200293 return
294
Sergiusz Bazanski0dcc7022020-03-28 17:58:19 +0100295 key = None
296 if not self.key_exists:
297 logger.info("{}: Generating key...".format(self))
298 key, csr = self.ca.gen_key(self.hosts, o=self.o, ou=self.ou, save=self.key)
299 else:
300 logger.info("{}: Renewing certificate...".format(self))
301 # Use already existing key
302 csr = self.ca.gen_csr(self.key_path, self.hosts, o=self.o, ou=self.ou)
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +0200303 self.ca.sign(csr, save=self.cert, profile=self.profile)
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200304
305 def upload(self, c, remote_cert, remote_key, concat_ca=False):
306 logger.info("Uploading Cert {} to {} & {}".format(self, remote_cert, remote_key))
307 if concat_ca:
308 f = StringIO(self.cert_data + self.ca.cert_data)
309 c.put(local=f, remote=remote_cert)
310 else:
311 c.put(local=self.cert_path, remote=remote_cert)
312 c.put(local=self.key_path, remote=remote_key)
313
314 def upload_pki(self, c, pki, concat_ca=False):
315 self.upload(c, pki['cert'], pki['key'], concat_ca)