Sergiusz Bazanski | 73cef11 | 2019-04-07 00:06:23 +0200 | [diff] [blame] | 1 | import json |
| 2 | import logging |
| 3 | import os |
| 4 | from six import StringIO |
| 5 | import subprocess |
| 6 | |
| 7 | |
| 8 | logger = 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 | |
| 64 | class CAException(Exception): |
| 65 | pass |
| 66 | |
| 67 | |
| 68 | class 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 | |
| 177 | class 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) |