mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 16:25:17 +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/*.svg
|
||||||
include certidude/templates/*.ovpn
|
include certidude/templates/*.ovpn
|
||||||
include certidude/templates/*.cnf
|
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.
|
* Standard request, sign, revoke workflow via web interface.
|
||||||
* Colored command-line interface, check out ``certidude list``
|
* Colored command-line interface, check out ``certidude list``.
|
||||||
* OpenVPN integration, check out ``certidude setup openvpn server`` and ``certidude setup openvpn client``
|
* 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
|
* Privilege isolation, separate signer process is spawned per private key isolating
|
||||||
private key use from the the web interface.
|
private key use from the the web interface.
|
||||||
* Certificate numbering obfuscation, certificate serial numbers are intentionally
|
* Certificate numbering obfuscation, certificate serial numbers are intentionally
|
||||||
randomized to avoid leaking information about business practices.
|
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
|
TODO
|
||||||
@ -27,7 +28,6 @@ TODO
|
|||||||
|
|
||||||
* Refactor mailing subsystem and server-side events to use hooks.
|
* Refactor mailing subsystem and server-side events to use hooks.
|
||||||
* Notifications via e-mail.
|
* Notifications via e-mail.
|
||||||
* strongSwan setup integration.
|
|
||||||
* OCSP support.
|
* OCSP support.
|
||||||
* Deep mailbox integration, eg fetch CSR-s from mailbox via IMAP.
|
* 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>`_.
|
* WebCrypto support, meanwhile check out `hwcrypto.js <https://github.com/open-eid/hwcrypto.js>`_.
|
||||||
@ -42,14 +42,14 @@ To install Certidude:
|
|||||||
|
|
||||||
.. code:: bash
|
.. 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
|
pip3 install certidude
|
||||||
|
|
||||||
Create a user for ``certidude``:
|
Create a system user for ``certidude``:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
useradd certidude
|
adduser --system --no-create-home --group certidude
|
||||||
|
|
||||||
|
|
||||||
Setting up CA
|
Setting up CA
|
||||||
@ -64,6 +64,12 @@ Certidude can set up CA relatively easily:
|
|||||||
Tweak command-line options until you meet your requirements and
|
Tweak command-line options until you meet your requirements and
|
||||||
then insert generated section to your /etc/ssl/openssl.cnf
|
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:
|
Finally serve the certificate authority via web:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
@ -102,7 +108,13 @@ Install uWSGI:
|
|||||||
|
|
||||||
apt-get install uwsgi uwsgi-plugin-python3
|
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
|
.. code:: ini
|
||||||
|
|
||||||
|
@ -191,6 +191,16 @@ class RequestListResource(CertificateAuthorityBase):
|
|||||||
"""
|
"""
|
||||||
Submit certificate signing request (CSR) in PEM format
|
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":
|
if req.get_header("Content-Type") != "application/pkcs10":
|
||||||
raise falcon.HTTPUnsupportedMediaType(
|
raise falcon.HTTPUnsupportedMediaType(
|
||||||
@ -207,20 +217,23 @@ class RequestListResource(CertificateAuthorityBase):
|
|||||||
else:
|
else:
|
||||||
cert = Certificate(cert_buf)
|
cert = Certificate(cert_buf)
|
||||||
if cert.pubkey == csr.pubkey:
|
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)
|
resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", csr.common_name)
|
||||||
return
|
return
|
||||||
|
|
||||||
# TODO: check for revoked certificates and return HTTP 410 Gone
|
# TODO: check for revoked certificates and return HTTP 410 Gone
|
||||||
|
|
||||||
# Process automatic signing if the IP address is whitelisted and autosigning was requested
|
# 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"):
|
if req.get_param("autosign").lower() in ("yes", "1", "true"):
|
||||||
try:
|
for subnet in ca.autosign_whitelist:
|
||||||
resp.append_header("Content-Type", "application/x-x509-user-cert")
|
if subnet.overlaps(remote_addr):
|
||||||
resp.body = ca.sign(req).dump()
|
try:
|
||||||
return
|
resp.append_header("Content-Type", "application/x-x509-user-cert")
|
||||||
except FileExistsError: # Certificate already exists, try to save the request
|
resp.body = ca.sign(req).dump()
|
||||||
pass
|
return
|
||||||
|
except FileExistsError: # Certificate already exists, try to save the request
|
||||||
|
pass
|
||||||
|
break
|
||||||
|
|
||||||
# Attempt to save the request otherwise
|
# Attempt to save the request otherwise
|
||||||
try:
|
try:
|
||||||
@ -237,7 +250,7 @@ class RequestListResource(CertificateAuthorityBase):
|
|||||||
# Redirect to nginx pub/sub
|
# Redirect to nginx pub/sub
|
||||||
url = url_template % dict(channel=request.fingerprint())
|
url = url_template % dict(channel=request.fingerprint())
|
||||||
click.echo("Redirecting to: %s" % url)
|
click.echo("Redirecting to: %s" % url)
|
||||||
resp.status = falcon.HTTP_FOUND
|
resp.status = falcon.HTTP_SEE_OTHER
|
||||||
resp.append_header("Location", url)
|
resp.append_header("Location", url)
|
||||||
else:
|
else:
|
||||||
click.echo("Using dummy streaming mode, please switch to nginx in production!", err=True)
|
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
|
# coding: utf-8
|
||||||
|
|
||||||
import sys
|
import asyncore
|
||||||
|
import click
|
||||||
|
import falcon
|
||||||
|
import logging
|
||||||
|
import mimetypes
|
||||||
|
import netifaces
|
||||||
|
import os
|
||||||
import pwd
|
import pwd
|
||||||
import random
|
import random
|
||||||
import socket
|
|
||||||
import click
|
|
||||||
import os
|
|
||||||
import asyncore
|
|
||||||
import time
|
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import logging
|
|
||||||
import signal
|
import signal
|
||||||
import netifaces
|
import socket
|
||||||
import urllib.request
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from humanize import naturaltime
|
import sys
|
||||||
from ipaddress import ip_network
|
import time
|
||||||
from time import sleep
|
from certidude.helpers import expand_paths, \
|
||||||
from datetime import datetime
|
certidude_request_certificate
|
||||||
from OpenSSL import crypto
|
|
||||||
from setproctitle import setproctitle
|
|
||||||
from certidude.signer import SignServer
|
from certidude.signer import SignServer
|
||||||
from jinja2 import Environment, PackageLoader
|
|
||||||
from certidude.wrappers import CertificateAuthorityConfig, \
|
from certidude.wrappers import CertificateAuthorityConfig, \
|
||||||
CertificateAuthority, Certificate, subject2dn, Request
|
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:
|
# Big fat warning:
|
||||||
# m2crypto overflows around 2030 because on 32-bit systems
|
# 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
|
# http://www.mad-hacking.net/documentation/linux/security/ssl-tls/creating-ca.xml
|
||||||
# https://kjur.github.io/jsrsasign/
|
# https://kjur.github.io/jsrsasign/
|
||||||
# keyUsage, extendedKeyUsage - https://www.openssl.org/docs/apps/x509v3_config.html
|
# 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")
|
config = CertificateAuthorityConfig("/etc/ssl/openssl.cnf")
|
||||||
|
|
||||||
# Parse command-line argument defaults from environment
|
# Parse command-line argument defaults from environment
|
||||||
HOSTNAME = socket.gethostname()
|
HOSTNAME = socket.gethostname()
|
||||||
USERNAME = os.environ.get("USER")
|
USERNAME = os.environ.get("USER")
|
||||||
EMAIL = USERNAME + "@" + HOSTNAME
|
|
||||||
NOW = datetime.utcnow().replace(tzinfo=None)
|
NOW = datetime.utcnow().replace(tzinfo=None)
|
||||||
|
|
||||||
FIRST_NAME = None
|
FIRST_NAME = None
|
||||||
SURNAME = None
|
SURNAME = None
|
||||||
|
EMAIL = None
|
||||||
|
|
||||||
|
if USERNAME:
|
||||||
|
EMAIL = USERNAME + "@" + HOSTNAME
|
||||||
|
|
||||||
if os.getuid() >= 1000:
|
if os.getuid() >= 1000:
|
||||||
_, _, _, _, gecos, _, _ = pwd.getpwnam(USERNAME)
|
_, _, _, _, gecos, _, _ = pwd.getpwnam(USERNAME)
|
||||||
@ -61,33 +66,18 @@ if os.getuid() >= 1000:
|
|||||||
else:
|
else:
|
||||||
FIRST_NAME = gecos
|
FIRST_NAME = gecos
|
||||||
|
|
||||||
def first_nic_address():
|
DEFAULT_ROUTE, PRIMARY_INTERFACE = netifaces.gateways().get("default").get(2)
|
||||||
"""
|
PRIMARY_ALIASES = netifaces.ifaddresses(PRIMARY_INTERFACE).get(2)
|
||||||
Return IP address of the first network interface
|
PRIMARY_ADDRESS = PRIMARY_ALIASES[0].get("addr")
|
||||||
"""
|
|
||||||
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")
|
|
||||||
|
|
||||||
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
|
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
|
# Process directories
|
||||||
run_dir = "/run/certidude"
|
run_dir = "/run/certidude"
|
||||||
signer_dir = os.path.join(run_dir, "signer")
|
signer_dir = os.path.join(run_dir, "signer")
|
||||||
@ -98,6 +88,14 @@ def spawn_signers(kill, no_interaction):
|
|||||||
click.echo("Creating: %s" % signer_dir)
|
click.echo("Creating: %s" % signer_dir)
|
||||||
os.makedirs(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
|
# Prepare chroot directories
|
||||||
if not os.path.exists(os.path.join(chroot_dir, "dev")):
|
if not os.path.exists(os.path.join(chroot_dir, "dev")):
|
||||||
os.makedirs(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"))
|
os.system("mknod -m 444 %s c 1 9" % os.path.join(chroot_dir, "dev", "urandom"))
|
||||||
|
|
||||||
for ca in config.all_authorities():
|
for ca in config.all_authorities():
|
||||||
|
socket_path = os.path.join(signer_dir, ca.slug + ".sock")
|
||||||
pidfile = "/run/certidude/signer/%s.pid" % ca.slug
|
pidfile_path = os.path.join(signer_dir, ca.slug + ".pid")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(pidfile) as fh:
|
with open(pidfile_path) as fh:
|
||||||
pid = int(fh.readline())
|
pid = int(fh.readline())
|
||||||
os.kill(pid, 0)
|
os.kill(pid, 0)
|
||||||
click.echo("Found process with PID %d for %s" % (pid, ca.slug))
|
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()
|
child_pid = os.fork()
|
||||||
|
|
||||||
if child_pid == 0:
|
if child_pid == 0:
|
||||||
with open(pidfile, "w") as fh:
|
with open(pidfile_path, "w") as fh:
|
||||||
fh.write("%d\n" % os.getpid())
|
fh.write("%d\n" % os.getpid())
|
||||||
|
|
||||||
setproctitle("%s spawn %s" % (sys.argv[0], ca.slug))
|
setproctitle("%s spawn %s" % (sys.argv[0], ca.slug))
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
filename="/var/log/certidude-%s.log" % ca.slug,
|
filename="/var/log/certidude-%s.log" % ca.slug,
|
||||||
level=logging.INFO)
|
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,
|
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()
|
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:
|
else:
|
||||||
raise FileNotFoundError("CA certificate not found and no URL specified")
|
click.echo("Spawned certidude signer process with PID %d at %s" % (child_pid, socket_path))
|
||||||
|
|
||||||
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.command("client", help="Setup X.509 certificates for application")
|
@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("--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("--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("--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("--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('--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",
|
@click.option("--config", "-o",
|
||||||
default="/etc/openvpn/site-to-client.conf",
|
default="/etc/openvpn/site-to-client.conf",
|
||||||
type=click.File(mode="w", atomic=True, lazy=True),
|
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("--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("--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")
|
@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
|
# TODO: Intelligent way of getting last IP address in the subnet
|
||||||
subnet_first = None
|
subnet_first = None
|
||||||
subnet_last = None
|
subnet_last = None
|
||||||
@ -339,16 +200,6 @@ def certidude_setup_openvpn_server(url, config, subnet, email_address, common_na
|
|||||||
subnet_second = addr
|
subnet_second = addr
|
||||||
subnet_last = 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):
|
if not os.path.exists(certificate_path):
|
||||||
click.echo("As OpenVPN server certificate needs specific key usage extensions please")
|
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:")
|
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,
|
org_unit,
|
||||||
email_address,
|
email_address,
|
||||||
key_usage="nonRepudiation,digitalSignature,keyEncipherment",
|
key_usage="nonRepudiation,digitalSignature,keyEncipherment",
|
||||||
extended_key_usage="serverAuth",
|
extended_key_usage="serverAuth,ikeIntermediate",
|
||||||
wait=True)
|
wait=True)
|
||||||
|
|
||||||
if not os.path.exists(dhparam_path):
|
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
|
return retval
|
||||||
|
|
||||||
# TODO: Add dhparam
|
# 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("Generated %s" % config.name)
|
||||||
click.echo()
|
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("--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("--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")
|
@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):
|
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(
|
retval = certidude_request_certificate(
|
||||||
url,
|
url,
|
||||||
key_path,
|
key_path,
|
||||||
@ -428,7 +271,7 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
|
|||||||
return retval
|
return retval
|
||||||
|
|
||||||
# TODO: Add dhparam
|
# 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("Generated %s" % config.name)
|
||||||
click.echo()
|
click.echo()
|
||||||
@ -438,6 +281,164 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
|
|||||||
click.echo()
|
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.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("--group", "-g", default="certidude", help="Group for file permissions, certidude by default")
|
||||||
@click.option("--parent", "-p", help="Parent CA, none 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("--country", "-c", default="ee", help="Country, Estonia by default")
|
||||||
@click.option("--state", "-s", default="Harjumaa", help="State or country, Harjumaa 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("--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("--organization", "-o", default="Example LLC", help="Company or organization name")
|
||||||
@click.option("--organizational-unit", "-ou", default="Certification Department")
|
@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("--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("--crl-distribution-url", default=None, help="CRL distribution URL")
|
||||||
@click.option("--ocsp-responder-url", default=None, help="OCSP responder 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("--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.option("--outbox", default="smtp://localhost", help="Outbound e-mail server")
|
||||||
@click.argument("directory")
|
@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)
|
logging.info("Creating certificate authority in %s", directory)
|
||||||
_, _, uid, gid, gecos, root, shell = pwd.getpwnam(group)
|
_, _, uid, gid, gecos, root, shell = pwd.getpwnam(group)
|
||||||
os.setgid(gid)
|
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
|
crl_distribution_points = "URI:%s" % crl_distribution_url
|
||||||
|
|
||||||
ca = crypto.X509()
|
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.set_serial_number(1)
|
||||||
ca.get_subject().CN = common_name
|
ca.get_subject().CN = common_name
|
||||||
ca.get_subject().C = country
|
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().O = organization
|
||||||
ca.get_subject().OU = organizational_unit
|
ca.get_subject().OU = organizational_unit
|
||||||
ca.gmtime_adj_notBefore(0)
|
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_issuer(ca.get_subject())
|
||||||
ca.set_pubkey(key)
|
ca.set_pubkey(key)
|
||||||
ca.add_extensions([
|
ca.add_extensions([
|
||||||
@ -522,7 +524,10 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
|
|||||||
subject_alt_name.encode("ascii"))
|
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)
|
ocsp_responder_url = "http://%s/api/%s/ocsp/" % (common_name, slug)
|
||||||
authority_info_access = "OCSP;URI:%s" % ocsp_responder_url
|
authority_info_access = "OCSP;URI:%s" % ocsp_responder_url
|
||||||
ca.add_extensions([
|
ca.add_extensions([
|
||||||
@ -531,6 +536,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
|
|||||||
False,
|
False,
|
||||||
authority_info_access.encode("ascii"))
|
authority_info_access.encode("ascii"))
|
||||||
])
|
])
|
||||||
|
"""
|
||||||
|
|
||||||
click.echo("Signing %s..." % subject2dn(ca.get_subject()))
|
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))
|
os.mkdir(os.path.join(directory, subdir))
|
||||||
with open(ca_crl, "wb") as fh:
|
with open(ca_crl, "wb") as fh:
|
||||||
crl = crypto.CRL()
|
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:
|
with open(os.path.join(directory, "serial"), "w") as fh:
|
||||||
fh.write("1")
|
fh.write("1")
|
||||||
|
|
||||||
@ -730,12 +736,35 @@ def certidude_sign(common_name, overwrite, lifetime):
|
|||||||
else:
|
else:
|
||||||
# Sign directly using private key
|
# Sign directly using private key
|
||||||
cert = ca.sign2(request, overwrite, True, lifetime)
|
cert = ca.sign2(request, overwrite, True, lifetime)
|
||||||
os.unlink(request.path)
|
|
||||||
click.echo("Signed %s" % cert.distinguished_name)
|
click.echo("Signed %s" % cert.distinguished_name)
|
||||||
for key, value, data in cert.extensions:
|
for key, value, data in cert.extensions:
|
||||||
click.echo("Added extension %s: %s" % (key, value))
|
click.echo("Added extension %s: %s" % (key, value))
|
||||||
click.echo()
|
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.command("serve", help="Run built-in HTTP server")
|
||||||
@click.option("-u", "--user", default="certidude", help="Run as user")
|
@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("-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")
|
@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):
|
def certidude_serve(user, port, listen, enable_signature):
|
||||||
spawn_signers(kill=False, no_interaction=False)
|
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
filename='/var/log/certidude.log',
|
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/{cn}/", RequestDetailResource(config))
|
||||||
app.add_route("/api/{ca}/request/", RequestListResource(config))
|
app.add_route("/api/{ca}/request/", RequestListResource(config))
|
||||||
app.add_route("/api/{ca}/", IndexResource(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)
|
httpd = make_server(listen, port, app, ThreadingWSGIServer)
|
||||||
if user:
|
if user:
|
||||||
_, _, uid, gid, gecos, root, shell = pwd.getpwnam(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!")
|
click.echo("Warning: running as root, this is not reccommended!")
|
||||||
httpd.serve_forever()
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
@click.group("strongswan", help="strongSwan helpers")
|
||||||
|
def certidude_setup_strongswan(): pass
|
||||||
|
|
||||||
@click.group("openvpn", help="OpenVPN helpers")
|
@click.group("openvpn", help="OpenVPN helpers")
|
||||||
def certidude_setup_openvpn(): pass
|
def certidude_setup_openvpn(): pass
|
||||||
|
|
||||||
@ -798,11 +831,15 @@ def certidude_setup(): pass
|
|||||||
@click.group()
|
@click.group()
|
||||||
def entry_point(): pass
|
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_server)
|
||||||
certidude_setup_openvpn.add_command(certidude_setup_openvpn_client)
|
certidude_setup_openvpn.add_command(certidude_setup_openvpn_client)
|
||||||
certidude_setup.add_command(certidude_setup_authority)
|
certidude_setup.add_command(certidude_setup_authority)
|
||||||
certidude_setup.add_command(certidude_setup_openvpn)
|
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_client)
|
||||||
|
certidude_setup.add_command(certidude_setup_production)
|
||||||
entry_point.add_command(certidude_setup)
|
entry_point.add_command(certidude_setup)
|
||||||
entry_point.add_command(certidude_serve)
|
entry_point.add_command(certidude_serve)
|
||||||
entry_point.add_command(certidude_spawn)
|
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
|
Sign certificate signing request directly with private key assuming it's readable by the process
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Initialize X.509 certificate object
|
||||||
cert = crypto.X509()
|
cert = crypto.X509()
|
||||||
|
ca.set_version(2) # This corresponds to X.509v3
|
||||||
|
|
||||||
# Set public key
|
# Set public key
|
||||||
cert.set_pubkey(request.get_pubkey())
|
cert.set_pubkey(request.get_pubkey())
|
||||||
@ -130,7 +132,8 @@ class SignHandler(asynchat.async_chat):
|
|||||||
self.send(crl.export(
|
self.send(crl.export(
|
||||||
self.server.certificate,
|
self.server.certificate,
|
||||||
self.server.private_key,
|
self.server.private_key,
|
||||||
crypto.FILETYPE_PEM))
|
crypto.FILETYPE_PEM,
|
||||||
|
self.server.revocation_list_lifetime))
|
||||||
|
|
||||||
elif cmd == "ocsp-request":
|
elif cmd == "ocsp-request":
|
||||||
NotImplemented # TODO: Implement OCSP
|
NotImplemented # TODO: Implement OCSP
|
||||||
@ -168,7 +171,7 @@ class SignHandler(asynchat.async_chat):
|
|||||||
|
|
||||||
|
|
||||||
class SignServer(asyncore.dispatcher):
|
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)
|
asyncore.dispatcher.__init__(self)
|
||||||
|
|
||||||
# Bind to sockets
|
# 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.private_key = crypto.load_privatekey(crypto.FILETYPE_PEM, open(private_key).read())
|
||||||
self.certificate = crypto.load_certificate(crypto.FILETYPE_PEM, open(certificate).read())
|
self.certificate = crypto.load_certificate(crypto.FILETYPE_PEM, open(certificate).read())
|
||||||
self.lifetime = lifetime
|
self.lifetime = lifetime
|
||||||
|
self.revocation_list_lifetime = revocation_list_lifetime
|
||||||
self.basic_constraints = basic_constraints
|
self.basic_constraints = basic_constraints
|
||||||
self.key_usage = key_usage
|
self.key_usage = key_usage
|
||||||
self.extended_key_usage = extended_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>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<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 charset="utf-8"/>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||||
<title>Certidude server</title>
|
<title>Certidude server</title>
|
||||||
<style type="text/css">
|
<link href="/css/style.css" rel="stylesheet" type="text/css"/>
|
||||||
svg {
|
<link href="//fonts.googleapis.com/css?family=Ubuntu+Mono" rel="stylesheet" type="text/css"/>
|
||||||
position: relative;
|
<link href="//fonts.googleapis.com/css?family=Gentium" rel="stylesheet" type="text/css"/>
|
||||||
top: 0.5em;
|
<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>
|
||||||
|
|
||||||
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>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
|
|
||||||
<h1>Submit signing request</h1>
|
<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 %}
|
{% set s = authority.certificate.subject %}
|
||||||
|
|
||||||
<!--
|
|
||||||
<p>To submit new certificate signing request first set common name, eg:</p>
|
|
||||||
<pre>
|
<pre>
|
||||||
export CN=$(hostname)
|
opkg update
|
||||||
|
opkg install strongswan-default curl openssl-util
|
||||||
|
modprobe authenc
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<p>Generate key and submit using standard shell tools:</p>
|
<p>Generate key and submit using standard shell tools:</p>
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
curl {{request.url}}/certificate/ > ca.crt
|
CN=$(cat /proc/sys/kernel/hostname)
|
||||||
openssl genrsa -out $CN.key 4096
|
curl {{request.url}}/certificate/ > /etc/ipsec.d/cacerts/ca.pem
|
||||||
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"
|
openssl genrsa -out /etc/ipsec.d/private/$CN.pem 4096
|
||||||
wget --header "Content-Type: application/pkcs10" --post-data="$(cat $CN.csr)" http://localhost:9090/api/buujaa/request/?autosign=1\&wait=30 -O $CN.crt
|
chmod 0600 /etc/ipsec.d/private/$CN.pem
|
||||||
openssl verify -CAfile ca.crt $CN.crt
|
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>
|
</pre>
|
||||||
-->
|
|
||||||
|
|
||||||
<p>Assuming you have Certidude installed</p>
|
<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}}]
|
[CA_{{slug}}]
|
||||||
default_days = 1825
|
default_crl_days = {{revocation_list_lifetime}}
|
||||||
|
default_days = {{certificate_lifetime}}
|
||||||
dir = {{directory}}
|
dir = {{directory}}
|
||||||
private_key = $dir/ca_key.pem
|
private_key = $dir/ca_key.pem
|
||||||
certificate = $dir/ca_crt.pem
|
certificate = $dir/ca_crt.pem
|
||||||
@ -9,12 +10,15 @@ certs = $dir/signed/
|
|||||||
crl = $dir/ca_crl.pem
|
crl = $dir/ca_crl.pem
|
||||||
serial = $dir/serial
|
serial = $dir/serial
|
||||||
{% if crl_distribution_points %}
|
{% if crl_distribution_points %}
|
||||||
crlDistributionPoints = {{crl_distribution_points}}{% endif %}
|
crlDistributionPoints = {{crl_distribution_points}}
|
||||||
|
{% endif %}
|
||||||
{% if email_address %}
|
{% if email_address %}
|
||||||
emailAddress = {{email_address}}{% endif %}
|
emailAddress = {{email_address}}
|
||||||
|
{% endif %}
|
||||||
x509_extensions = {{slug}}_cert
|
x509_extensions = {{slug}}_cert
|
||||||
policy = poliy_{{slug}}
|
policy = poliy_{{slug}}
|
||||||
autosign_whitelist = 127.
|
request_whitelist =
|
||||||
|
autosign_whitelist = 127.0.0.0/8
|
||||||
inbox = {{inbox}}
|
inbox = {{inbox}}
|
||||||
outbox = {{outbox}}
|
outbox = {{outbox}}
|
||||||
|
|
||||||
|
@ -14,4 +14,6 @@ group nogroup
|
|||||||
ifconfig-pool-persist /tmp/openvpn-leases.txt
|
ifconfig-pool-persist /tmp/openvpn-leases.txt
|
||||||
ifconfig {{subnet_first}} {{subnet.netmask}}
|
ifconfig {{subnet_first}} {{subnet.netmask}}
|
||||||
server-bridge {{subnet_first}} {{subnet.netmask}} {{subnet_second}} {{subnet_last}}
|
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 socket
|
||||||
import io
|
import io
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
import ipaddress
|
||||||
from configparser import RawConfigParser
|
from configparser import RawConfigParser
|
||||||
from Crypto.Util import asn1
|
from Crypto.Util import asn1
|
||||||
from OpenSSL import crypto
|
from OpenSSL import crypto
|
||||||
@ -78,7 +79,7 @@ class CertificateAuthorityConfig(object):
|
|||||||
section = "CA_" + slug
|
section = "CA_" + slug
|
||||||
|
|
||||||
dirs = dict([(key, self.get(section, key))
|
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
|
# Variable expansion, eg $dir
|
||||||
for key, value in dirs.items():
|
for key, value in dirs.items():
|
||||||
@ -89,7 +90,8 @@ class CertificateAuthorityConfig(object):
|
|||||||
dirs["email_address"] = self.get(section, "emailAddress")
|
dirs["email_address"] = self.get(section, "emailAddress")
|
||||||
dirs["inbox"] = self.get(section, "inbox")
|
dirs["inbox"] = self.get(section, "inbox")
|
||||||
dirs["outbox"] = self.get(section, "outbox")
|
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")
|
extensions_section = self.get(section, "x509_extensions")
|
||||||
if extensions_section:
|
if extensions_section:
|
||||||
@ -296,7 +298,7 @@ class Request(CertificateBase):
|
|||||||
else:
|
else:
|
||||||
raise ValueError("Can't parse %s as X.509 certificate signing request!" % mixed)
|
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
|
@property
|
||||||
def signable(self):
|
def signable(self):
|
||||||
@ -390,29 +392,25 @@ class Certificate(CertificateBase):
|
|||||||
|
|
||||||
class CertificateAuthority(object):
|
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.slug = slug
|
||||||
self.revocation_list = crl
|
self.revocation_list = crl
|
||||||
self.signed_dir = certs
|
self.signed_dir = certs
|
||||||
self.request_dir = new_certs_dir
|
self.request_dir = new_certs_dir
|
||||||
self.revoked_dir = revoked_certs_dir
|
self.revoked_dir = revoked_certs_dir
|
||||||
self.private_key = private_key
|
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.certificate = Certificate(open(certificate))
|
||||||
self.mailer = Mailer(outbox) if outbox else None
|
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.basic_constraints = basic_constraints
|
||||||
self.key_usage = key_usage
|
self.key_usage = key_usage
|
||||||
self.extended_key_usage = extended_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):
|
def _signer_exec(self, cmd, *bits):
|
||||||
sock = self.connect_signer()
|
sock = self.connect_signer()
|
||||||
sock.send(cmd.encode("ascii"))
|
sock.send(cmd.encode("ascii"))
|
||||||
@ -540,7 +538,7 @@ class CertificateAuthority(object):
|
|||||||
self.certificate._obj,
|
self.certificate._obj,
|
||||||
request._obj,
|
request._obj,
|
||||||
self.basic_constraints,
|
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")
|
path = os.path.join(self.signed_dir, request.common_name + ".pem")
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
|
@ -13,8 +13,8 @@ from certidude.api import CertificateAuthorityResource, \
|
|||||||
|
|
||||||
config = CertificateAuthorityConfig("/etc/ssl/openssl.cnf")
|
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_SUBSCRIBE"), "Please set CERTIDUDE_EVENT_SUBSCRIBE to your web server's subscription URL"
|
||||||
assert os.getenv("CERTIDUDE_EVENT_PUBLISH"), "Please set CERTIDUDE_EVENT_SUBSCRIBE to your web server's subscribe URL"
|
assert os.getenv("CERTIDUDE_EVENT_PUBLISH"), "Please set CERTIDUDE_EVENT_PUBLISH to your web server's publishing URL"
|
||||||
|
|
||||||
app = falcon.API()
|
app = falcon.API()
|
||||||
app.add_route("/api/{ca}/ocsp/", CertificateStatusResource(config))
|
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
|
# coding: utf-8
|
||||||
import os
|
import os
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name = "certidude",
|
name = "certidude",
|
||||||
version = "0.1.7",
|
version = "0.1.17",
|
||||||
author = u"Lauri Võsandi",
|
author = u"Lauri Võsandi",
|
||||||
author_email = "lauri.vosandi@gmail.com",
|
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.",
|
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