blob: e4973db6953b4fd673f0920f47f5a3b8c6da8d60 [file] [log] [blame]
Sergiusz Bazanski73cef112019-04-07 00:06:23 +02001import json
2import logging
3import os
4from six import StringIO
5import subprocess
6
7
8logger = logging.getLogger(__name__)
9
10
11_std_subj = {
12 "C": "PL",
13 "ST": "Mazowieckie",
14 "L": "Warsaw",
15 "O": "Warsaw Hackerspace",
16 "OU": "clustercfg",
17}
18
19_ca_csr = {
20 "CN": "Prototype Test Certificate Authority",
21 "key": {
22 "algo": "rsa",
23 "size": 2048
24 },
25 "names": [ _std_subj ],
26}
27
28_ca_config = {
29 "signing": {
30 "default": {
31 "expiry": "168h"
32 },
33 "profiles": {
34 "server": {
35 "expiry": "8760h",
36 "usages": [
37 "signing",
38 "key encipherment",
39 "server auth"
40 ]
41 },
42 "client": {
43 "expiry": "8760h",
44 "usages": [
45 "signing",
46 "key encipherment",
47 "client auth"
48 ]
49 },
50 "client-server": {
51 "expiry": "8760h",
52 "usages": [
53 "signing",
54 "key encipherment",
55 "server auth",
56 "client auth"
57 ]
58 }
59 }
60 }
61}
62
63
64class CAException(Exception):
65 pass
66
67
68class CA(object):
69 def __init__(self, secretstore, certdir, short, cn):
70 self.ss = secretstore
71 self.cdir = certdir
72 self.short = short
73 self.cn = cn
74 self._init_ca()
75
76 def __str__(self):
77 return 'CN={} ({})'.format(self.cn, self.short)
78
79 @property
80 def _secret_key(self):
81 return 'ca-{}.key'.format(self.short)
82
83 @property
84 def _cert(self):
85 return os.path.join(self.cdir, 'ca-{}.crt'.format(self.short))
86
87 @property
88 def cert_data(self):
89 with open(self._cert) as f:
90 return f.read()
91
92 def _init_ca(self):
93 if self.ss.exists(self._secret_key):
94 return
95
96 ca_csr = dict(_ca_csr)
97 ca_csr['CN'] = self.cn
98
99 logger.info("{}: Generating CA...".format(self))
100 p = subprocess.Popen(['cfssl', 'gencert', '-initca', '-'],
101 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
102 stderr=subprocess.PIPE)
103 outs, errs = p.communicate(json.dumps(ca_csr).encode())
104 out = json.loads(outs)
105
106 f = self.ss.open(self._secret_key, 'w')
107 f.write(out['key'])
108 f.close()
109
110 f = open(self._cert, 'w')
111 f.write(out['cert'])
112 f.close()
113
114 def gen_key(self, hosts, o=_std_subj['O'], ou=_std_subj['OU'], save=None):
115 """お元気ですか?"""
116 cfg = {
117 "CN": hosts[0],
118 "hosts": hosts,
119 "key": {
120 "algo": "rsa",
121 "size": 4096,
122 },
123 "names": [
124 {
125 "C": _std_subj["C"],
126 "ST": _std_subj["ST"],
127 "L": _std_subj["L"],
128 "O": o,
129 "OU": ou,
130 },
131 ],
132 }
133 cfg.update(_ca_config)
134 logger.info("{}: Generating key/CSR for {}".format(self, hosts))
135 p = subprocess.Popen(['cfssl', 'genkey', '-'],
136 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
137 stderr=subprocess.PIPE)
138 outs, errs = p.communicate(json.dumps(cfg).encode())
139 out = json.loads(outs)
140 key, csr = out['key'], out['csr']
141 if save is not None:
142 logging.info("{}: Saving new key to secret {}".format(self, save))
143 f = self.ss.open(save, 'w')
144 f.write(key)
145 f.close()
146
147 return key, csr
148
149 def sign(self, csr, save=None):
150 logging.info("{}: Signing CSR".format(self))
151 ca = self._cert
152 cakey = self.ss.plaintext(self._secret_key)
153 p = subprocess.Popen(['cfssl', 'sign', '-ca=' + ca, '-ca-key=' + cakey,
154 '-profile=client-server', '-'],
155 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
156 stderr=subprocess.PIPE)
157 outs, errs = p.communicate(csr.encode())
158 out = json.loads(outs)
159 cert = out['cert']
160 if save is not None:
161 name = os.path.join(self.cdir, save)
162 logging.info("{}: Saving new certificate to {}".format(self, name))
163 f = open(name, 'w')
164 f.write(cert)
165 f.close()
166
167 return cert
168
169 def upload(self, c, remote_cert):
170 logger.info("Uploading CA {} to {}".format(self, remote_cert))
171 c.put(local=self._cert, remote=remote_cert)
172
173 def make_cert(self, *a, **kw):
174 return ManagedCertificate(self, *a, **kw)
175
176
177class ManagedCertificate(object):
178 def __init__(self, ca, name, hosts, o=None, ou=None):
179 self.ca = ca
180
181 self.hosts = hosts
182 self.name = name
183 self.key = '{}.key'.format(name)
184 self.cert = '{}.cert'.format(name)
185 self.o = o
186 self.ou = ou
187
188 self.ensure()
189
190 def __str__(self):
191 return '{}'.format(self.name)
192
193 @property
194 def key_exists(self):
195 return self.ca.ss.exists(self.key)
196
197 @property
198 def key_data(self):
199 f = open(self.ca.ss.open(self.key))
200 d = f.read()
201 f.close()
202 return d
203
204 @property
205 def key_path(self):
206 return self.ca.ss.plaintext(self.key)
207
208 @property
209 def cert_path(self):
210 return os.path.join(self.ca.cdir, self.cert)
211
212 @property
213 def cert_exists(self):
214 return os.path.exists(self.cert_path)
215
216 @property
217 def cert_data(self):
218 with open(self.cert_path) as f:
219 return f.read()
220
221 def ensure(self):
222 if self.key_exists and self.cert_exists:
223 return
224
225 logger.info("{}: Generating...".format(self))
226 key, csr = self.ca.gen_key(self.hosts, o=self.o, ou=self.ou, save=self.key)
227 self.ca.sign(csr, save=self.cert)
228
229 def upload(self, c, remote_cert, remote_key, concat_ca=False):
230 logger.info("Uploading Cert {} to {} & {}".format(self, remote_cert, remote_key))
231 if concat_ca:
232 f = StringIO(self.cert_data + self.ca.cert_data)
233 c.put(local=f, remote=remote_cert)
234 else:
235 c.put(local=self.cert_path, remote=remote_cert)
236 c.put(local=self.key_path, remote=remote_key)
237
238 def upload_pki(self, c, pki, concat_ca=False):
239 self.upload(c, pki['cert'], pki['key'], concat_ca)