mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 01:19:11 +00:00 
			
		
		
		
	Released 0.1.17
This commit is contained in:
		| @@ -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 | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								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.", | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user