blob: 9ed205341f49b012d9fe39b90ba6fc58bc6f357f [file] [log] [blame]
Piotr Dobrowolski598a0792019-04-09 13:28:46 +02001# encoding: utf-8
Sergiusz Bazanski73cef112019-04-07 00:06:23 +02002import json
3import logging
4import os
5from six import StringIO
6import subprocess
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +02007import tempfile
Sergiusz Bazanski73cef112019-04-07 00:06:23 +02008
9
10logger = logging.getLogger(__name__)
11
12
13_std_subj = {
14 "C": "PL",
15 "ST": "Mazowieckie",
16 "L": "Warsaw",
17 "O": "Warsaw Hackerspace",
18 "OU": "clustercfg",
19}
20
21_ca_csr = {
22 "CN": "Prototype Test Certificate Authority",
23 "key": {
24 "algo": "rsa",
25 "size": 2048
26 },
27 "names": [ _std_subj ],
28}
29
30_ca_config = {
31 "signing": {
32 "default": {
33 "expiry": "168h"
34 },
35 "profiles": {
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +020036 "intermediate": {
37 "expiry": "8760h",
38 "usages": [
39 "signing",
40 "key encipherment",
41 "cert sign",
42 "crl sign",
43 "server auth",
44 "client auth",
45 ],
46 "ca_constraint": {
47 "is_ca": True,
48 },
49 },
Sergiusz Bazanski73cef112019-04-07 00:06:23 +020050 "server": {
51 "expiry": "8760h",
52 "usages": [
53 "signing",
54 "key encipherment",
55 "server auth"
56 ]
57 },
58 "client": {
59 "expiry": "8760h",
60 "usages": [
61 "signing",
62 "key encipherment",
63 "client auth"
64 ]
65 },
66 "client-server": {
67 "expiry": "8760h",
68 "usages": [
69 "signing",
70 "key encipherment",
71 "server auth",
72 "client auth"
73 ]
74 }
75 }
76 }
77}
78
79
80class CAException(Exception):
81 pass
82
83
84class CA(object):
85 def __init__(self, secretstore, certdir, short, cn):
86 self.ss = secretstore
87 self.cdir = certdir
88 self.short = short
89 self.cn = cn
90 self._init_ca()
91
92 def __str__(self):
93 return 'CN={} ({})'.format(self.cn, self.short)
94
95 @property
96 def _secret_key(self):
97 return 'ca-{}.key'.format(self.short)
98
99 @property
100 def _cert(self):
101 return os.path.join(self.cdir, 'ca-{}.crt'.format(self.short))
102
103 @property
104 def cert_data(self):
105 with open(self._cert) as f:
106 return f.read()
107
Piotr Dobrowolski598a0792019-04-09 13:28:46 +0200108 def _cfssl_call(self, args, obj=None, stdin=None):
109 p = subprocess.Popen(['cfssl'] + args,
110 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
111 stderr=subprocess.PIPE)
Piotr Dobrowolskie24ccd62019-04-09 13:43:54 +0200112 if obj is not None:
113 stdin = json.dumps(obj)
Piotr Dobrowolski598a0792019-04-09 13:28:46 +0200114
115 outs, errs = p.communicate(stdin.encode())
116 if p.returncode != 0:
117 raise Exception(
118 'cfssl failed. stderr: %r, stdout: %r, code: %r' % (
119 errs, outs, p.returncode))
120
121 out = json.loads(outs)
122 return out
123
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200124 def _init_ca(self):
125 if self.ss.exists(self._secret_key):
126 return
127
128 ca_csr = dict(_ca_csr)
129 ca_csr['CN'] = self.cn
130
131 logger.info("{}: Generating CA...".format(self))
Piotr Dobrowolski598a0792019-04-09 13:28:46 +0200132 out = self._cfssl_call(['gencert', '-initca', '-'], obj=ca_csr)
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200133
134 f = self.ss.open(self._secret_key, 'w')
135 f.write(out['key'])
136 f.close()
137
138 f = open(self._cert, 'w')
139 f.write(out['cert'])
140 f.close()
141
142 def gen_key(self, hosts, o=_std_subj['O'], ou=_std_subj['OU'], save=None):
143 """お元気ですか?"""
144 cfg = {
145 "CN": hosts[0],
146 "hosts": hosts,
147 "key": {
148 "algo": "rsa",
149 "size": 4096,
150 },
151 "names": [
152 {
153 "C": _std_subj["C"],
154 "ST": _std_subj["ST"],
155 "L": _std_subj["L"],
156 "O": o,
157 "OU": ou,
158 },
159 ],
160 }
161 cfg.update(_ca_config)
162 logger.info("{}: Generating key/CSR for {}".format(self, hosts))
Piotr Dobrowolski598a0792019-04-09 13:28:46 +0200163 out = self._cfssl_call(['genkey', '-'], obj=cfg)
164
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200165 key, csr = out['key'], out['csr']
166 if save is not None:
167 logging.info("{}: Saving new key to secret {}".format(self, save))
168 f = self.ss.open(save, 'w')
169 f.write(key)
170 f.close()
171
172 return key, csr
173
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +0200174 def sign(self, csr, save=None, profile='client-server'):
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200175 logging.info("{}: Signing CSR".format(self))
176 ca = self._cert
177 cakey = self.ss.plaintext(self._secret_key)
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +0200178
179 config = tempfile.NamedTemporaryFile(mode='w')
180 json.dump(_ca_config, config)
181 config.flush()
182
Piotr Dobrowolski598a0792019-04-09 13:28:46 +0200183 out = self._cfssl_call(['sign', '-ca=' + ca, '-ca-key=' + cakey,
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +0200184 '-profile='+profile, '-config='+config.name, '-'], stdin=csr)
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200185 cert = out['cert']
186 if save is not None:
187 name = os.path.join(self.cdir, save)
188 logging.info("{}: Saving new certificate to {}".format(self, name))
189 f = open(name, 'w')
190 f.write(cert)
191 f.close()
192
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +0200193 config.close()
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200194 return cert
195
196 def upload(self, c, remote_cert):
197 logger.info("Uploading CA {} to {}".format(self, remote_cert))
198 c.put(local=self._cert, remote=remote_cert)
199
200 def make_cert(self, *a, **kw):
201 return ManagedCertificate(self, *a, **kw)
202
203
204class ManagedCertificate(object):
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +0200205 def __init__(self, ca, name, hosts, o=None, ou=None, profile='client-server'):
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200206 self.ca = ca
207
208 self.hosts = hosts
209 self.name = name
210 self.key = '{}.key'.format(name)
211 self.cert = '{}.cert'.format(name)
212 self.o = o
213 self.ou = ou
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +0200214 self.profile = profile
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200215
216 self.ensure()
217
218 def __str__(self):
219 return '{}'.format(self.name)
220
221 @property
222 def key_exists(self):
223 return self.ca.ss.exists(self.key)
224
225 @property
226 def key_data(self):
227 f = open(self.ca.ss.open(self.key))
228 d = f.read()
229 f.close()
230 return d
231
232 @property
233 def key_path(self):
234 return self.ca.ss.plaintext(self.key)
235
236 @property
237 def cert_path(self):
238 return os.path.join(self.ca.cdir, self.cert)
239
240 @property
241 def cert_exists(self):
242 return os.path.exists(self.cert_path)
243
244 @property
245 def cert_data(self):
246 with open(self.cert_path) as f:
247 return f.read()
248
249 def ensure(self):
250 if self.key_exists and self.cert_exists:
251 return
252
253 logger.info("{}: Generating...".format(self))
254 key, csr = self.ca.gen_key(self.hosts, o=self.o, ou=self.ou, save=self.key)
Sergiusz Bazanskib13b7ff2019-08-29 20:12:24 +0200255 self.ca.sign(csr, save=self.cert, profile=self.profile)
Sergiusz Bazanski73cef112019-04-07 00:06:23 +0200256
257 def upload(self, c, remote_cert, remote_key, concat_ca=False):
258 logger.info("Uploading Cert {} to {} & {}".format(self, remote_cert, remote_key))
259 if concat_ca:
260 f = StringIO(self.cert_data + self.ca.cert_data)
261 c.put(local=f, remote=remote_cert)
262 else:
263 c.put(local=self.cert_path, remote=remote_cert)
264 c.put(local=self.key_path, remote=remote_key)
265
266 def upload_pki(self, c, pki, concat_ca=False):
267 self.upload(c, pki['cert'], pki['key'], concat_ca)