mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 08:15:18 +00:00
Released 0.1.17
This commit is contained in:
parent
f24ef4024c
commit
c5d27e8a76
@ -3,3 +3,8 @@ include certidude/templates/*.html
|
||||
include certidude/templates/*.svg
|
||||
include certidude/templates/*.ovpn
|
||||
include certidude/templates/*.cnf
|
||||
include certidude/templates/*.conf
|
||||
include certidude/templates/*.ini
|
||||
include certidude/static/js/*.js
|
||||
include certidude/static/css/*.css
|
||||
include certidude/static/*.html
|
||||
|
30
README.rst
30
README.rst
@ -13,13 +13,14 @@ Features
|
||||
--------
|
||||
|
||||
* Standard request, sign, revoke workflow via web interface.
|
||||
* Colored command-line interface, check out ``certidude list``
|
||||
* OpenVPN integration, check out ``certidude setup openvpn server`` and ``certidude setup openvpn client``
|
||||
* Colored command-line interface, check out ``certidude list``.
|
||||
* OpenVPN integration, check out ``certidude setup openvpn server`` and ``certidude setup openvpn client``.
|
||||
* strongSwan integration, check out ``certidude setup strongswan server`` and ``certidude setup strongswan client``.
|
||||
* Privilege isolation, separate signer process is spawned per private key isolating
|
||||
private key use from the the web interface.
|
||||
* Certificate numbering obfuscation, certificate serial numbers are intentionally
|
||||
randomized to avoid leaking information about business practices.
|
||||
* Server-side events support via for example nginx-push-stream-module
|
||||
* Server-side events support via for example nginx-push-stream-module.
|
||||
|
||||
|
||||
TODO
|
||||
@ -27,7 +28,6 @@ TODO
|
||||
|
||||
* Refactor mailing subsystem and server-side events to use hooks.
|
||||
* Notifications via e-mail.
|
||||
* strongSwan setup integration.
|
||||
* OCSP support.
|
||||
* Deep mailbox integration, eg fetch CSR-s from mailbox via IMAP.
|
||||
* WebCrypto support, meanwhile check out `hwcrypto.js <https://github.com/open-eid/hwcrypto.js>`_.
|
||||
@ -42,14 +42,14 @@ To install Certidude:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
apt-get install python3 python3-pip python3-dev cython3 build-essential libffi-dev libssl-dev
|
||||
apt-get install -y python3 python3-netifaces python3-pip python3-dev cython3 build-essential libffi-dev libssl-dev
|
||||
pip3 install certidude
|
||||
|
||||
Create a user for ``certidude``:
|
||||
|
||||
Create a system user for ``certidude``:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
useradd certidude
|
||||
adduser --system --no-create-home --group certidude
|
||||
|
||||
|
||||
Setting up CA
|
||||
@ -64,6 +64,12 @@ Certidude can set up CA relatively easily:
|
||||
Tweak command-line options until you meet your requirements and
|
||||
then insert generated section to your /etc/ssl/openssl.cnf
|
||||
|
||||
Spawn the signer process:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
certidude spawn
|
||||
|
||||
Finally serve the certificate authority via web:
|
||||
|
||||
.. code:: bash
|
||||
@ -102,7 +108,13 @@ Install uWSGI:
|
||||
|
||||
apt-get install uwsgi uwsgi-plugin-python3
|
||||
|
||||
Configure uUWSGI application in ``/etc/uwsgi/apps-available/certidude.ini``:
|
||||
To set up ``nginx`` and ``uwsgi`` is suggested:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
certidude setup production
|
||||
|
||||
Otherwise manually configure uUWSGI application in ``/etc/uwsgi/apps-available/certidude.ini``:
|
||||
|
||||
.. code:: ini
|
||||
|
||||
|
@ -191,6 +191,16 @@ class RequestListResource(CertificateAuthorityBase):
|
||||
"""
|
||||
Submit certificate signing request (CSR) in PEM format
|
||||
"""
|
||||
# Parse remote IPv4/IPv6 address
|
||||
remote_addr = ipaddress.ip_address(req.env["REMOTE_ADDR"])
|
||||
|
||||
# Check for CSR submission whitelist
|
||||
if ca.request_whitelist:
|
||||
for subnet in ca.request_whitelist:
|
||||
if subnet.overlaps(remote_addr):
|
||||
break
|
||||
else:
|
||||
raise falcon.HTTPForbidden("IP address %s not whitelisted" % remote_addr)
|
||||
|
||||
if req.get_header("Content-Type") != "application/pkcs10":
|
||||
raise falcon.HTTPUnsupportedMediaType(
|
||||
@ -207,20 +217,23 @@ class RequestListResource(CertificateAuthorityBase):
|
||||
else:
|
||||
cert = Certificate(cert_buf)
|
||||
if cert.pubkey == csr.pubkey:
|
||||
resp.status = falcon.HTTP_FOUND
|
||||
resp.status = falcon.HTTP_SEE_OTHER
|
||||
resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", csr.common_name)
|
||||
return
|
||||
|
||||
# TODO: check for revoked certificates and return HTTP 410 Gone
|
||||
|
||||
# Process automatic signing if the IP address is whitelisted and autosigning was requested
|
||||
if ca.autosign_allowed(req.env["REMOTE_ADDR"]) and req.get_param("autosign"):
|
||||
try:
|
||||
resp.append_header("Content-Type", "application/x-x509-user-cert")
|
||||
resp.body = ca.sign(req).dump()
|
||||
return
|
||||
except FileExistsError: # Certificate already exists, try to save the request
|
||||
pass
|
||||
if req.get_param("autosign").lower() in ("yes", "1", "true"):
|
||||
for subnet in ca.autosign_whitelist:
|
||||
if subnet.overlaps(remote_addr):
|
||||
try:
|
||||
resp.append_header("Content-Type", "application/x-x509-user-cert")
|
||||
resp.body = ca.sign(req).dump()
|
||||
return
|
||||
except FileExistsError: # Certificate already exists, try to save the request
|
||||
pass
|
||||
break
|
||||
|
||||
# Attempt to save the request otherwise
|
||||
try:
|
||||
@ -237,7 +250,7 @@ class RequestListResource(CertificateAuthorityBase):
|
||||
# Redirect to nginx pub/sub
|
||||
url = url_template % dict(channel=request.fingerprint())
|
||||
click.echo("Redirecting to: %s" % url)
|
||||
resp.status = falcon.HTTP_FOUND
|
||||
resp.status = falcon.HTTP_SEE_OTHER
|
||||
resp.append_header("Location", url)
|
||||
else:
|
||||
click.echo("Using dummy streaming mode, please switch to nginx in production!", err=True)
|
||||
|
481
certidude/cli.py
481
certidude/cli.py
@ -1,33 +1,35 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
|
||||
import sys
|
||||
import asyncore
|
||||
import click
|
||||
import falcon
|
||||
import logging
|
||||
import mimetypes
|
||||
import netifaces
|
||||
import os
|
||||
import pwd
|
||||
import random
|
||||
import socket
|
||||
import click
|
||||
import os
|
||||
import asyncore
|
||||
import time
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
import signal
|
||||
import netifaces
|
||||
import urllib.request
|
||||
import socket
|
||||
import subprocess
|
||||
from humanize import naturaltime
|
||||
from ipaddress import ip_network
|
||||
from time import sleep
|
||||
from datetime import datetime
|
||||
from OpenSSL import crypto
|
||||
from setproctitle import setproctitle
|
||||
import sys
|
||||
import time
|
||||
from certidude.helpers import expand_paths, \
|
||||
certidude_request_certificate
|
||||
from certidude.signer import SignServer
|
||||
from jinja2 import Environment, PackageLoader
|
||||
from certidude.wrappers import CertificateAuthorityConfig, \
|
||||
CertificateAuthority, Certificate, subject2dn, Request
|
||||
from datetime import datetime
|
||||
from humanize import naturaltime
|
||||
from ipaddress import ip_network
|
||||
from jinja2 import Environment, PackageLoader
|
||||
from time import sleep
|
||||
from setproctitle import setproctitle
|
||||
from OpenSSL import crypto
|
||||
|
||||
env = Environment(loader=PackageLoader("certidude", "templates"))
|
||||
env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True)
|
||||
|
||||
# Big fat warning:
|
||||
# m2crypto overflows around 2030 because on 32-bit systems
|
||||
@ -42,17 +44,20 @@ assert hasattr(crypto.X509Req(), "get_extensions"), "You're running too old vers
|
||||
# http://www.mad-hacking.net/documentation/linux/security/ssl-tls/creating-ca.xml
|
||||
# https://kjur.github.io/jsrsasign/
|
||||
# keyUsage, extendedKeyUsage - https://www.openssl.org/docs/apps/x509v3_config.html
|
||||
# strongSwan key paths - https://wiki.strongswan.org/projects/1/wiki/SimpleCA
|
||||
|
||||
config = CertificateAuthorityConfig("/etc/ssl/openssl.cnf")
|
||||
|
||||
# Parse command-line argument defaults from environment
|
||||
HOSTNAME = socket.gethostname()
|
||||
USERNAME = os.environ.get("USER")
|
||||
EMAIL = USERNAME + "@" + HOSTNAME
|
||||
NOW = datetime.utcnow().replace(tzinfo=None)
|
||||
|
||||
FIRST_NAME = None
|
||||
SURNAME = None
|
||||
EMAIL = None
|
||||
|
||||
if USERNAME:
|
||||
EMAIL = USERNAME + "@" + HOSTNAME
|
||||
|
||||
if os.getuid() >= 1000:
|
||||
_, _, _, _, gecos, _, _ = pwd.getpwnam(USERNAME)
|
||||
@ -61,33 +66,18 @@ if os.getuid() >= 1000:
|
||||
else:
|
||||
FIRST_NAME = gecos
|
||||
|
||||
def first_nic_address():
|
||||
"""
|
||||
Return IP address of the first network interface
|
||||
"""
|
||||
for interface in netifaces.interfaces():
|
||||
if interface == "lo":
|
||||
continue
|
||||
for iftype, addresses in netifaces.ifaddresses(interface).items():
|
||||
if iftype != 2:
|
||||
continue
|
||||
for address in addresses:
|
||||
return address.pop("addr")
|
||||
raise ValueError("Unable to determine IP address of first NIC")
|
||||
DEFAULT_ROUTE, PRIMARY_INTERFACE = netifaces.gateways().get("default").get(2)
|
||||
PRIMARY_ALIASES = netifaces.ifaddresses(PRIMARY_INTERFACE).get(2)
|
||||
PRIMARY_ADDRESS = PRIMARY_ALIASES[0].get("addr")
|
||||
|
||||
def spawn_signers(kill, no_interaction):
|
||||
@click.command("spawn", help="Run privilege isolated signer processes")
|
||||
@click.option("-k", "--kill", default=False, is_flag=True, help="Kill previous instances")
|
||||
@click.option("-n", "--no-interaction", default=True, is_flag=True, help="Don't load password protected keys")
|
||||
def certidude_spawn(kill, no_interaction):
|
||||
"""
|
||||
Spawn processes for signers
|
||||
"""
|
||||
|
||||
os.umask(0o027)
|
||||
uid = os.getuid()
|
||||
assert uid == 0, "Not running as root"
|
||||
|
||||
# Preload charmap encoding for byte_string() function of pyOpenSSL
|
||||
# in order to enable chrooting
|
||||
"".encode("charmap")
|
||||
|
||||
# Process directories
|
||||
run_dir = "/run/certidude"
|
||||
signer_dir = os.path.join(run_dir, "signer")
|
||||
@ -98,6 +88,14 @@ def spawn_signers(kill, no_interaction):
|
||||
click.echo("Creating: %s" % signer_dir)
|
||||
os.makedirs(signer_dir)
|
||||
|
||||
os.umask(0o027)
|
||||
uid = os.getuid()
|
||||
assert uid == 0, "Not running as root"
|
||||
|
||||
# Preload charmap encoding for byte_string() function of pyOpenSSL
|
||||
# in order to enable chrooting
|
||||
"".encode("charmap")
|
||||
|
||||
# Prepare chroot directories
|
||||
if not os.path.exists(os.path.join(chroot_dir, "dev")):
|
||||
os.makedirs(os.path.join(chroot_dir, "dev"))
|
||||
@ -106,11 +104,11 @@ def spawn_signers(kill, no_interaction):
|
||||
os.system("mknod -m 444 %s c 1 9" % os.path.join(chroot_dir, "dev", "urandom"))
|
||||
|
||||
for ca in config.all_authorities():
|
||||
|
||||
pidfile = "/run/certidude/signer/%s.pid" % ca.slug
|
||||
socket_path = os.path.join(signer_dir, ca.slug + ".sock")
|
||||
pidfile_path = os.path.join(signer_dir, ca.slug + ".pid")
|
||||
|
||||
try:
|
||||
with open(pidfile) as fh:
|
||||
with open(pidfile_path) as fh:
|
||||
pid = int(fh.readline())
|
||||
os.kill(pid, 0)
|
||||
click.echo("Found process with PID %d for %s" % (pid, ca.slug))
|
||||
@ -133,158 +131,19 @@ def spawn_signers(kill, no_interaction):
|
||||
child_pid = os.fork()
|
||||
|
||||
if child_pid == 0:
|
||||
with open(pidfile, "w") as fh:
|
||||
with open(pidfile_path, "w") as fh:
|
||||
fh.write("%d\n" % os.getpid())
|
||||
|
||||
setproctitle("%s spawn %s" % (sys.argv[0], ca.slug))
|
||||
logging.basicConfig(
|
||||
filename="/var/log/certidude-%s.log" % ca.slug,
|
||||
level=logging.INFO)
|
||||
socket_path = os.path.join(signer_dir, ca.slug + ".sock")
|
||||
click.echo("Spawned certidude signer process with PID %d at %s" % (os.getpid(), socket_path))
|
||||
server = SignServer(socket_path, ca.private_key, ca.certificate.path,
|
||||
ca.lifetime, ca.basic_constraints, ca.key_usage, ca.extended_key_usage)
|
||||
ca.certificate_lifetime, ca.basic_constraints, ca.key_usage,
|
||||
ca.extended_key_usage, ca.revocation_list_lifetime)
|
||||
asyncore.loop()
|
||||
|
||||
def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_path, common_name, org_unit, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None):
|
||||
"""
|
||||
Exchange CSR for certificate using Certidude HTTP API server
|
||||
"""
|
||||
|
||||
# Set up URL-s
|
||||
request_params = set()
|
||||
if autosign:
|
||||
request_params.add("autosign=yes")
|
||||
if wait:
|
||||
request_params.add("wait=forever")
|
||||
|
||||
if not url.endswith("/"):
|
||||
url = url + "/"
|
||||
|
||||
authority_url = url + "certificate"
|
||||
request_url = url + "request"
|
||||
|
||||
if request_params:
|
||||
request_url = request_url + "?" + "&".join(request_params)
|
||||
|
||||
if os.path.exists(authority_path):
|
||||
click.echo("Found CA certificate in: %s" % authority_path)
|
||||
else:
|
||||
if authority_url:
|
||||
click.echo("Attempting to fetch CA certificate from %s" % authority_url)
|
||||
try:
|
||||
with urllib.request.urlopen(authority_url) as fh:
|
||||
buf = fh.read()
|
||||
try:
|
||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, buf)
|
||||
except crypto.Error:
|
||||
raise ValueError("Failed to parse PEM: %s" % buf)
|
||||
with open(authority_path + ".part", "wb") as oh:
|
||||
oh.write(buf)
|
||||
click.echo("Writing CA certificate to: %s" % authority_path)
|
||||
os.rename(authority_path + ".part", authority_path)
|
||||
except urllib.error.HTTPError as e:
|
||||
click.echo("Failed to fetch CA certificate, server responded with: %d %s" % (e.code, e.reason), err=True)
|
||||
return 1
|
||||
else:
|
||||
raise FileNotFoundError("CA certificate not found and no URL specified")
|
||||
|
||||
try:
|
||||
certificate = Certificate(open(certificate_path))
|
||||
click.echo("Found certificate: %s" % certificate_path)
|
||||
except FileNotFoundError:
|
||||
try:
|
||||
request = Request(open(request_path))
|
||||
click.echo("Found signing request: %s" % request_path)
|
||||
except FileNotFoundError:
|
||||
|
||||
# Construct private key
|
||||
click.echo("Generating 4096-bit RSA key...")
|
||||
key = crypto.PKey()
|
||||
key.generate_key(crypto.TYPE_RSA, 4096)
|
||||
|
||||
# Dump private key
|
||||
os.umask(0o077)
|
||||
with open(key_path + ".part", "wb") as fh:
|
||||
fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
|
||||
|
||||
# Construct CSR
|
||||
csr = crypto.X509Req()
|
||||
csr.set_pubkey(key)
|
||||
request = Request(csr)
|
||||
|
||||
# Set subject attributes
|
||||
request.common_name = common_name
|
||||
if given_name:
|
||||
request.given_name = given_name
|
||||
if surname:
|
||||
request.surname = surname
|
||||
if org_unit:
|
||||
request.organizational_unit = org_unit
|
||||
|
||||
# Set extensions
|
||||
extensions = []
|
||||
if key_usage:
|
||||
extensions.append(("keyUsage", key_usage, True))
|
||||
if extended_key_usage:
|
||||
extensions.append(("extendedKeyUsage", extended_key_usage, True))
|
||||
if email_address:
|
||||
extensions.append(("subjectAltName", "email:" + email_address, False))
|
||||
request.set_extensions(extensions)
|
||||
|
||||
# Dump CSR
|
||||
os.umask(0o022)
|
||||
with open(request_path + ".part", "w") as fh:
|
||||
fh.write(request.dump())
|
||||
|
||||
click.echo("Writing private key to: %s" % key_path)
|
||||
os.rename(key_path + ".part", key_path)
|
||||
click.echo("Writing certificate signing request to: %s" % request_path)
|
||||
os.rename(request_path + ".part", request_path)
|
||||
|
||||
|
||||
with open(request_path, "rb") as fh:
|
||||
buf = fh.read()
|
||||
submission = urllib.request.Request(request_url, buf)
|
||||
submission.add_header("User-Agent", "Certidude")
|
||||
submission.add_header("Content-Type", "application/pkcs10")
|
||||
|
||||
click.echo("Submitting to %s, waiting for response..." % request_url)
|
||||
try:
|
||||
response = urllib.request.urlopen(submission)
|
||||
buf = response.read()
|
||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, buf)
|
||||
except crypto.Error:
|
||||
raise ValueError("Failed to parse PEM: %s" % buf)
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 409:
|
||||
click.echo("Different signing request with same CN is already present on server, server refuses to overwrite", err=True)
|
||||
return 2
|
||||
else:
|
||||
click.echo("Failed to fetch certificate, server responded with: %d %s" % (e.code, e.reason), err=True)
|
||||
return 3
|
||||
else:
|
||||
if response.code == 202:
|
||||
click.echo("Server stored the request for processing (202 Accepted), but waiting was not requested, hence quitting for now", err=True)
|
||||
return 254
|
||||
|
||||
os.umask(0o022)
|
||||
with open(certificate_path + ".part", "wb") as gh:
|
||||
gh.write(buf)
|
||||
|
||||
click.echo("Writing certificate to: %s" % certificate_path)
|
||||
os.rename(certificate_path + ".part", certificate_path)
|
||||
|
||||
# TODO: Validate fetched certificate against CA
|
||||
# TODO: Check that recevied certificate CN and pubkey match
|
||||
# TODO: Check file permissions
|
||||
|
||||
|
||||
@click.command("spawn", help="Run privilege isolated signer processes")
|
||||
@click.option("-k", "--kill", default=False, is_flag=True, help="Kill previous instances")
|
||||
@click.option("-n", "--no-interaction", default=True, is_flag=True, help="Don't load password protected keys")
|
||||
def certidude_spawn(**args):
|
||||
spawn_signers(**args)
|
||||
click.echo("Spawned certidude signer process with PID %d at %s" % (child_pid, socket_path))
|
||||
|
||||
|
||||
@click.command("client", help="Setup X.509 certificates for application")
|
||||
@ -313,9 +172,10 @@ def certidude_setup_client(quiet, **kwargs):
|
||||
@click.option("--org-unit", "-ou", help="Organizational unit")
|
||||
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL)
|
||||
@click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="OpenVPN subnet, 192.168.33.0/24 by default")
|
||||
@click.option("--local", "-l", default=first_nic_address(), help="OpenVPN listening address, %s" % first_nic_address())
|
||||
@click.option("--local", "-l", default=PRIMARY_ADDRESS, help="OpenVPN listening address, %s" % PRIMARY_ADDRESS)
|
||||
@click.option("--port", "-p", default=1194, type=click.IntRange(1,60000), help="OpenVPN listening port, 1194 by default")
|
||||
@click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default")
|
||||
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
|
||||
@click.option("--config", "-o",
|
||||
default="/etc/openvpn/site-to-client.conf",
|
||||
type=click.File(mode="w", atomic=True, lazy=True),
|
||||
@ -326,7 +186,8 @@ def certidude_setup_client(quiet, **kwargs):
|
||||
@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME)
|
||||
@click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to --directory by default")
|
||||
@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir by default")
|
||||
def certidude_setup_openvpn_server(url, config, subnet, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, dhparam_path, local, proto, port):
|
||||
@expand_paths()
|
||||
def certidude_setup_openvpn_server(url, config, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, dhparam_path, local, proto, port):
|
||||
# TODO: Intelligent way of getting last IP address in the subnet
|
||||
subnet_first = None
|
||||
subnet_last = None
|
||||
@ -339,16 +200,6 @@ def certidude_setup_openvpn_server(url, config, subnet, email_address, common_na
|
||||
subnet_second = addr
|
||||
subnet_last = addr
|
||||
|
||||
if directory:
|
||||
if not os.path.exists(directory):
|
||||
click.echo("Making directory: %s" % directory)
|
||||
os.makedirs(directory)
|
||||
key_path = os.path.join(directory, key_path)
|
||||
certificate_path = os.path.join(directory, certificate_path)
|
||||
request_path = os.path.join(directory, request_path)
|
||||
authority_path = os.path.join(directory, authority_path)
|
||||
dhparam_path = os.path.join(directory, dhparam_path)
|
||||
|
||||
if not os.path.exists(certificate_path):
|
||||
click.echo("As OpenVPN server certificate needs specific key usage extensions please")
|
||||
click.echo("use following command to sign on Certidude server instead of web interface:")
|
||||
@ -365,7 +216,7 @@ def certidude_setup_openvpn_server(url, config, subnet, email_address, common_na
|
||||
org_unit,
|
||||
email_address,
|
||||
key_usage="nonRepudiation,digitalSignature,keyEncipherment",
|
||||
extended_key_usage="serverAuth",
|
||||
extended_key_usage="serverAuth,ikeIntermediate",
|
||||
wait=True)
|
||||
|
||||
if not os.path.exists(dhparam_path):
|
||||
@ -376,7 +227,7 @@ def certidude_setup_openvpn_server(url, config, subnet, email_address, common_na
|
||||
return retval
|
||||
|
||||
# TODO: Add dhparam
|
||||
config.write(env.get_template("site-to-client.ovpn").render(locals()))
|
||||
config.write(env.get_template("openvpn-site-to-client.ovpn").render(locals()))
|
||||
|
||||
click.echo("Generated %s" % config.name)
|
||||
click.echo()
|
||||
@ -402,17 +253,9 @@ def certidude_setup_openvpn_server(url, config, subnet, email_address, common_na
|
||||
@click.option("--request-path", "-r", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to --directory by default" % HOSTNAME)
|
||||
@click.option("--certificate-path", "-c", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME)
|
||||
@click.option("--authority-path", "-a", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir by default")
|
||||
@expand_paths()
|
||||
def certidude_setup_openvpn_client(url, config, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, proto, remote):
|
||||
|
||||
if directory:
|
||||
if not os.path.exists(directory):
|
||||
click.echo("Making directory: %s" % directory)
|
||||
os.makedirs(directory)
|
||||
key_path = os.path.join(directory, key_path)
|
||||
certificate_path = os.path.join(directory, certificate_path)
|
||||
request_path = os.path.join(directory, request_path)
|
||||
authority_path = os.path.join(directory, authority_path)
|
||||
|
||||
retval = certidude_request_certificate(
|
||||
url,
|
||||
key_path,
|
||||
@ -428,7 +271,7 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
|
||||
return retval
|
||||
|
||||
# TODO: Add dhparam
|
||||
config.write(env.get_template("client-to-site.ovpn").render(locals()))
|
||||
config.write(env.get_template("openvpn-client-to-site.ovpn").render(locals()))
|
||||
|
||||
click.echo("Generated %s" % config.name)
|
||||
click.echo()
|
||||
@ -438,6 +281,164 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
|
||||
click.echo()
|
||||
|
||||
|
||||
@click.command("server", help="Set up strongSwan server")
|
||||
@click.argument("url")
|
||||
@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME)
|
||||
@click.option("--org-unit", "-ou", help="Organizational unit")
|
||||
@click.option("--fqdn", "-f", default=HOSTNAME, help="Fully qualified hostname, %s by default" % PRIMARY_ADDRESS)
|
||||
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, %s by default" % EMAIL)
|
||||
@click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="IPsec virtual subnet, 192.168.33.0/24 by default")
|
||||
@click.option("--local", "-l", default=PRIMARY_ADDRESS, help="IPsec gateway address, %s" % PRIMARY_ADDRESS)
|
||||
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
|
||||
@click.option("--config", "-o",
|
||||
default="/etc/ipsec.conf",
|
||||
type=click.File(mode="w", atomic=True, lazy=True),
|
||||
help="strongSwan configuration file, /etc/ipsec.conf by default")
|
||||
@click.option("--secrets", "-s",
|
||||
default="/etc/ipsec.secrets",
|
||||
type=click.File(mode="w", atomic=True, lazy=True),
|
||||
help="strongSwan secrets file, /etc/ipsec.secrets by default")
|
||||
@click.option("--directory", "-d", default="/etc/ipsec.d", help="Directory for keys, /etc/ipsec.d by default")
|
||||
@click.option("--key-path", "-key", default="private/%s.pem" % HOSTNAME, help="Key path, private/%s.pem by default" % HOSTNAME)
|
||||
@click.option("--request-path", "-csr", default="reqs/%s.pem" % HOSTNAME, help="Request path, reqs/%s.pem by default" % HOSTNAME)
|
||||
@click.option("--certificate-path", "-crt", default="certs/%s.pem" % HOSTNAME, help="Certificate path, certs/%s.pem by default" % HOSTNAME)
|
||||
@click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default")
|
||||
@expand_paths()
|
||||
def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, local, ip_address, fqdn):
|
||||
|
||||
config.write(env.get_template("strongswan-site-to-client.conf").render(locals()))
|
||||
|
||||
if not os.path.exists(certificate_path):
|
||||
click.echo("As strongSwan server certificate needs specific key usage extensions please")
|
||||
click.echo("use following command to sign on Certidude server instead of web interface:")
|
||||
click.echo()
|
||||
click.echo(" certidude sign %s" % common_name)
|
||||
|
||||
retval = certidude_request_certificate(
|
||||
url,
|
||||
key_path,
|
||||
request_path,
|
||||
certificate_path,
|
||||
authority_path,
|
||||
common_name,
|
||||
org_unit,
|
||||
email_address,
|
||||
key_usage="nonRepudiation,digitalSignature,keyEncipherment",
|
||||
extended_key_usage="serverAuth,ikeIntermediate",
|
||||
ipv4_address=None if local.is_private else local,
|
||||
dns=None if local.is_private or "." not in fdqn else fdqn,
|
||||
wait=True)
|
||||
|
||||
if retval:
|
||||
return retval
|
||||
|
||||
|
||||
click.echo("Generated %s" % config.name)
|
||||
click.echo()
|
||||
click.echo("Inspect newly created %s and start strongSwan service:" % config.name)
|
||||
click.echo()
|
||||
click.echo(" apt-get install strongswan strongswan-starter strongswan-ikev2")
|
||||
click.secho(" service strongswan restart", bold=True)
|
||||
click.echo()
|
||||
|
||||
|
||||
@click.command("client", help="Set up strongSwan client")
|
||||
@click.argument("url")
|
||||
@click.argument("remote")
|
||||
@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME)
|
||||
@click.option("--org-unit", "-ou", help="Organizational unit")
|
||||
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL)
|
||||
@click.option("--config", "-o",
|
||||
default="/etc/ipsec.conf",
|
||||
type=click.File(mode="w", atomic=True, lazy=True),
|
||||
help="strongSwan configuration file, /etc/ipsec.conf by default")
|
||||
@click.option("--secrets", "-s",
|
||||
default="/etc/ipsec.secrets",
|
||||
type=click.File(mode="w", atomic=True, lazy=True),
|
||||
help="strongSwan secrets file, /etc/ipsec.secrets by default")
|
||||
@click.option("--dpdaction", "-d",
|
||||
default="restart",
|
||||
type=click.Choice(["none", "clear", "hold", "restart"]),
|
||||
help="Action upon dead peer detection; either none, clear, hold or restart")
|
||||
@click.option("--auto", "-a",
|
||||
default="start",
|
||||
type=click.Choice(["ignore", "add", "route", "start"]),
|
||||
help="Operation at startup; either ignore, add, route or start")
|
||||
@click.option("--directory", "-d", default="/etc/ipsec.d", help="Directory for keys, /etc/ipsec.d by default")
|
||||
@click.option("--key-path", "-key", default="private/%s.pem" % HOSTNAME, help="Key path, private/%s.pem by default" % HOSTNAME)
|
||||
@click.option("--request-path", "-csr", default="reqs/%s.pem" % HOSTNAME, help="Request path, reqs/%s.pem by default" % HOSTNAME)
|
||||
@click.option("--certificate-path", "-crt", default="certs/%s.pem" % HOSTNAME, help="Certificate path, certs/%s.pem by default" % HOSTNAME)
|
||||
@click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default")
|
||||
@expand_paths()
|
||||
def certidude_setup_strongswan_client(url, config, secrets, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote, auto, dpdaction):
|
||||
|
||||
retval = certidude_request_certificate(
|
||||
url,
|
||||
key_path,
|
||||
request_path,
|
||||
certificate_path,
|
||||
authority_path,
|
||||
common_name,
|
||||
org_unit,
|
||||
email_address,
|
||||
wait=True)
|
||||
|
||||
if retval:
|
||||
return retval
|
||||
|
||||
# TODO: Add dhparam
|
||||
config.write(env.get_template("strongswan-client-to-site.conf").render(locals()))
|
||||
|
||||
click.echo("Generated %s" % config.name)
|
||||
click.echo()
|
||||
click.echo("Inspect newly created %s and start strongSwan service:" % config.name)
|
||||
click.echo()
|
||||
click.echo(" apt-get install strongswan strongswan-starter")
|
||||
click.echo(" service strongswan restart")
|
||||
click.echo()
|
||||
|
||||
|
||||
@click.command("production", help="Set up nginx and uwsgi")
|
||||
@click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default")
|
||||
@click.option("--hostname", default=HOSTNAME, help="nginx hostname, '%s' by default" % HOSTNAME)
|
||||
@click.option("--static-path", default=os.path.join(os.path.dirname(__file__), "static"), help="Static files")
|
||||
@click.option("--nginx-config", "-n",
|
||||
default="/etc/nginx/nginx.conf",
|
||||
type=click.File(mode="w", atomic=True, lazy=True),
|
||||
help="nginx configuration, /etc/nginx/nginx.conf by default")
|
||||
@click.option("--uwsgi-config", "-u",
|
||||
default="/etc/uwsgi/apps-available/certidude.ini",
|
||||
type=click.File(mode="w", atomic=True, lazy=True),
|
||||
help="uwsgi configuration, /etc/uwsgi/ by default")
|
||||
@click.option("--push-server", help="Push server URL, in case of different nginx instance")
|
||||
def certidude_setup_production(username, hostname, push_server, nginx_config, uwsgi_config, static_path):
|
||||
try:
|
||||
pwd.getpwnam(username)
|
||||
click.echo("Username '%s' already exists, excellent!" % username)
|
||||
except KeyError:
|
||||
cmd = "adduser", "--system", "--no-create-home", "--group", username
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
# cmd = "gpasswd", "-a", username, "www-data"
|
||||
# subprocess.check_call(cmd)
|
||||
|
||||
if not static_path.endswith("/"):
|
||||
static_path += "/"
|
||||
|
||||
nginx_config.write(env.get_template("nginx.conf").render(locals()))
|
||||
click.echo("Generated: %s" % nginx_config.name)
|
||||
uwsgi_config.write(env.get_template("uwsgi.ini").render(locals()))
|
||||
click.echo("Generated: %s" % uwsgi_config.name)
|
||||
|
||||
if os.path.exists("/etc/uwsgi/apps-enabled/certidude.ini"):
|
||||
os.unlink("/etc/uwsgi/apps-enabled/certidude.ini")
|
||||
os.symlink(uwsgi_config.name, "/etc/uwsgi/apps-enabled/certidude.ini")
|
||||
click.echo("Symlinked %s -> /etc/uwsgi/apps-enabled/certidude.ini" % uwsgi_config.name)
|
||||
|
||||
if not push_server:
|
||||
click.echo("Remember to install nginx with wandenberg/nginx-push-stream-module!")
|
||||
|
||||
|
||||
@click.command("authority", help="Set up Certificate Authority in a directory")
|
||||
@click.option("--group", "-g", default="certidude", help="Group for file permissions, certidude by default")
|
||||
@click.option("--parent", "-p", help="Parent CA, none by default")
|
||||
@ -445,10 +446,11 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
|
||||
@click.option("--country", "-c", default="ee", help="Country, Estonia by default")
|
||||
@click.option("--state", "-s", default="Harjumaa", help="State or country, Harjumaa by default")
|
||||
@click.option("--locality", "-l", default="Tallinn", help="City or locality, Tallinn by default")
|
||||
@click.option("--lifetime", default=20*365, help="Lifetime in days, 7300 days (20 years) by default")
|
||||
@click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 7300 days (20 years) by default")
|
||||
@click.option("--certificate-lifetime", default=5*365, help="Certificate lifetime in days, 1825 days (5 years) by default")
|
||||
@click.option("--revocation-list-lifetime", default=1, help="Revocation list lifetime in days, 1 day by default")
|
||||
@click.option("--organization", "-o", default="Example LLC", help="Company or organization name")
|
||||
@click.option("--organizational-unit", "-ou", default="Certification Department")
|
||||
@click.option("--crl-age", default=1, help="CRL expiration age, 1 day by default")
|
||||
@click.option("--pkcs11", default=False, is_flag=True, help="Use PKCS#11 token instead of files")
|
||||
@click.option("--crl-distribution-url", default=None, help="CRL distribution URL")
|
||||
@click.option("--ocsp-responder-url", default=None, help="OCSP responder URL")
|
||||
@ -456,7 +458,7 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
|
||||
@click.option("--inbox", default="imap://user:pass@host:port/INBOX", help="Inbound e-mail server")
|
||||
@click.option("--outbox", default="smtp://localhost", help="Outbound e-mail server")
|
||||
@click.argument("directory")
|
||||
def certidude_setup_authority(parent, country, state, locality, organization, organizational_unit, common_name, directory, crl_age, lifetime, pkcs11, group, crl_distribution_url, ocsp_responder_url, email_address, inbox, outbox):
|
||||
def certidude_setup_authority(parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, pkcs11, group, crl_distribution_url, ocsp_responder_url, email_address, inbox, outbox):
|
||||
logging.info("Creating certificate authority in %s", directory)
|
||||
_, _, uid, gid, gecos, root, shell = pwd.getpwnam(group)
|
||||
os.setgid(gid)
|
||||
@ -481,7 +483,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
|
||||
crl_distribution_points = "URI:%s" % crl_distribution_url
|
||||
|
||||
ca = crypto.X509()
|
||||
#ca.set_version(3) # breaks gcr-viewer?!
|
||||
ca.set_version(2) # This corresponds to X.509v3
|
||||
ca.set_serial_number(1)
|
||||
ca.get_subject().CN = common_name
|
||||
ca.get_subject().C = country
|
||||
@ -490,7 +492,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
|
||||
ca.get_subject().O = organization
|
||||
ca.get_subject().OU = organizational_unit
|
||||
ca.gmtime_adj_notBefore(0)
|
||||
ca.gmtime_adj_notAfter(lifetime * 24 * 60 * 60)
|
||||
ca.gmtime_adj_notAfter(authority_lifetime * 24 * 60 * 60)
|
||||
ca.set_issuer(ca.get_subject())
|
||||
ca.set_pubkey(key)
|
||||
ca.add_extensions([
|
||||
@ -522,7 +524,10 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
|
||||
subject_alt_name.encode("ascii"))
|
||||
])
|
||||
|
||||
if not ocsp_responder_url:
|
||||
if ocsp_responder_url:
|
||||
raise NotImplementedError()
|
||||
|
||||
"""
|
||||
ocsp_responder_url = "http://%s/api/%s/ocsp/" % (common_name, slug)
|
||||
authority_info_access = "OCSP;URI:%s" % ocsp_responder_url
|
||||
ca.add_extensions([
|
||||
@ -531,6 +536,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
|
||||
False,
|
||||
authority_info_access.encode("ascii"))
|
||||
])
|
||||
"""
|
||||
|
||||
click.echo("Signing %s..." % subject2dn(ca.get_subject()))
|
||||
|
||||
@ -550,7 +556,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
|
||||
os.mkdir(os.path.join(directory, subdir))
|
||||
with open(ca_crl, "wb") as fh:
|
||||
crl = crypto.CRL()
|
||||
fh.write(crl.export(ca, key, days=crl_age))
|
||||
fh.write(crl.export(ca, key, days=revocation_list_lifetime))
|
||||
with open(os.path.join(directory, "serial"), "w") as fh:
|
||||
fh.write("1")
|
||||
|
||||
@ -730,12 +736,35 @@ def certidude_sign(common_name, overwrite, lifetime):
|
||||
else:
|
||||
# Sign directly using private key
|
||||
cert = ca.sign2(request, overwrite, True, lifetime)
|
||||
os.unlink(request.path)
|
||||
|
||||
click.echo("Signed %s" % cert.distinguished_name)
|
||||
for key, value, data in cert.extensions:
|
||||
click.echo("Added extension %s: %s" % (key, value))
|
||||
click.echo()
|
||||
|
||||
class StaticResource(object):
|
||||
def __init__(self, root):
|
||||
self.root = os.path.realpath(root)
|
||||
click.echo("Serving static from: %s" % self.root)
|
||||
|
||||
def __call__(self, req, resp):
|
||||
|
||||
path = os.path.realpath(os.path.join(self.root, req.path[1:]))
|
||||
if not path.startswith(self.root):
|
||||
raise falcon.HTTPForbidden
|
||||
|
||||
print("Serving:", path)
|
||||
if os.path.exists(path):
|
||||
content_type, content_encoding = mimetypes.guess_type(path)
|
||||
if content_type:
|
||||
resp.append_header("Content-Type", content_type)
|
||||
if content_encoding:
|
||||
resp.append_header("Content-Encoding", content_encoding)
|
||||
resp.append_header("Content-Disposition", "attachment")
|
||||
resp.stream = open(path, "rb")
|
||||
else:
|
||||
resp.status = falcon.HTTP_404
|
||||
resp.body = "File '%s' not found" % req.path
|
||||
|
||||
@click.command("serve", help="Run built-in HTTP server")
|
||||
@click.option("-u", "--user", default="certidude", help="Run as user")
|
||||
@ -743,7 +772,6 @@ def certidude_sign(common_name, overwrite, lifetime):
|
||||
@click.option("-l", "--listen", default="0.0.0.0", help="Listen address")
|
||||
@click.option("-s", "--enable-signature", default=False, is_flag=True, help="Allow signing operations with private key of CA")
|
||||
def certidude_serve(user, port, listen, enable_signature):
|
||||
spawn_signers(kill=False, no_interaction=False)
|
||||
|
||||
logging.basicConfig(
|
||||
filename='/var/log/certidude.log',
|
||||
@ -775,6 +803,8 @@ def certidude_serve(user, port, listen, enable_signature):
|
||||
app.add_route("/api/{ca}/request/{cn}/", RequestDetailResource(config))
|
||||
app.add_route("/api/{ca}/request/", RequestListResource(config))
|
||||
app.add_route("/api/{ca}/", IndexResource(config))
|
||||
|
||||
app.add_sink(StaticResource(os.path.join(os.path.dirname(__file__), "static")))
|
||||
httpd = make_server(listen, port, app, ThreadingWSGIServer)
|
||||
if user:
|
||||
_, _, uid, gid, gecos, root, shell = pwd.getpwnam(user)
|
||||
@ -789,6 +819,9 @@ def certidude_serve(user, port, listen, enable_signature):
|
||||
click.echo("Warning: running as root, this is not reccommended!")
|
||||
httpd.serve_forever()
|
||||
|
||||
@click.group("strongswan", help="strongSwan helpers")
|
||||
def certidude_setup_strongswan(): pass
|
||||
|
||||
@click.group("openvpn", help="OpenVPN helpers")
|
||||
def certidude_setup_openvpn(): pass
|
||||
|
||||
@ -798,11 +831,15 @@ def certidude_setup(): pass
|
||||
@click.group()
|
||||
def entry_point(): pass
|
||||
|
||||
certidude_setup_strongswan.add_command(certidude_setup_strongswan_server)
|
||||
certidude_setup_strongswan.add_command(certidude_setup_strongswan_client)
|
||||
certidude_setup_openvpn.add_command(certidude_setup_openvpn_server)
|
||||
certidude_setup_openvpn.add_command(certidude_setup_openvpn_client)
|
||||
certidude_setup.add_command(certidude_setup_authority)
|
||||
certidude_setup.add_command(certidude_setup_openvpn)
|
||||
certidude_setup.add_command(certidude_setup_strongswan)
|
||||
certidude_setup.add_command(certidude_setup_client)
|
||||
certidude_setup.add_command(certidude_setup_production)
|
||||
entry_point.add_command(certidude_setup)
|
||||
entry_point.add_command(certidude_serve)
|
||||
entry_point.add_command(certidude_spawn)
|
||||
|
183
certidude/helpers.py
Normal file
183
certidude/helpers.py
Normal file
@ -0,0 +1,183 @@
|
||||
|
||||
import click
|
||||
import logging
|
||||
import netifaces
|
||||
import os
|
||||
import urllib.request
|
||||
from certidude.wrappers import Certificate, Request
|
||||
from certidude.signer import SignServer
|
||||
from OpenSSL import crypto
|
||||
|
||||
def expand_paths():
|
||||
"""
|
||||
Prefix '..._path' keyword arguments of target function with 'directory' keyword argument
|
||||
and create the directory if necessary
|
||||
|
||||
TODO: Move to separate file
|
||||
"""
|
||||
def wrapper(func):
|
||||
def wrapped(**arguments):
|
||||
d = arguments.get("directory")
|
||||
for key, value in arguments.items():
|
||||
if key.endswith("_path"):
|
||||
if d:
|
||||
value = os.path.join(d, value)
|
||||
value = os.path.realpath(value)
|
||||
parent = os.path.dirname(value)
|
||||
if not os.path.exists(parent):
|
||||
click.echo("Making directory %s for %s" % (repr(parent), repr(key)))
|
||||
os.makedirs(parent)
|
||||
elif not os.path.isdir(parent):
|
||||
raise Exception("Path %s is not directory!" % parent)
|
||||
arguments[key] = value
|
||||
return func(**arguments)
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
|
||||
def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_path, common_name, org_unit, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None):
|
||||
"""
|
||||
Exchange CSR for certificate using Certidude HTTP API server
|
||||
"""
|
||||
|
||||
# Set up URL-s
|
||||
request_params = set()
|
||||
if autosign:
|
||||
request_params.add("autosign=yes")
|
||||
if wait:
|
||||
request_params.add("wait=forever")
|
||||
|
||||
if not url.endswith("/"):
|
||||
url = url + "/"
|
||||
|
||||
authority_url = url + "certificate"
|
||||
request_url = url + "request"
|
||||
|
||||
if request_params:
|
||||
request_url = request_url + "?" + "&".join(request_params)
|
||||
|
||||
if os.path.exists(authority_path):
|
||||
click.echo("Found CA certificate in: %s" % authority_path)
|
||||
else:
|
||||
if authority_url:
|
||||
click.echo("Attempting to fetch CA certificate from %s" % authority_url)
|
||||
try:
|
||||
with urllib.request.urlopen(authority_url) as fh:
|
||||
buf = fh.read()
|
||||
try:
|
||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, buf)
|
||||
except crypto.Error:
|
||||
raise ValueError("Failed to parse PEM: %s" % buf)
|
||||
with open(authority_path + ".part", "wb") as oh:
|
||||
oh.write(buf)
|
||||
click.echo("Writing CA certificate to: %s" % authority_path)
|
||||
os.rename(authority_path + ".part", authority_path)
|
||||
except urllib.error.HTTPError as e:
|
||||
click.echo("Failed to fetch CA certificate, server responded with: %d %s" % (e.code, e.reason), err=True)
|
||||
return 1
|
||||
else:
|
||||
raise FileNotFoundError("CA certificate not found and no URL specified")
|
||||
|
||||
try:
|
||||
certificate = Certificate(open(certificate_path))
|
||||
click.echo("Found certificate: %s" % certificate_path)
|
||||
except FileNotFoundError:
|
||||
try:
|
||||
request = Request(open(request_path))
|
||||
click.echo("Found signing request: %s" % request_path)
|
||||
except FileNotFoundError:
|
||||
|
||||
# Construct private key
|
||||
click.echo("Generating 4096-bit RSA key...")
|
||||
key = crypto.PKey()
|
||||
key.generate_key(crypto.TYPE_RSA, 4096)
|
||||
|
||||
# Dump private key
|
||||
os.umask(0o077)
|
||||
with open(key_path + ".part", "wb") as fh:
|
||||
fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
|
||||
|
||||
# Construct CSR
|
||||
csr = crypto.X509Req()
|
||||
csr.set_version(2) # Corresponds to X.509v3
|
||||
csr.set_pubkey(key)
|
||||
request = Request(csr)
|
||||
|
||||
# Set subject attributes
|
||||
request.common_name = common_name
|
||||
if given_name:
|
||||
request.given_name = given_name
|
||||
if surname:
|
||||
request.surname = surname
|
||||
if org_unit:
|
||||
request.organizational_unit = org_unit
|
||||
|
||||
# Collect subject alternative names
|
||||
subject_alt_name = set()
|
||||
if email_address:
|
||||
subject_alt_name.add("email:" + email_address)
|
||||
if ip_address:
|
||||
subject_alt_name.add("IP:" + ip_address)
|
||||
if dns:
|
||||
subject_alt_name.add("DNS:" + dns)
|
||||
|
||||
# Set extensions
|
||||
extensions = []
|
||||
if key_usage:
|
||||
extensions.append(("keyUsage", key_usage, True))
|
||||
if extended_key_usage:
|
||||
extensions.append(("extendedKeyUsage", extended_key_usage, True))
|
||||
if subject_alt_name:
|
||||
extensions.append(("subjectAltName", ", ".join(subject_alt_name), True))
|
||||
request.set_extensions(extensions)
|
||||
|
||||
# Dump CSR
|
||||
os.umask(0o022)
|
||||
with open(request_path + ".part", "w") as fh:
|
||||
fh.write(request.dump())
|
||||
|
||||
click.echo("Writing private key to: %s" % key_path)
|
||||
os.rename(key_path + ".part", key_path)
|
||||
click.echo("Writing certificate signing request to: %s" % request_path)
|
||||
os.rename(request_path + ".part", request_path)
|
||||
|
||||
|
||||
with open(request_path, "rb") as fh:
|
||||
buf = fh.read()
|
||||
submission = urllib.request.Request(request_url, buf)
|
||||
submission.add_header("User-Agent", "Certidude")
|
||||
submission.add_header("Content-Type", "application/pkcs10")
|
||||
|
||||
click.echo("Submitting to %s, waiting for response..." % request_url)
|
||||
try:
|
||||
response = urllib.request.urlopen(submission)
|
||||
buf = response.read()
|
||||
if response.code == 202:
|
||||
click.echo("No waiting was requested and server responded with 202 Accepted, run this command again once the certificate is signed")
|
||||
return 1
|
||||
assert buf, "Server responded with no body, status code %d" % response.code
|
||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, buf)
|
||||
except crypto.Error:
|
||||
raise ValueError("Failed to parse PEM: %s" % buf)
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 409:
|
||||
click.echo("Different signing request with same CN is already present on server, server refuses to overwrite", err=True)
|
||||
return 2
|
||||
else:
|
||||
click.echo("Failed to fetch certificate, server responded with: %d %s" % (e.code, e.reason), err=True)
|
||||
return 3
|
||||
else:
|
||||
if response.code == 202:
|
||||
click.echo("Server stored the request for processing (202 Accepted), but waiting was not requested, hence quitting for now", err=True)
|
||||
return 254
|
||||
|
||||
os.umask(0o022)
|
||||
with open(certificate_path + ".part", "wb") as gh:
|
||||
gh.write(buf)
|
||||
|
||||
click.echo("Writing certificate to: %s" % certificate_path)
|
||||
os.rename(certificate_path + ".part", certificate_path)
|
||||
|
||||
# TODO: Validate fetched certificate against CA
|
||||
# TODO: Check that recevied certificate CN and pubkey match
|
||||
# TODO: Check file permissions
|
@ -33,7 +33,9 @@ def raw_sign(private_key, ca_cert, request, basic_constraints, lifetime, key_usa
|
||||
Sign certificate signing request directly with private key assuming it's readable by the process
|
||||
"""
|
||||
|
||||
# Initialize X.509 certificate object
|
||||
cert = crypto.X509()
|
||||
ca.set_version(2) # This corresponds to X.509v3
|
||||
|
||||
# Set public key
|
||||
cert.set_pubkey(request.get_pubkey())
|
||||
@ -130,7 +132,8 @@ class SignHandler(asynchat.async_chat):
|
||||
self.send(crl.export(
|
||||
self.server.certificate,
|
||||
self.server.private_key,
|
||||
crypto.FILETYPE_PEM))
|
||||
crypto.FILETYPE_PEM,
|
||||
self.server.revocation_list_lifetime))
|
||||
|
||||
elif cmd == "ocsp-request":
|
||||
NotImplemented # TODO: Implement OCSP
|
||||
@ -168,7 +171,7 @@ class SignHandler(asynchat.async_chat):
|
||||
|
||||
|
||||
class SignServer(asyncore.dispatcher):
|
||||
def __init__(self, socket_path, private_key, certificate, lifetime, basic_constraints, key_usage, extended_key_usage):
|
||||
def __init__(self, socket_path, private_key, certificate, lifetime, basic_constraints, key_usage, extended_key_usage, revocation_list_lifetime):
|
||||
asyncore.dispatcher.__init__(self)
|
||||
|
||||
# Bind to sockets
|
||||
@ -183,6 +186,7 @@ class SignServer(asyncore.dispatcher):
|
||||
self.private_key = crypto.load_privatekey(crypto.FILETYPE_PEM, open(private_key).read())
|
||||
self.certificate = crypto.load_certificate(crypto.FILETYPE_PEM, open(certificate).read())
|
||||
self.lifetime = lifetime
|
||||
self.revocation_list_lifetime = revocation_list_lifetime
|
||||
self.basic_constraints = basic_constraints
|
||||
self.key_usage = key_usage
|
||||
self.extended_key_usage = extended_key_usage
|
||||
|
121
certidude/static/css/style.css
Normal file
121
certidude/static/css/style.css
Normal file
@ -0,0 +1,121 @@
|
||||
svg {
|
||||
position: relative;
|
||||
top: 0.5em;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 1em 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button, .button {
|
||||
color: #000;
|
||||
float: right;
|
||||
border: 1pt solid #ccc;
|
||||
background-color: #eee;
|
||||
border-radius: 6px;
|
||||
margin: 2px;
|
||||
padding: 4px 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button:disabled, .button:disabled {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: 'Ubuntu Mono', courier, monospace;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: block;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #44c;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #aaf;
|
||||
}
|
||||
|
||||
html,body {
|
||||
margin: 0;
|
||||
padding: 0 0 1em 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #222;
|
||||
background-image: url('//fc00.deviantart.net/fs71/i/2013/078/9/6/free_hexa_pattern_cc0_by_black_light_studio-d4ig12f.png');
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.comment {
|
||||
color: #aaf;
|
||||
}
|
||||
|
||||
table th, table td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
h1, h2, th {
|
||||
font-family: 'Gentium';
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 22pt;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18pt;
|
||||
}
|
||||
|
||||
h2 svg {
|
||||
position: relative;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
p, td, footer, li, button {
|
||||
font-family: 'PT Sans Narrow';
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
border: 1px solid #000;
|
||||
background: #444;
|
||||
color: #fff;
|
||||
font-size: 12pt;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
margin: 0 0;
|
||||
}
|
||||
|
||||
#container {
|
||||
max-width: 60em;
|
||||
margin: 1em auto;
|
||||
background: #fff;
|
||||
padding: 1em;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-color: #aaa;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 4px 0;
|
||||
padding: 4px 0;
|
||||
clear: both;
|
||||
border-top: 1px dashed #ccc;
|
||||
}
|
4
certidude/static/js/jquery-2.1.4.min.js
vendored
Normal file
4
certidude/static/js/jquery-2.1.4.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,162 +1,55 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
|
||||
<link href='http://fonts.googleapis.com/css?family=Ubuntu+Mono' rel='stylesheet' type='text/css'>
|
||||
<link href='http://fonts.googleapis.com/css?family=Gentium' rel='stylesheet' type='text/css'>
|
||||
<link href="//fonts.googleapis.com/css?family=PT+Sans+Narrow" rel="stylesheet" type="text/css">
|
||||
<meta charset="utf-8"/>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<title>Certidude server</title>
|
||||
<style type="text/css">
|
||||
svg {
|
||||
position: relative;
|
||||
top: 0.5em;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 1em 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button, .button {
|
||||
color: #000;
|
||||
float: right;
|
||||
border: 1pt solid #ccc;
|
||||
background-color: #eee;
|
||||
border-radius: 6px;
|
||||
margin: 2px;
|
||||
padding: 4px 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button:disabled, .button:disabled {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: 'Ubuntu Mono', courier, monospace;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: block;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #44c;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #aaf;
|
||||
}
|
||||
|
||||
html,body {
|
||||
margin: 0;
|
||||
padding: 0 0 1em 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #222;
|
||||
background-image: url('http://fc00.deviantart.net/fs71/i/2013/078/9/6/free_hexa_pattern_cc0_by_black_light_studio-d4ig12f.png');
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.comment {
|
||||
color: #aaf;
|
||||
}
|
||||
|
||||
table th, table td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
h1, h2, th {
|
||||
font-family: 'Gentium';
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 22pt;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18pt;
|
||||
}
|
||||
|
||||
h2 svg {
|
||||
position: relative;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
p, td, footer, li, button {
|
||||
font-family: 'PT Sans Narrow';
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
border: 1px solid #000;
|
||||
background: #444;
|
||||
color: #fff;
|
||||
font-size: 12pt;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
margin: 0 0;
|
||||
}
|
||||
|
||||
#container {
|
||||
max-width: 60em;
|
||||
margin: 1em auto;
|
||||
background: #fff;
|
||||
padding: 1em;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-color: #aaa;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 4px 0;
|
||||
padding: 4px 0;
|
||||
clear: both;
|
||||
border-top: 1px dashed #ccc;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
<link href="/css/style.css" rel="stylesheet" type="text/css"/>
|
||||
<link href="//fonts.googleapis.com/css?family=Ubuntu+Mono" rel="stylesheet" type="text/css"/>
|
||||
<link href="//fonts.googleapis.com/css?family=Gentium" rel="stylesheet" type="text/css"/>
|
||||
<link href="//fonts.googleapis.com/css?family=PT+Sans+Narrow" rel="stylesheet" type="text/css"/>
|
||||
<script type="text/javascript" src="/js/jquery-2.1.4.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
|
||||
<h1>Submit signing request</h1>
|
||||
|
||||
<p>Request submission is allowed from: {% for i in authority.request_whitelist %}{{ i }} {% endfor %}</p>
|
||||
<p>Autosign is allowed from: {% for i in authority.autosign_whitelist %}{{ i }} {% endfor %}</p>
|
||||
|
||||
<h2>IPsec gateway on OpenWrt</h2>
|
||||
|
||||
{% set s = authority.certificate.subject %}
|
||||
|
||||
<!--
|
||||
<p>To submit new certificate signing request first set common name, eg:</p>
|
||||
<pre>
|
||||
export CN=$(hostname)
|
||||
opkg update
|
||||
opkg install strongswan-default curl openssl-util
|
||||
modprobe authenc
|
||||
</pre>
|
||||
|
||||
<p>Generate key and submit using standard shell tools:</p>
|
||||
|
||||
<pre>
|
||||
curl {{request.url}}/certificate/ > ca.crt
|
||||
openssl genrsa -out $CN.key 4096
|
||||
openssl req -new -sha256 -key $CN.key -out $CN.csr -subj "{% if s.C %}/C={{s.C}}{% endif %}{% if s.ST %}/ST={{s.ST}}{% endif %}{% if s.L %}/L={{s.L}}{% endif %}{% if s.O %}/O={{s.O}}{% endif %}{% if s.OU %}/OU={{s.OU}}{% endif %}/CN=$CN"
|
||||
wget --header "Content-Type: application/pkcs10" --post-data="$(cat $CN.csr)" http://localhost:9090/api/buujaa/request/?autosign=1\&wait=30 -O $CN.crt
|
||||
openssl verify -CAfile ca.crt $CN.crt
|
||||
CN=$(cat /proc/sys/kernel/hostname)
|
||||
curl {{request.url}}/certificate/ > /etc/ipsec.d/cacerts/ca.pem
|
||||
openssl genrsa -out /etc/ipsec.d/private/$CN.pem 4096
|
||||
chmod 0600 /etc/ipsec.d/private/$CN.pem
|
||||
openssl req -new -sha256 -key /etc/ipsec.d/private/$CN.pem -out /etc/ipsec.d/reqs/$CN.pem -subj "{% if s.C %}/C={{s.C}}{% endif %}{% if s.ST %}/ST={{s.ST}}{% endif %}{% if s.L %}/L={{s.L}}{% endif %}{% if s.O %}/O={{s.O}}{% endif %}{% if s.OU %}/OU={{s.OU}}{% endif %}/CN=$CN"
|
||||
curl -L -H "Content-Type: application/pkcs10" --data-binary @/etc/ipsec.d/reqs/$CN.pem {{request.uri}}/request/?autosign=1\&wait=30 > /etc/ipsec.d/certs/$CN.pem.part
|
||||
if [ $? -eq 0 ]; then mv /etc/ipsec.d/certs/$CN.pem.part /etc/ipsec.d/certs/$CN.pem; fi
|
||||
openssl verify -CAfile /etc/ipsec.d/cacerts/ca.pem /etc/ipsec.d/certs/$CN.pem
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
Inspect newly created files:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
openssl x509 -text -noout -in /etc/ipsec.d/cacerts/ca.pem
|
||||
openssl x509 -text -noout -in /etc/ipsec.d/certs/$CN.pem
|
||||
openssl rsa -check -in /etc/ipsec.d/private/$CN.pem
|
||||
</pre>
|
||||
-->
|
||||
|
||||
<p>Assuming you have Certidude installed</p>
|
||||
|
||||
|
51
certidude/templates/nginx.conf
Normal file
51
certidude/templates/nginx.conf
Normal file
@ -0,0 +1,51 @@
|
||||
user www-data;
|
||||
worker_processes 4;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
{% if not push_server %}
|
||||
push_stream_shared_memory_size 32M;
|
||||
{% endif %}
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
gzip on;
|
||||
|
||||
upstream certidude_api {
|
||||
server unix:///run/uwsgi/app/certidude/socket;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name {{hostname}};
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server ipv6only=on;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
root {{static_path}};
|
||||
|
||||
location /api/ {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass certidude_api;
|
||||
}
|
||||
|
||||
{% if not push_server %}
|
||||
location ~ /publish/(.*) {
|
||||
allow 127.0.0.1;
|
||||
push_stream_publisher admin;
|
||||
push_stream_channels_path $1;
|
||||
}
|
||||
|
||||
location ~ /subscribe/(.*) {
|
||||
push_stream_channels_path $1;
|
||||
push_stream_subscriber long-polling;
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
[CA_{{slug}}]
|
||||
default_days = 1825
|
||||
default_crl_days = {{revocation_list_lifetime}}
|
||||
default_days = {{certificate_lifetime}}
|
||||
dir = {{directory}}
|
||||
private_key = $dir/ca_key.pem
|
||||
certificate = $dir/ca_crt.pem
|
||||
@ -9,12 +10,15 @@ certs = $dir/signed/
|
||||
crl = $dir/ca_crl.pem
|
||||
serial = $dir/serial
|
||||
{% if crl_distribution_points %}
|
||||
crlDistributionPoints = {{crl_distribution_points}}{% endif %}
|
||||
crlDistributionPoints = {{crl_distribution_points}}
|
||||
{% endif %}
|
||||
{% if email_address %}
|
||||
emailAddress = {{email_address}}{% endif %}
|
||||
emailAddress = {{email_address}}
|
||||
{% endif %}
|
||||
x509_extensions = {{slug}}_cert
|
||||
policy = poliy_{{slug}}
|
||||
autosign_whitelist = 127.
|
||||
request_whitelist =
|
||||
autosign_whitelist = 127.0.0.0/8
|
||||
inbox = {{inbox}}
|
||||
outbox = {{outbox}}
|
||||
|
||||
|
@ -14,4 +14,6 @@ group nogroup
|
||||
ifconfig-pool-persist /tmp/openvpn-leases.txt
|
||||
ifconfig {{subnet_first}} {{subnet.netmask}}
|
||||
server-bridge {{subnet_first}} {{subnet.netmask}} {{subnet_second}} {{subnet_last}}
|
||||
|
||||
{% for subnet in route %}
|
||||
push "route {{subnet}}"
|
||||
{% endfor %}
|
27
certidude/templates/strongswan-client-to-site.conf
Normal file
27
certidude/templates/strongswan-client-to-site.conf
Normal file
@ -0,0 +1,27 @@
|
||||
# /etc/ipsec.conf - strongSwan IPsec configuration file
|
||||
|
||||
# left/local = client
|
||||
# right/remote = gateway
|
||||
|
||||
config setup
|
||||
|
||||
conn %default
|
||||
ikelifetime=60m
|
||||
keylife=20m
|
||||
rekeymargin=3m
|
||||
keyingtries=1
|
||||
keyexchange=ikev2
|
||||
dpdaction={{dpdaction}}
|
||||
|
||||
conn home
|
||||
auto={{auto}}
|
||||
type=tunnel
|
||||
left=%defaultroute # Use IP of default route for listening
|
||||
leftcert={{certificate_path}} # Client certificate
|
||||
leftid={{common_name}} # Client certificate identifier
|
||||
leftfirewall=yes
|
||||
right={{remote}} # Gateway IP address
|
||||
rightid=%any # Allow any common name
|
||||
rightsubnet=0.0.0.0/0 # Accept all subnets suggested by server
|
||||
#rightcert=server.pem
|
||||
|
28
certidude/templates/strongswan-site-to-client.conf
Normal file
28
certidude/templates/strongswan-site-to-client.conf
Normal file
@ -0,0 +1,28 @@
|
||||
# /etc/ipsec.conf - strongSwan IPsec configuration file
|
||||
|
||||
# left/local = gateway
|
||||
# right/remote = client
|
||||
|
||||
config setup
|
||||
|
||||
conn %default
|
||||
ikelifetime=60m
|
||||
keylife=20m
|
||||
rekeymargin=3m
|
||||
keyingtries=1
|
||||
keyexchange=ikev2
|
||||
|
||||
conn rw
|
||||
auto=add
|
||||
right=%any # Allow connecting from any IP address
|
||||
left={{local}} # Gateway IP address
|
||||
leftcert={{certificate_path}} # Gateway certificate
|
||||
leftfirewall=yes
|
||||
{% if route %}
|
||||
{% if route | length == 1 %}
|
||||
leftsubnet={{route[0]}} # Advertise routes via this connection
|
||||
{% else %}
|
||||
leftsubnet={ {{ route | join(', ') }} }
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
23
certidude/templates/uwsgi.ini
Normal file
23
certidude/templates/uwsgi.ini
Normal file
@ -0,0 +1,23 @@
|
||||
[uwsgi]
|
||||
exec-as-root = /usr/local/bin/certidude spawn
|
||||
master = true
|
||||
processes = 1
|
||||
vacuum = true
|
||||
uid = {{username}}
|
||||
gid = {{username}}
|
||||
plugins = python34
|
||||
chdir = /tmp
|
||||
module = certidude.wsgi
|
||||
callable = app
|
||||
chmod-socket = 660
|
||||
chown-socket = {{username}}:www-data
|
||||
{% if push_server %}
|
||||
env = CERTIDUDE_EVENT_PUBLISH={{push_server}}/publish/%(channel)s
|
||||
env = CERTIDUDE_EVENT_SUBSCRIBE={{push_server}}/subscribe/%(channel)s
|
||||
{% else %}
|
||||
env = CERTIDUDE_EVENT_PUBLISH=http://localhost/event/publish/%(channel)s
|
||||
env = CERTIDUDE_EVENT_SUBSCRIBE=http://localhost/event/subscribe/%(channel)s
|
||||
{% endif %}
|
||||
env = LANG=C.UTF-8
|
||||
env = LC_ALL=C.UTF-8
|
||||
|
@ -7,6 +7,7 @@ import click
|
||||
import socket
|
||||
import io
|
||||
import urllib.request
|
||||
import ipaddress
|
||||
from configparser import RawConfigParser
|
||||
from Crypto.Util import asn1
|
||||
from OpenSSL import crypto
|
||||
@ -78,7 +79,7 @@ class CertificateAuthorityConfig(object):
|
||||
section = "CA_" + slug
|
||||
|
||||
dirs = dict([(key, self.get(section, key))
|
||||
for key in ("dir", "certificate", "crl", "certs", "new_certs_dir", "private_key", "revoked_certs_dir", "autosign_whitelist")])
|
||||
for key in ("dir", "certificate", "crl", "certs", "new_certs_dir", "private_key", "revoked_certs_dir", "request_whitelist", "autosign_whitelist")])
|
||||
|
||||
# Variable expansion, eg $dir
|
||||
for key, value in dirs.items():
|
||||
@ -89,7 +90,8 @@ class CertificateAuthorityConfig(object):
|
||||
dirs["email_address"] = self.get(section, "emailAddress")
|
||||
dirs["inbox"] = self.get(section, "inbox")
|
||||
dirs["outbox"] = self.get(section, "outbox")
|
||||
dirs["lifetime"] = int(self.get(section, "default_days", "1825"))
|
||||
dirs["certificate_lifetime"] = int(self.get(section, "default_days", "1825"))
|
||||
dirs["revocation_list_lifetime"] = int(self.get(section, "default_crl_days", "1"))
|
||||
|
||||
extensions_section = self.get(section, "x509_extensions")
|
||||
if extensions_section:
|
||||
@ -296,7 +298,7 @@ class Request(CertificateBase):
|
||||
else:
|
||||
raise ValueError("Can't parse %s as X.509 certificate signing request!" % mixed)
|
||||
|
||||
assert not self.buf or self.buf == self.dump(), "%s is not %s" % (self.buf, self.dump())
|
||||
assert not self.buf or self.buf == self.dump(), "%s is not %s" % (repr(self.buf), repr(self.dump()))
|
||||
|
||||
@property
|
||||
def signable(self):
|
||||
@ -390,29 +392,25 @@ class Certificate(CertificateBase):
|
||||
|
||||
class CertificateAuthority(object):
|
||||
|
||||
def __init__(self, slug, certificate, crl, certs, new_certs_dir, revoked_certs_dir=None, private_key=None, autosign=False, autosign_whitelist=None, email_address=None, inbox=None, outbox=None, basic_constraints="CA:FALSE", key_usage="digitalSignature,keyEncipherment", extended_key_usage="clientAuth", lifetime=5*365):
|
||||
def __init__(self, slug, certificate, crl, certs, new_certs_dir, revoked_certs_dir=None, private_key=None, autosign=False, autosign_whitelist=None, request_whitelist=None, email_address=None, inbox=None, outbox=None, basic_constraints="CA:FALSE", key_usage="digitalSignature,keyEncipherment", extended_key_usage="clientAuth", certificate_lifetime=5*365, revocation_list_lifetime=1):
|
||||
self.slug = slug
|
||||
self.revocation_list = crl
|
||||
self.signed_dir = certs
|
||||
self.request_dir = new_certs_dir
|
||||
self.revoked_dir = revoked_certs_dir
|
||||
self.private_key = private_key
|
||||
self.autosign_whitelist = set([j for j in autosign_whitelist.split(" ") if j])
|
||||
|
||||
self.autosign_whitelist = set([ipaddress.ip_network(j) for j in autosign_whitelist.split(" ") if j])
|
||||
self.request_whitelist = set([ipaddress.ip_network(j) for j in request_whitelist.split(" ") if j]).union(self.autosign_whitelist)
|
||||
|
||||
self.certificate = Certificate(open(certificate))
|
||||
self.mailer = Mailer(outbox) if outbox else None
|
||||
self.lifetime = lifetime
|
||||
self.certificate_lifetime = certificate_lifetime
|
||||
self.revocation_list_lifetime = revocation_list_lifetime
|
||||
self.basic_constraints = basic_constraints
|
||||
self.key_usage = key_usage
|
||||
self.extended_key_usage = extended_key_usage
|
||||
|
||||
def autosign_allowed(self, addr):
|
||||
for j in self.autosign_whitelist:
|
||||
if j.endswith(".") and addr.startswith(j):
|
||||
return True
|
||||
elif j == addr:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _signer_exec(self, cmd, *bits):
|
||||
sock = self.connect_signer()
|
||||
sock.send(cmd.encode("ascii"))
|
||||
@ -540,7 +538,7 @@ class CertificateAuthority(object):
|
||||
self.certificate._obj,
|
||||
request._obj,
|
||||
self.basic_constraints,
|
||||
lifetime=lifetime or self.lifetime)
|
||||
lifetime=lifetime or self.certificate_lifetime)
|
||||
|
||||
path = os.path.join(self.signed_dir, request.common_name + ".pem")
|
||||
if os.path.exists(path):
|
||||
|
@ -13,8 +13,8 @@ from certidude.api import CertificateAuthorityResource, \
|
||||
|
||||
config = CertificateAuthorityConfig("/etc/ssl/openssl.cnf")
|
||||
|
||||
assert os.getenv("CERTIDUDE_EVENT_SUBSCRIBE"), "Please set CERTIDUDE_EVENT_SUBSCRIBE to your web server's subscribe URL"
|
||||
assert os.getenv("CERTIDUDE_EVENT_PUBLISH"), "Please set CERTIDUDE_EVENT_SUBSCRIBE to your web server's subscribe URL"
|
||||
assert os.getenv("CERTIDUDE_EVENT_SUBSCRIBE"), "Please set CERTIDUDE_EVENT_SUBSCRIBE to your web server's subscription URL"
|
||||
assert os.getenv("CERTIDUDE_EVENT_PUBLISH"), "Please set CERTIDUDE_EVENT_PUBLISH to your web server's publishing URL"
|
||||
|
||||
app = falcon.API()
|
||||
app.add_route("/api/{ca}/ocsp/", CertificateStatusResource(config))
|
||||
|
4
setup.py
4
setup.py
@ -1,11 +1,11 @@
|
||||
#!/usr/bin/python
|
||||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
import os
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name = "certidude",
|
||||
version = "0.1.7",
|
||||
version = "0.1.17",
|
||||
author = u"Lauri Võsandi",
|
||||
author_email = "lauri.vosandi@gmail.com",
|
||||
description = "Certidude is a novel X.509 Certificate Authority management tool aiming to support PKCS#11 and in far future WebCrypto.",
|
||||
|
Loading…
Reference in New Issue
Block a user