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