forked from hswaw/hscloud
316 lines
8.3 KiB
Python
316 lines
8.3 KiB
Python
# encoding: utf-8
|
|
from datetime import datetime, timezone
|
|
import json
|
|
import logging
|
|
import os
|
|
from six import StringIO
|
|
import subprocess
|
|
import tempfile
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
_std_subj = {
|
|
"C": "PL",
|
|
"ST": "Mazowieckie",
|
|
"L": "Warsaw",
|
|
"O": "Warsaw Hackerspace",
|
|
"OU": "clustercfg",
|
|
}
|
|
|
|
_ca_csr = {
|
|
"CN": "Prototype Test Certificate Authority",
|
|
"key": {
|
|
"algo": "rsa",
|
|
"size": 2048
|
|
},
|
|
"names": [ _std_subj ],
|
|
}
|
|
|
|
_ca_config = {
|
|
"signing": {
|
|
"default": {
|
|
"expiry": "168h"
|
|
},
|
|
"profiles": {
|
|
"intermediate": {
|
|
"expiry": "8760h",
|
|
"usages": [
|
|
"signing",
|
|
"key encipherment",
|
|
"cert sign",
|
|
"crl sign",
|
|
"server auth",
|
|
"client auth",
|
|
],
|
|
"ca_constraint": {
|
|
"is_ca": True,
|
|
},
|
|
},
|
|
"server": {
|
|
"expiry": "8760h",
|
|
"usages": [
|
|
"signing",
|
|
"key encipherment",
|
|
"server auth"
|
|
]
|
|
},
|
|
"client": {
|
|
"expiry": "8760h",
|
|
"usages": [
|
|
"signing",
|
|
"key encipherment",
|
|
"client auth"
|
|
]
|
|
},
|
|
"client-server": {
|
|
"expiry": "8760h",
|
|
"usages": [
|
|
"signing",
|
|
"key encipherment",
|
|
"server auth",
|
|
"client auth"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
class CAException(Exception):
|
|
pass
|
|
|
|
|
|
class CA(object):
|
|
def __init__(self, secretstore, certdir, short, cn):
|
|
self.ss = secretstore
|
|
self.cdir = certdir
|
|
self.short = short
|
|
self.cn = cn
|
|
self._init_ca()
|
|
|
|
def __str__(self):
|
|
return 'CN={} ({})'.format(self.cn, self.short)
|
|
|
|
@property
|
|
def _secret_key(self):
|
|
return 'ca-{}.key'.format(self.short)
|
|
|
|
@property
|
|
def _cert(self):
|
|
return os.path.join(self.cdir, 'ca-{}.crt'.format(self.short))
|
|
|
|
@property
|
|
def cert_data(self):
|
|
with open(self._cert) as f:
|
|
return f.read()
|
|
|
|
def _cfssl_call(self, args, obj=None, stdin=None):
|
|
p = subprocess.Popen(['cfssl'] + args,
|
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
if obj is not None:
|
|
stdin = json.dumps(obj)
|
|
|
|
outs, errs = p.communicate(stdin.encode())
|
|
if p.returncode != 0:
|
|
raise Exception(
|
|
'cfssl failed. stderr: %r, stdout: %r, code: %r' % (
|
|
errs, outs, p.returncode))
|
|
|
|
out = json.loads(outs)
|
|
return out
|
|
|
|
def _init_ca(self):
|
|
if self.ss.exists(self._secret_key):
|
|
return
|
|
|
|
ca_csr = dict(_ca_csr)
|
|
ca_csr['CN'] = self.cn
|
|
|
|
logger.info("{}: Generating CA...".format(self))
|
|
out = self._cfssl_call(['gencert', '-initca', '-'], obj=ca_csr)
|
|
|
|
f = self.ss.open(self._secret_key, 'w')
|
|
f.write(out['key'])
|
|
f.close()
|
|
|
|
f = open(self._cert, 'w')
|
|
f.write(out['cert'])
|
|
f.close()
|
|
|
|
def gen_key(self, hosts, o=_std_subj['O'], ou=_std_subj['OU'], save=None):
|
|
"""お元気ですか?"""
|
|
cfg = {
|
|
"CN": hosts[0],
|
|
"hosts": hosts,
|
|
"key": {
|
|
"algo": "rsa",
|
|
"size": 4096,
|
|
},
|
|
"names": [
|
|
{
|
|
"C": _std_subj["C"],
|
|
"ST": _std_subj["ST"],
|
|
"L": _std_subj["L"],
|
|
"O": o,
|
|
"OU": ou,
|
|
},
|
|
],
|
|
}
|
|
cfg.update(_ca_config)
|
|
logger.info("{}: Generating key/CSR for {}".format(self, hosts))
|
|
out = self._cfssl_call(['genkey', '-'], obj=cfg)
|
|
|
|
key, csr = out['key'], out['csr']
|
|
if save is not None:
|
|
logging.info("{}: Saving new key to secret {}".format(self, save))
|
|
f = self.ss.open(save, 'w')
|
|
f.write(key)
|
|
f.close()
|
|
|
|
return key, csr
|
|
|
|
def gen_csr(self, key, hosts, o=_std_subj['O'], ou=_std_subj['OU']):
|
|
"""
|
|
Generate a CSR while already having a private key - for renewals, etc.
|
|
|
|
TODO(q3k): this shouldn't be a CA method, but a cert method.
|
|
"""
|
|
cfg = {
|
|
"CN": hosts[0],
|
|
"hosts": hosts,
|
|
"key": {
|
|
"algo": "rsa",
|
|
"size": 4096,
|
|
},
|
|
"names": [
|
|
{
|
|
"C": _std_subj["C"],
|
|
"ST": _std_subj["ST"],
|
|
"L": _std_subj["L"],
|
|
"O": o,
|
|
"OU": ou,
|
|
},
|
|
],
|
|
}
|
|
cfg.update(_ca_config)
|
|
logger.info("{}: Generating CSR for {}".format(self, hosts))
|
|
out = self._cfssl_call(['gencsr', '-key', key, '-'], obj=cfg)
|
|
|
|
return out['csr']
|
|
|
|
def sign(self, csr, save=None, profile='client-server'):
|
|
logging.info("{}: Signing CSR".format(self))
|
|
ca = self._cert
|
|
cakey = self.ss.plaintext(self._secret_key)
|
|
|
|
config = tempfile.NamedTemporaryFile(mode='w')
|
|
json.dump(_ca_config, config)
|
|
config.flush()
|
|
|
|
out = self._cfssl_call(['sign', '-ca=' + ca, '-ca-key=' + cakey,
|
|
'-profile='+profile, '-config='+config.name, '-'], stdin=csr)
|
|
cert = out['cert']
|
|
if save is not None:
|
|
name = os.path.join(self.cdir, save)
|
|
logging.info("{}: Saving new certificate to {}".format(self, name))
|
|
f = open(name, 'w')
|
|
f.write(cert)
|
|
f.close()
|
|
|
|
config.close()
|
|
return cert
|
|
|
|
def upload(self, c, remote_cert):
|
|
logger.info("Uploading CA {} to {}".format(self, remote_cert))
|
|
c.put(local=self._cert, remote=remote_cert)
|
|
|
|
def make_cert(self, *a, **kw):
|
|
return ManagedCertificate(self, *a, **kw)
|
|
|
|
|
|
class ManagedCertificate(object):
|
|
def __init__(self, ca, name, hosts, o=None, ou=None, profile='client-server'):
|
|
self.ca = ca
|
|
|
|
self.hosts = hosts
|
|
self.name = name
|
|
self.key = '{}.key'.format(name)
|
|
self.cert = '{}.cert'.format(name)
|
|
self.o = o
|
|
self.ou = ou
|
|
self.profile = profile
|
|
|
|
self.ensure()
|
|
|
|
def __str__(self):
|
|
return '{}'.format(self.name)
|
|
|
|
@property
|
|
def key_exists(self):
|
|
return self.ca.ss.exists(self.key)
|
|
|
|
@property
|
|
def key_data(self):
|
|
f = open(self.ca.ss.open(self.key))
|
|
d = f.read()
|
|
f.close()
|
|
return d
|
|
|
|
@property
|
|
def key_path(self):
|
|
return self.ca.ss.plaintext(self.key)
|
|
|
|
@property
|
|
def cert_path(self):
|
|
return os.path.join(self.ca.cdir, self.cert)
|
|
|
|
@property
|
|
def cert_exists(self):
|
|
return os.path.exists(self.cert_path)
|
|
|
|
@property
|
|
def cert_data(self):
|
|
with open(self.cert_path) as f:
|
|
return f.read()
|
|
|
|
@property
|
|
def cert_expires_soon(self):
|
|
if not self.cert_exists:
|
|
return False
|
|
|
|
out = self.ca._cfssl_call(['certinfo', '-cert', self.cert_path], stdin="")
|
|
not_after = datetime.strptime(out['not_after'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc)
|
|
until = not_after - datetime.now(timezone.utc)
|
|
if until.days < 30:
|
|
return True
|
|
return False
|
|
|
|
def ensure(self):
|
|
if self.key_exists and self.cert_exists and not self.cert_expires_soon:
|
|
return
|
|
|
|
key = None
|
|
if not self.key_exists:
|
|
logger.info("{}: Generating key...".format(self))
|
|
key, csr = self.ca.gen_key(self.hosts, o=self.o, ou=self.ou, save=self.key)
|
|
else:
|
|
logger.info("{}: Renewing certificate...".format(self))
|
|
# Use already existing key
|
|
csr = self.ca.gen_csr(self.key_path, self.hosts, o=self.o, ou=self.ou)
|
|
self.ca.sign(csr, save=self.cert, profile=self.profile)
|
|
|
|
def upload(self, c, remote_cert, remote_key, concat_ca=False):
|
|
logger.info("Uploading Cert {} to {} & {}".format(self, remote_cert, remote_key))
|
|
if concat_ca:
|
|
f = StringIO(self.cert_data + self.ca.cert_data)
|
|
c.put(local=f, remote=remote_cert)
|
|
else:
|
|
c.put(local=self.cert_path, remote=remote_cert)
|
|
c.put(local=self.key_path, remote=remote_key)
|
|
|
|
def upload_pki(self, c, pki, concat_ca=False):
|
|
self.upload(c, pki['cert'], pki['key'], concat_ca)
|