Piotr Dobrowolski | 598a079 | 2019-04-09 13:28:46 +0200 | [diff] [blame] | 1 | # encoding: utf-8 |
Sergiusz Bazanski | 73cef11 | 2019-04-07 00:06:23 +0200 | [diff] [blame] | 2 | import json |
| 3 | import logging |
| 4 | import os |
| 5 | from six import StringIO |
| 6 | import subprocess |
| 7 | |
| 8 | |
| 9 | logger = 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 | |
| 65 | class CAException(Exception): |
| 66 | pass |
| 67 | |
| 68 | |
| 69 | class 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 Dobrowolski | 598a079 | 2019-04-09 13:28:46 +0200 | [diff] [blame] | 93 | 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 Dobrowolski | e24ccd6 | 2019-04-09 13:43:54 +0200 | [diff] [blame] | 97 | if obj is not None: |
| 98 | stdin = json.dumps(obj) |
Piotr Dobrowolski | 598a079 | 2019-04-09 13:28:46 +0200 | [diff] [blame] | 99 | |
| 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 Bazanski | 73cef11 | 2019-04-07 00:06:23 +0200 | [diff] [blame] | 109 | 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 Dobrowolski | 598a079 | 2019-04-09 13:28:46 +0200 | [diff] [blame] | 117 | out = self._cfssl_call(['gencert', '-initca', '-'], obj=ca_csr) |
Sergiusz Bazanski | 73cef11 | 2019-04-07 00:06:23 +0200 | [diff] [blame] | 118 | |
| 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 Dobrowolski | 598a079 | 2019-04-09 13:28:46 +0200 | [diff] [blame] | 148 | out = self._cfssl_call(['genkey', '-'], obj=cfg) |
| 149 | |
Sergiusz Bazanski | 73cef11 | 2019-04-07 00:06:23 +0200 | [diff] [blame] | 150 | 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 Dobrowolski | 598a079 | 2019-04-09 13:28:46 +0200 | [diff] [blame] | 163 | out = self._cfssl_call(['sign', '-ca=' + ca, '-ca-key=' + cakey, |
| 164 | '-profile=client-server', '-'], stdin=csr) |
Sergiusz Bazanski | 73cef11 | 2019-04-07 00:06:23 +0200 | [diff] [blame] | 165 | 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 | |
| 183 | class 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) |