2017-07-05 15:22:03 +00:00
|
|
|
from __future__ import division, absolute_import, print_function
|
2015-12-12 22:34:08 +00:00
|
|
|
import click
|
|
|
|
import os
|
|
|
|
import re
|
2016-02-28 20:37:56 +00:00
|
|
|
import requests
|
2017-03-13 11:42:58 +00:00
|
|
|
import hashlib
|
2016-09-17 21:00:14 +00:00
|
|
|
import socket
|
2017-12-30 13:57:48 +00:00
|
|
|
import sys
|
2017-08-16 20:25:16 +00:00
|
|
|
from oscrypto import asymmetric
|
|
|
|
from asn1crypto import pem, x509
|
|
|
|
from asn1crypto.csr import CertificationRequest
|
|
|
|
from certbuilder import CertificateBuilder
|
2016-09-17 21:00:14 +00:00
|
|
|
from certidude import config, push, mailer, const
|
2016-01-14 22:47:30 +00:00
|
|
|
from certidude import errors
|
2017-08-16 20:25:16 +00:00
|
|
|
from crlbuilder import CertificateListBuilder, pem_armor_crl
|
|
|
|
from csrbuilder import CSRBuilder, pem_armor_csr
|
|
|
|
from datetime import datetime, timedelta
|
2017-01-25 11:34:08 +00:00
|
|
|
from jinja2 import Template
|
2017-08-16 20:25:16 +00:00
|
|
|
from random import SystemRandom
|
2017-07-08 12:08:23 +00:00
|
|
|
from xattr import getxattr, listxattr, setxattr
|
2015-12-12 22:34:08 +00:00
|
|
|
|
2017-08-16 20:25:16 +00:00
|
|
|
random = SystemRandom()
|
|
|
|
|
2016-03-21 21:42:39 +00:00
|
|
|
RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(@(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$"
|
2015-12-12 22:34:08 +00:00
|
|
|
|
|
|
|
# https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/
|
|
|
|
# https://jamielinux.com/docs/openssl-certificate-authority/
|
|
|
|
# http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py
|
|
|
|
|
2016-03-21 21:42:39 +00:00
|
|
|
# Cache CA certificate
|
2015-12-12 22:34:08 +00:00
|
|
|
|
2017-12-30 13:57:48 +00:00
|
|
|
with open(config.AUTHORITY_CERTIFICATE_PATH, "rb") as fh:
|
2017-08-16 20:25:16 +00:00
|
|
|
certificate_buf = fh.read()
|
|
|
|
header, _, certificate_der_bytes = pem.unarmor(certificate_buf)
|
|
|
|
certificate = x509.Certificate.load(certificate_der_bytes)
|
|
|
|
public_key = asymmetric.load_public_key(certificate["tbs_certificate"]["subject_public_key_info"])
|
2017-12-30 13:57:48 +00:00
|
|
|
with open(config.AUTHORITY_PRIVATE_KEY_PATH, "rb") as fh:
|
2017-08-16 20:25:16 +00:00
|
|
|
key_buf = fh.read()
|
|
|
|
header, _, key_der_bytes = pem.unarmor(key_buf)
|
|
|
|
private_key = asymmetric.load_private_key(key_der_bytes)
|
2016-03-21 21:42:39 +00:00
|
|
|
|
2017-12-30 13:57:48 +00:00
|
|
|
def self_enroll():
|
|
|
|
from certidude import const
|
|
|
|
common_name = const.FQDN
|
|
|
|
directory = os.path.join("/var/lib/certidude", const.FQDN)
|
2018-01-02 13:13:48 +00:00
|
|
|
self_key_path = os.path.join(directory, "self_key.pem")
|
|
|
|
|
|
|
|
try:
|
|
|
|
path, buf, cert, signed, expires = get_signed(common_name)
|
2018-04-09 13:08:12 +00:00
|
|
|
self_public_key = asymmetric.load_public_key(path)
|
2018-01-02 13:13:48 +00:00
|
|
|
private_key = asymmetric.load_private_key(self_key_path)
|
|
|
|
except FileNotFoundError: # certificate or private key not found
|
|
|
|
with open(self_key_path, 'wb') as fh:
|
2018-04-09 13:08:12 +00:00
|
|
|
if public_key.algorithm == "ec":
|
|
|
|
self_public_key, private_key = asymmetric.generate_pair("ec", curve=public_key.curve)
|
|
|
|
elif public_key.algorithm == "rsa":
|
|
|
|
self_public_key, private_key = asymmetric.generate_pair("rsa", bit_size=public_key.bit_size)
|
|
|
|
else:
|
|
|
|
NotImplemented
|
2018-01-02 13:13:48 +00:00
|
|
|
fh.write(asymmetric.dump_private_key(private_key, None))
|
|
|
|
else:
|
|
|
|
now = datetime.utcnow()
|
2018-01-23 13:13:49 +00:00
|
|
|
if now + timedelta(days=1) < expires:
|
2018-01-02 13:13:48 +00:00
|
|
|
click.echo("Certificate %s still valid, delete to self-enroll again" % path)
|
|
|
|
return
|
|
|
|
|
2018-04-09 13:08:12 +00:00
|
|
|
builder = CSRBuilder({"common_name": common_name}, self_public_key)
|
2017-12-30 13:57:48 +00:00
|
|
|
request = builder.build(private_key)
|
|
|
|
with open(os.path.join(directory, "requests", common_name + ".pem"), "wb") as fh:
|
|
|
|
fh.write(pem_armor_csr(request))
|
|
|
|
pid = os.fork()
|
|
|
|
if not pid:
|
|
|
|
from certidude import authority
|
|
|
|
from certidude.common import drop_privileges
|
|
|
|
drop_privileges()
|
2018-03-03 13:54:31 +00:00
|
|
|
authority.sign(common_name, skip_push=True, overwrite=True, profile="srv")
|
2017-12-30 13:57:48 +00:00
|
|
|
sys.exit(0)
|
|
|
|
else:
|
|
|
|
os.waitpid(pid, 0)
|
|
|
|
if os.path.exists("/etc/systemd"):
|
|
|
|
os.system("systemctl reload nginx")
|
|
|
|
else:
|
|
|
|
os.system("service nginx reload")
|
|
|
|
|
|
|
|
|
2015-12-12 22:34:08 +00:00
|
|
|
def get_request(common_name):
|
|
|
|
if not re.match(RE_HOSTNAME, common_name):
|
2016-03-21 21:42:39 +00:00
|
|
|
raise ValueError("Invalid common name %s" % repr(common_name))
|
2017-03-13 11:42:58 +00:00
|
|
|
path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
2017-05-06 21:07:41 +00:00
|
|
|
try:
|
2017-12-30 13:57:48 +00:00
|
|
|
with open(path, "rb") as fh:
|
2017-05-06 21:07:41 +00:00
|
|
|
buf = fh.read()
|
2017-08-16 20:25:16 +00:00
|
|
|
header, _, der_bytes = pem.unarmor(buf)
|
2017-12-30 13:57:48 +00:00
|
|
|
return path, buf, CertificationRequest.load(der_bytes), \
|
|
|
|
datetime.utcfromtimestamp(os.stat(path).st_ctime)
|
2017-05-06 21:07:41 +00:00
|
|
|
except EnvironmentError:
|
|
|
|
raise errors.RequestDoesNotExist("Certificate signing request file %s does not exist" % path)
|
2016-03-21 21:42:39 +00:00
|
|
|
|
2015-12-12 22:34:08 +00:00
|
|
|
def get_signed(common_name):
|
|
|
|
if not re.match(RE_HOSTNAME, common_name):
|
2016-03-21 21:42:39 +00:00
|
|
|
raise ValueError("Invalid common name %s" % repr(common_name))
|
2017-03-13 11:42:58 +00:00
|
|
|
path = os.path.join(config.SIGNED_DIR, common_name + ".pem")
|
2017-12-30 13:57:48 +00:00
|
|
|
with open(path, "rb") as fh:
|
2017-03-13 11:42:58 +00:00
|
|
|
buf = fh.read()
|
2017-08-16 20:25:16 +00:00
|
|
|
header, _, der_bytes = pem.unarmor(buf)
|
2017-12-30 13:57:48 +00:00
|
|
|
cert = x509.Certificate.load(der_bytes)
|
|
|
|
return path, buf, cert, \
|
|
|
|
cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None), \
|
|
|
|
cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None)
|
2015-12-12 22:34:08 +00:00
|
|
|
|
2017-03-13 11:42:58 +00:00
|
|
|
def get_revoked(serial):
|
2017-12-30 13:57:48 +00:00
|
|
|
if isinstance(serial, str):
|
|
|
|
serial = int(serial, 16)
|
2017-05-25 19:20:29 +00:00
|
|
|
path = os.path.join(config.REVOKED_DIR, "%x.pem" % serial)
|
2017-12-30 13:57:48 +00:00
|
|
|
with open(path, "rb") as fh:
|
2017-03-13 11:42:58 +00:00
|
|
|
buf = fh.read()
|
2017-08-16 20:25:16 +00:00
|
|
|
header, _, der_bytes = pem.unarmor(buf)
|
2017-12-30 13:57:48 +00:00
|
|
|
cert = x509.Certificate.load(der_bytes)
|
|
|
|
return path, buf, cert, \
|
|
|
|
cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None), \
|
|
|
|
cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None), \
|
2017-05-25 19:20:29 +00:00
|
|
|
datetime.utcfromtimestamp(os.stat(path).st_ctime)
|
2016-03-21 21:42:39 +00:00
|
|
|
|
2017-05-04 17:56:53 +00:00
|
|
|
|
2017-07-05 15:22:03 +00:00
|
|
|
def get_attributes(cn, namespace=None):
|
2017-12-30 13:57:48 +00:00
|
|
|
path, buf, cert, signed, expires = get_signed(cn)
|
2017-05-04 17:56:53 +00:00
|
|
|
attribs = dict()
|
|
|
|
for key in listxattr(path):
|
2017-12-30 13:57:48 +00:00
|
|
|
key = key.decode("ascii")
|
2017-05-04 17:56:53 +00:00
|
|
|
if not key.startswith("user."):
|
|
|
|
continue
|
2017-07-05 15:22:03 +00:00
|
|
|
if namespace and not key.startswith("user.%s." % namespace):
|
|
|
|
continue
|
2017-05-04 17:56:53 +00:00
|
|
|
value = getxattr(path, key)
|
|
|
|
current = attribs
|
|
|
|
if "." in key:
|
2017-07-05 15:22:03 +00:00
|
|
|
prefix, key = key.rsplit(".", 1)
|
|
|
|
for component in prefix.split("."):
|
2017-05-04 17:56:53 +00:00
|
|
|
if component not in current:
|
|
|
|
current[component] = dict()
|
|
|
|
current = current[component]
|
2017-12-30 13:57:48 +00:00
|
|
|
current[key] = value.decode("utf-8")
|
2017-05-04 17:56:53 +00:00
|
|
|
return path, buf, cert, attribs
|
|
|
|
|
|
|
|
|
2017-08-09 21:45:43 +00:00
|
|
|
def store_request(buf, overwrite=False, address="", user=""):
|
2015-12-12 22:34:08 +00:00
|
|
|
"""
|
|
|
|
Store CSR for later processing
|
|
|
|
"""
|
2016-09-17 21:00:14 +00:00
|
|
|
|
2017-03-13 11:42:58 +00:00
|
|
|
if not buf:
|
2017-05-04 09:14:47 +00:00
|
|
|
raise ValueError("No signing request supplied")
|
2016-09-17 21:00:14 +00:00
|
|
|
|
2017-08-16 20:25:16 +00:00
|
|
|
if pem.detect(buf):
|
|
|
|
header, _, der_bytes = pem.unarmor(buf)
|
|
|
|
csr = CertificationRequest.load(der_bytes)
|
2017-05-18 19:29:49 +00:00
|
|
|
else:
|
2017-08-16 20:25:16 +00:00
|
|
|
csr = CertificationRequest.load(buf)
|
|
|
|
buf = pem_armor_csr(csr)
|
2015-12-12 22:34:08 +00:00
|
|
|
|
2017-08-16 20:25:16 +00:00
|
|
|
common_name = csr["certification_request_info"]["subject"].native["common_name"]
|
|
|
|
|
|
|
|
if not re.match(RE_HOSTNAME, common_name):
|
2015-12-12 22:34:08 +00:00
|
|
|
raise ValueError("Invalid common name")
|
|
|
|
|
2017-08-16 20:25:16 +00:00
|
|
|
request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
2017-03-13 11:42:58 +00:00
|
|
|
|
|
|
|
|
2015-12-12 22:34:08 +00:00
|
|
|
# If there is cert, check if it's the same
|
2017-05-18 19:29:49 +00:00
|
|
|
if os.path.exists(request_path) and not overwrite:
|
2017-12-30 13:57:48 +00:00
|
|
|
if open(request_path, "rb").read() == buf:
|
2016-01-14 22:47:30 +00:00
|
|
|
raise errors.RequestExists("Request already exists")
|
2016-01-14 09:02:57 +00:00
|
|
|
else:
|
2016-01-14 22:47:30 +00:00
|
|
|
raise errors.DuplicateCommonNameError("Another request with same common name already exists")
|
2015-12-12 22:34:08 +00:00
|
|
|
else:
|
2017-12-30 13:57:48 +00:00
|
|
|
with open(request_path + ".part", "wb") as fh:
|
2016-01-14 08:44:26 +00:00
|
|
|
fh.write(buf)
|
2015-12-12 22:34:08 +00:00
|
|
|
os.rename(request_path + ".part", request_path)
|
|
|
|
|
2017-08-16 20:25:16 +00:00
|
|
|
attach_csr = buf, "application/x-pem-file", common_name + ".csr"
|
2017-03-13 11:42:58 +00:00
|
|
|
mailer.send("request-stored.md",
|
|
|
|
attachments=(attach_csr,),
|
2017-08-16 20:25:16 +00:00
|
|
|
common_name=common_name)
|
2017-08-09 21:45:43 +00:00
|
|
|
setxattr(request_path, "user.request.address", address)
|
|
|
|
setxattr(request_path, "user.request.user", user)
|
2017-12-30 13:57:48 +00:00
|
|
|
try:
|
|
|
|
hostname, aliaslist, ipaddrlist = socket.gethostbyaddr(address)
|
|
|
|
except (socket.herror, OSError): # Failed to resolve hostname or resolved to multiple
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
setxattr(request_path, "user.request.hostname", hostname)
|
2017-08-16 20:25:16 +00:00
|
|
|
return request_path, csr, common_name
|
2015-12-12 22:34:08 +00:00
|
|
|
|
|
|
|
|
2017-03-13 11:42:58 +00:00
|
|
|
def revoke(common_name):
|
2015-12-12 22:34:08 +00:00
|
|
|
"""
|
|
|
|
Revoke valid certificate
|
|
|
|
"""
|
2017-12-30 13:57:48 +00:00
|
|
|
signed_path, buf, cert, signed, expires = get_signed(common_name)
|
2017-08-16 20:25:16 +00:00
|
|
|
revoked_path = os.path.join(config.REVOKED_DIR, "%x.pem" % cert.serial_number)
|
2017-12-30 13:57:48 +00:00
|
|
|
|
2017-08-16 20:25:16 +00:00
|
|
|
os.unlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number))
|
2017-12-30 13:57:48 +00:00
|
|
|
os.rename(signed_path, revoked_path)
|
|
|
|
|
2017-05-25 19:20:29 +00:00
|
|
|
|
2017-03-13 11:42:58 +00:00
|
|
|
push.publish("certificate-revoked", common_name)
|
2017-01-30 06:29:01 +00:00
|
|
|
|
|
|
|
# Publish CRL for long polls
|
2017-05-07 19:11:24 +00:00
|
|
|
url = config.LONG_POLL_PUBLISH % "crl"
|
|
|
|
click.echo("Publishing CRL at %s ..." % url)
|
|
|
|
requests.post(url, data=export_crl(),
|
|
|
|
headers={"User-Agent": "Certidude API", "Content-Type": "application/x-pem-file"})
|
2017-01-30 06:29:01 +00:00
|
|
|
|
2017-03-13 11:42:58 +00:00
|
|
|
attach_cert = buf, "application/x-pem-file", common_name + ".crt"
|
|
|
|
mailer.send("certificate-revoked.md",
|
|
|
|
attachments=(attach_cert,),
|
2017-08-16 20:25:16 +00:00
|
|
|
serial_hex="%x" % cert.serial_number,
|
2017-03-13 11:42:58 +00:00
|
|
|
common_name=common_name)
|
2017-04-13 15:42:38 +00:00
|
|
|
return revoked_path
|
2017-03-13 11:42:58 +00:00
|
|
|
|
|
|
|
def server_flags(cn):
|
|
|
|
if config.USER_ENROLLMENT_ALLOWED and not config.USER_MULTIPLE_CERTIFICATES:
|
|
|
|
# Common name set to username, used for only HTTPS client validation anyway
|
|
|
|
return False
|
|
|
|
if "@" in cn:
|
|
|
|
# username@hostname is user certificate anyway, can't be server
|
|
|
|
return False
|
|
|
|
if "." in cn:
|
|
|
|
# CN is hostname, if contains dot has to be FQDN, hence a server
|
|
|
|
return True
|
|
|
|
return False
|
2015-12-12 22:34:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
def list_requests(directory=config.REQUESTS_DIR):
|
|
|
|
for filename in os.listdir(directory):
|
|
|
|
if filename.endswith(".pem"):
|
2017-03-13 11:42:58 +00:00
|
|
|
common_name = filename[:-4]
|
2017-12-30 13:57:48 +00:00
|
|
|
path, buf, req, submitted = get_request(common_name)
|
|
|
|
yield common_name, path, buf, req, submitted, "." in common_name
|
2015-12-12 22:34:08 +00:00
|
|
|
|
2017-03-13 11:42:58 +00:00
|
|
|
def _list_certificates(directory):
|
2015-12-12 22:34:08 +00:00
|
|
|
for filename in os.listdir(directory):
|
|
|
|
if filename.endswith(".pem"):
|
2017-03-13 11:42:58 +00:00
|
|
|
path = os.path.join(directory, filename)
|
2017-12-30 13:57:48 +00:00
|
|
|
with open(path, "rb") as fh:
|
2017-03-13 11:42:58 +00:00
|
|
|
buf = fh.read()
|
2017-08-16 20:25:16 +00:00
|
|
|
header, _, der_bytes = pem.unarmor(buf)
|
|
|
|
cert = x509.Certificate.load(der_bytes)
|
2017-03-13 11:42:58 +00:00
|
|
|
server = False
|
2017-08-16 20:25:16 +00:00
|
|
|
for extension in cert["tbs_certificate"]["extensions"]:
|
2017-12-30 13:57:48 +00:00
|
|
|
if extension["extn_id"].native == "extended_key_usage":
|
|
|
|
if "server_auth" in extension["extn_value"].native:
|
2017-08-16 20:25:16 +00:00
|
|
|
server = True
|
2017-12-30 13:57:48 +00:00
|
|
|
yield cert.subject.native["common_name"], path, buf, cert, server
|
2017-03-13 11:42:58 +00:00
|
|
|
|
2017-12-30 13:57:48 +00:00
|
|
|
def list_signed(directory=config.SIGNED_DIR):
|
|
|
|
for filename in os.listdir(directory):
|
|
|
|
if filename.endswith(".pem"):
|
|
|
|
common_name = filename[:-4]
|
|
|
|
path, buf, cert, signed, expires = get_signed(common_name)
|
|
|
|
yield common_name, path, buf, cert, signed, expires
|
2017-03-13 11:42:58 +00:00
|
|
|
|
2017-12-30 13:57:48 +00:00
|
|
|
def list_revoked(directory=config.REVOKED_DIR):
|
|
|
|
for filename in os.listdir(directory):
|
|
|
|
if filename.endswith(".pem"):
|
|
|
|
common_name = filename[:-4]
|
|
|
|
path, buf, cert, signed, expired, revoked = get_revoked(common_name)
|
|
|
|
yield cert.subject.native["common_name"], path, buf, cert, signed, expired, revoked
|
2015-12-12 22:34:08 +00:00
|
|
|
|
2017-04-24 17:33:55 +00:00
|
|
|
def list_server_names():
|
|
|
|
return [cn for cn, path, buf, cert, server in list_signed() if server]
|
|
|
|
|
2017-08-16 20:25:16 +00:00
|
|
|
def export_crl(pem=True):
|
|
|
|
builder = CertificateListBuilder(
|
|
|
|
config.AUTHORITY_CRL_URL,
|
|
|
|
certificate,
|
|
|
|
1 # TODO: monotonically increasing
|
|
|
|
)
|
|
|
|
|
2016-01-01 23:08:04 +00:00
|
|
|
for filename in os.listdir(config.REVOKED_DIR):
|
2015-12-12 22:34:08 +00:00
|
|
|
if not filename.endswith(".pem"):
|
|
|
|
continue
|
|
|
|
serial_number = filename[:-4]
|
|
|
|
# TODO: Assert serial against regex
|
2016-01-01 23:08:04 +00:00
|
|
|
revoked_path = os.path.join(config.REVOKED_DIR, filename)
|
2015-12-12 22:34:08 +00:00
|
|
|
# TODO: Skip expired certificates
|
|
|
|
s = os.stat(revoked_path)
|
2017-08-16 20:25:16 +00:00
|
|
|
builder.add_certificate(
|
|
|
|
int(filename[:-4], 16),
|
|
|
|
datetime.utcfromtimestamp(s.st_ctime),
|
2017-12-30 13:57:48 +00:00
|
|
|
"key_compromise")
|
2017-08-16 20:25:16 +00:00
|
|
|
|
|
|
|
certificate_list = builder.build(private_key)
|
|
|
|
if pem:
|
|
|
|
return pem_armor_crl(certificate_list)
|
|
|
|
return certificate_list.dump()
|
2015-12-12 22:34:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
def delete_request(common_name):
|
|
|
|
# Validate CN
|
|
|
|
if not re.match(RE_HOSTNAME, common_name):
|
|
|
|
raise ValueError("Invalid common name")
|
|
|
|
|
2017-12-30 13:57:48 +00:00
|
|
|
path, buf, csr, submitted = get_request(common_name)
|
2015-12-12 22:34:08 +00:00
|
|
|
os.unlink(path)
|
|
|
|
|
|
|
|
# Publish event at CA channel
|
2017-03-13 11:42:58 +00:00
|
|
|
push.publish("request-deleted", common_name)
|
2015-12-12 22:34:08 +00:00
|
|
|
|
|
|
|
# Write empty certificate to long-polling URL
|
2017-05-07 19:11:24 +00:00
|
|
|
requests.delete(
|
|
|
|
config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(),
|
|
|
|
headers={"User-Agent": "Certidude API"})
|
2015-12-12 22:34:08 +00:00
|
|
|
|
2018-03-03 13:54:31 +00:00
|
|
|
def sign(common_name, skip_notify=False, skip_push=False, overwrite=False, profile=None, signer=None):
|
2015-12-12 22:34:08 +00:00
|
|
|
"""
|
2017-08-16 20:25:16 +00:00
|
|
|
Sign certificate signing request by it's common name
|
2015-12-12 22:34:08 +00:00
|
|
|
"""
|
2017-03-13 11:42:58 +00:00
|
|
|
|
|
|
|
req_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
2017-12-30 13:57:48 +00:00
|
|
|
with open(req_path, "rb") as fh:
|
2017-03-13 11:42:58 +00:00
|
|
|
csr_buf = fh.read()
|
2017-08-16 20:25:16 +00:00
|
|
|
header, _, der_bytes = pem.unarmor(csr_buf)
|
|
|
|
csr = CertificationRequest.load(der_bytes)
|
|
|
|
|
2017-03-13 11:42:58 +00:00
|
|
|
|
|
|
|
# Sign with function below
|
2018-01-23 13:13:49 +00:00
|
|
|
cert, buf = _sign(csr, csr_buf, skip_notify, skip_push, overwrite, profile, signer)
|
2017-03-13 11:42:58 +00:00
|
|
|
|
|
|
|
os.unlink(req_path)
|
|
|
|
return cert, buf
|
|
|
|
|
2018-03-03 13:54:31 +00:00
|
|
|
def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, profile=None, signer=None):
|
2017-08-16 20:25:16 +00:00
|
|
|
# TODO: CRLDistributionPoints, OCSP URL, Certificate URL
|
2018-01-23 13:13:49 +00:00
|
|
|
if profile not in config.PROFILES:
|
|
|
|
raise ValueError("Invalid profile supplied '%s'" % profile)
|
2017-04-13 20:30:28 +00:00
|
|
|
|
2018-03-03 13:54:31 +00:00
|
|
|
assert buf.startswith(b"-----BEGIN ")
|
2017-08-16 20:25:16 +00:00
|
|
|
assert isinstance(csr, CertificationRequest)
|
|
|
|
csr_pubkey = asymmetric.load_public_key(csr["certification_request_info"]["subject_pk_info"])
|
|
|
|
common_name = csr["certification_request_info"]["subject"].native["common_name"]
|
|
|
|
cert_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name)
|
2017-03-13 11:42:58 +00:00
|
|
|
renew = False
|
2015-12-12 22:34:08 +00:00
|
|
|
|
2017-04-22 19:48:29 +00:00
|
|
|
attachments = [
|
2017-08-16 20:25:16 +00:00
|
|
|
(buf, "application/x-pem-file", common_name + ".csr"),
|
2017-04-22 19:48:29 +00:00
|
|
|
]
|
|
|
|
|
2017-03-26 00:10:09 +00:00
|
|
|
revoked_path = None
|
2017-04-22 19:48:29 +00:00
|
|
|
overwritten = False
|
2017-03-26 00:10:09 +00:00
|
|
|
|
2015-12-12 22:34:08 +00:00
|
|
|
# Move existing certificate if necessary
|
|
|
|
if os.path.exists(cert_path):
|
2017-12-30 13:57:48 +00:00
|
|
|
with open(cert_path, "rb") as fh:
|
2017-03-13 11:42:58 +00:00
|
|
|
prev_buf = fh.read()
|
2017-08-16 20:25:16 +00:00
|
|
|
header, _, der_bytes = pem.unarmor(prev_buf)
|
|
|
|
prev = x509.Certificate.load(der_bytes)
|
|
|
|
|
2017-03-13 11:42:58 +00:00
|
|
|
# TODO: assert validity here again?
|
2017-08-16 20:25:16 +00:00
|
|
|
renew = \
|
|
|
|
asymmetric.load_public_key(prev["tbs_certificate"]["subject_public_key_info"]) == \
|
|
|
|
csr_pubkey
|
|
|
|
# BUGBUG: is this enough?
|
2017-03-13 11:42:58 +00:00
|
|
|
|
2015-12-12 22:34:08 +00:00
|
|
|
if overwrite:
|
2017-04-22 19:48:29 +00:00
|
|
|
# TODO: is this the best approach?
|
2017-08-16 20:25:16 +00:00
|
|
|
prev_serial_hex = "%x" % prev.serial_number
|
2017-04-22 19:48:29 +00:00
|
|
|
revoked_path = os.path.join(config.REVOKED_DIR, "%s.pem" % prev_serial_hex)
|
|
|
|
os.rename(cert_path, revoked_path)
|
|
|
|
attachments += [(prev_buf, "application/x-pem-file", "deprecated.crt" if renew else "overwritten.crt")]
|
|
|
|
overwritten = True
|
2015-12-12 22:34:08 +00:00
|
|
|
else:
|
2017-12-30 13:57:48 +00:00
|
|
|
raise FileExistsError("Will not overwrite existing certificate")
|
2015-12-12 22:34:08 +00:00
|
|
|
|
|
|
|
# Sign via signer process
|
2017-12-30 13:57:48 +00:00
|
|
|
dn = {u'common_name': common_name }
|
2018-01-23 13:13:49 +00:00
|
|
|
profile_server_flags, lifetime, dn["organizational_unit_name"], _ = config.PROFILES[profile]
|
|
|
|
lifetime = int(lifetime)
|
|
|
|
|
2017-12-30 13:57:48 +00:00
|
|
|
builder = CertificateBuilder(dn, csr_pubkey)
|
2017-08-16 20:25:16 +00:00
|
|
|
builder.serial_number = random.randint(
|
|
|
|
0x1000000000000000000000000000000000000000,
|
|
|
|
0xffffffffffffffffffffffffffffffffffffffff)
|
|
|
|
|
|
|
|
now = datetime.utcnow()
|
|
|
|
builder.begin_date = now - timedelta(minutes=5)
|
2018-01-23 13:13:49 +00:00
|
|
|
builder.end_date = now + timedelta(days=lifetime)
|
2017-08-16 20:25:16 +00:00
|
|
|
builder.issuer = certificate
|
|
|
|
builder.ca = False
|
2017-12-30 13:57:48 +00:00
|
|
|
builder.key_usage = set(["digital_signature", "key_encipherment"])
|
2017-08-16 20:25:16 +00:00
|
|
|
|
2018-01-23 13:13:49 +00:00
|
|
|
# If we have FQDN and profile suggests server flags, enable them
|
|
|
|
if server_flags(common_name) and profile_server_flags:
|
|
|
|
builder.subject_alt_domains = [common_name] # OpenVPN uses CN while StrongSwan uses SAN to match hostname of the server
|
2017-12-30 13:57:48 +00:00
|
|
|
builder.extended_key_usage = set(["server_auth", "1.3.6.1.5.5.8.2.2", "client_auth"])
|
2017-08-16 20:25:16 +00:00
|
|
|
else:
|
2018-04-09 13:08:12 +00:00
|
|
|
builder.subject_alt_domains = [common_name] # iOS demands SAN also for clients
|
2017-12-30 13:57:48 +00:00
|
|
|
builder.extended_key_usage = set(["client_auth"])
|
2017-08-16 20:25:16 +00:00
|
|
|
|
|
|
|
end_entity_cert = builder.build(private_key)
|
|
|
|
end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert)
|
2015-12-12 22:34:08 +00:00
|
|
|
with open(cert_path + ".part", "wb") as fh:
|
2017-08-16 20:25:16 +00:00
|
|
|
fh.write(end_entity_cert_buf)
|
|
|
|
|
2015-12-12 22:34:08 +00:00
|
|
|
os.rename(cert_path + ".part", cert_path)
|
2017-08-16 20:25:16 +00:00
|
|
|
attachments.append((end_entity_cert_buf, "application/x-pem-file", common_name + ".crt"))
|
|
|
|
cert_serial_hex = "%x" % end_entity_cert.serial_number
|
2015-12-12 22:34:08 +00:00
|
|
|
|
2017-05-25 19:20:29 +00:00
|
|
|
# Create symlink
|
2017-08-16 20:25:16 +00:00
|
|
|
link_name = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % end_entity_cert.serial_number)
|
|
|
|
assert not os.path.exists(link_name), "Certificate with same serial number already exists: %s" % link_name
|
|
|
|
os.symlink("../%s.pem" % common_name, link_name)
|
2017-05-25 19:20:29 +00:00
|
|
|
|
2017-03-26 00:10:09 +00:00
|
|
|
# Copy filesystem attributes to newly signed certificate
|
|
|
|
if revoked_path:
|
|
|
|
for key in listxattr(revoked_path):
|
2017-12-30 13:57:48 +00:00
|
|
|
if not key.startswith(b"user."):
|
2017-03-26 00:10:09 +00:00
|
|
|
continue
|
2017-04-22 19:48:29 +00:00
|
|
|
setxattr(cert_path, key, getxattr(revoked_path, key))
|
2017-03-26 00:10:09 +00:00
|
|
|
|
2017-12-30 13:57:48 +00:00
|
|
|
# Attach signer username
|
|
|
|
if signer:
|
|
|
|
setxattr(cert_path, "user.signature.username", signer)
|
|
|
|
|
|
|
|
if not skip_notify:
|
|
|
|
# Send mail
|
|
|
|
if renew: # Same keypair
|
|
|
|
mailer.send("certificate-renewed.md", **locals())
|
|
|
|
else: # New keypair
|
|
|
|
mailer.send("certificate-signed.md", **locals())
|
2016-09-17 21:00:14 +00:00
|
|
|
|
2017-12-30 13:57:48 +00:00
|
|
|
if not skip_push:
|
|
|
|
url = config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest()
|
|
|
|
click.echo("Publishing certificate at %s ..." % url)
|
|
|
|
requests.post(url, data=end_entity_cert_buf,
|
|
|
|
headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"})
|
2017-03-13 11:42:58 +00:00
|
|
|
|
2017-12-30 13:57:48 +00:00
|
|
|
push.publish("request-signed", common_name)
|
2017-08-16 20:25:16 +00:00
|
|
|
return end_entity_cert, end_entity_cert_buf
|