diff --git a/README.rst b/README.rst index 67d0255..ae790ea 100644 --- a/README.rst +++ b/README.rst @@ -336,26 +336,28 @@ To uninstall: Offline install --------------- -To set up certificate authority in an isolated environment use a -vanilla Ubuntu 16.04 or container to collect the artifacts: +To prepare packages for offline installation use following snippet on a +vanilla Ubuntu 16.04 or container: .. code:: bash + rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl + apt install --download-only python3-pip + pip3 wheel --wheel-dir=/var/cache/certidude/wheels -r requirements.txt + pip3 wheel --wheel-dir=/var/cache/certidude/wheels . + tar -cf certidude-client.tar /var/cache/certidude/wheels add-apt-repository -y ppa:nginx/stable apt-get update -q - rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl apt install --download-only python3-markdown python3-pyxattr python3-jinja2 python3-cffi software-properties-common libnginx-mod-nchan nginx-full - pip3 wheel --wheel-dir=/var/cache/certidude/wheels -r requirements.txt pip3 wheel --wheel-dir=/var/cache/certidude/wheels falcon humanize ipaddress simplepam user-agents python-ldap gssapi - pip3 wheel --wheel-dir=/var/cache/certidude/wheels . - tar -cf certidude-assets.tar /var/lib/certidude/assets/ /var/cache/apt/archives/ /var/cache/certidude/wheels + tar -cf certidude-server.tar /var/lib/certidude/assets/ /var/cache/apt/archives/ /var/cache/certidude/wheels -Transfer certidude-artifacts.tar to the target machine and execute: +Transfer certidude-server.tar or certidude-client.tar to the target machine and execute: .. code:: bash rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl - tar -xvf certidude-artifacts.tar -C / + tar -xvf certidude-*.tar -C / dpkg -i /var/cache/apt/archives/*.deb pip3 install --use-wheel --no-index --find-links /var/cache/certidude/wheels/*.whl diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index dfc6f8c..9039002 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -17,6 +17,7 @@ class NormalizeMiddleware(object): def certidude_app(log_handlers=[]): from certidude import authority, config + from certidude.tokens import TokenManager from .signed import SignedCertificateDetailResource from .request import RequestListResource, RequestDetailResource from .lease import LeaseResource, LeaseDetailResource @@ -36,10 +37,20 @@ def certidude_app(log_handlers=[]): app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource(authority)) app.add_route("/api/request/{cn}/", RequestDetailResource(authority)) app.add_route("/api/request/", RequestListResource(authority)) - app.add_route("/api/", SessionResource(authority)) + token_resource = None + token_manager = None if config.USER_ENROLLMENT_ALLOWED: # TODO: add token enable/disable flag for config - app.add_route("/api/token/", TokenResource(authority)) + if config.TOKEN_BACKEND == "sql": + token_manager = TokenManager(config.TOKEN_DATABASE) + token_resource = TokenResource(authority, token_manager) + app.add_route("/api/token/", token_resource) + elif not config.TOKEN_BACKEND: + pass + else: + raise NotImplementedError("Token backend '%s' not supported" % config.TOKEN_BACKEND) + + app.add_route("/api/", SessionResource(authority, token_manager)) # Extended attributes for scripting etc. app.add_route("/api/signed/{cn}/attr/", AttributeResource(authority, namespace="machine")) diff --git a/certidude/api/log.py b/certidude/api/log.py index 78424fc..19e4701 100644 --- a/certidude/api/log.py +++ b/certidude/api/log.py @@ -11,4 +11,5 @@ class LogResource(RelationalMixin): @authorize_admin def on_get(self, req, resp): # TODO: Add last id parameter - return self.iterfetch("select * from log order by created desc") + return self.iterfetch("select * from log order by created desc limit ?", + req.get_param_as_int("limit")) diff --git a/certidude/api/ocsp.py b/certidude/api/ocsp.py index a277139..bcbfaeb 100644 --- a/certidude/api/ocsp.py +++ b/certidude/api/ocsp.py @@ -4,8 +4,8 @@ import os from asn1crypto.util import timezone from asn1crypto import ocsp from base64 import b64decode -from certidude import config -from datetime import datetime +from certidude import config, const +from datetime import datetime, timedelta from oscrypto import asymmetric from .utils import AuthorityHandler from .utils.firewall import whitelist_subnets @@ -88,7 +88,8 @@ class OCSPResource(AuthorityHandler): 'serial_number': serial, }, 'cert_status': status, - 'this_update': now, + 'this_update': now - const.CLOCK_SKEW_TOLERANCE, + 'next_update': now + timedelta(minutes=15) + const.CLOCK_SKEW_TOLERANCE, 'single_extensions': [] }) diff --git a/certidude/api/request.py b/certidude/api/request.py index 0afcba0..ece9ea6 100644 --- a/certidude/api/request.py +++ b/certidude/api/request.py @@ -10,6 +10,7 @@ from base64 import b64decode from certidude import config, push, errors from certidude.decorators import csrf_protection, MyEncoder from certidude.profile import SignatureProfile +from certidude.user import DirectoryConnection from datetime import datetime from oscrypto import asymmetric from oscrypto.errors import SignatureError @@ -84,13 +85,28 @@ class RequestListResource(AuthorityHandler): "Bad request", "Common name %s differs from Kerberos credential %s!" % (common_name, machine)) - # Automatic enroll with Kerberos machine cerdentials - resp.set_header("Content-Type", "application/x-pem-file") - cert, resp.body = self.authority._sign(csr, body, - profile=config.PROFILES["rw"], overwrite=overwrite_allowed) - logger.info("Automatically enrolled Kerberos authenticated machine %s from %s", - machine, req.context.get("remote_addr")) - return + hit = False + with DirectoryConnection() as conn: + ft = config.LDAP_COMPUTER_FILTER % ("%s$" % machine) + attribs = "cn", + r = conn.search_s(config.LDAP_BASE, 2, ft, attribs) + for dn, entry in r: + if not dn: + continue + else: + hit = True + break + + if hit: + # Automatic enroll with Kerberos machine cerdentials + resp.set_header("Content-Type", "application/x-pem-file") + cert, resp.body = self.authority._sign(csr, body, + profile=config.PROFILES["rw"], overwrite=overwrite_allowed) + logger.info("Automatically enrolled Kerberos authenticated machine %s (%s) from %s", + machine, dn, req.context.get("remote_addr")) + return + else: + logger.error("Kerberos authenticated machine %s didn't fit the 'ldap computer filter' criteria %s" % (machine, ft)) """ diff --git a/certidude/api/session.py b/certidude/api/session.py index d4d1db9..7919c26 100644 --- a/certidude/api/session.py +++ b/certidude/api/session.py @@ -18,9 +18,13 @@ class CertificateAuthorityResource(object): resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb") resp.append_header("Content-Type", "application/x-x509-ca-cert") resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % - const.HOSTNAME.encode("ascii")) + const.HOSTNAME) class SessionResource(AuthorityHandler): + def __init__(self, authority, token_manager): + AuthorityHandler.__init__(self, authority) + self.token_manager = token_manager + @csrf_protection @serialize @login_required @@ -97,7 +101,7 @@ class SessionResource(AuthorityHandler): signer_username = None # TODO: dedup - yield dict( + serialized = dict( serial = "%x" % cert.serial_number, organizational_unit = cert.subject.native.get("organizational_unit_name"), common_name = common_name, @@ -109,12 +113,37 @@ class SessionResource(AuthorityHandler): lease = lease, tags = tags, attributes = attributes or None, - extensions = dict([ - (e["extn_id"].native, e["extn_value"].native) - for e in cert["tbs_certificate"]["extensions"] - if e["extn_id"].native in ("extended_key_usage",)]) + responder_url = None ) + for e in cert["tbs_certificate"]["extensions"].native: + if e["extn_id"] == "key_usage": + serialized["key_usage"] = e["extn_value"] + elif e["extn_id"] == "extended_key_usage": + serialized["extended_key_usage"] = e["extn_value"] + elif e["extn_id"] == "basic_constraints": + serialized["basic_constraints"] = e["extn_value"] + elif e["extn_id"] == "crl_distribution_points": + for c in e["extn_value"]: + serialized["revoked_url"] = c["distribution_point"] + break + serialized["extended_key_usage"] = e["extn_value"] + elif e["extn_id"] == "authority_information_access": + for a in e["extn_value"]: + if a["access_method"] == "ocsp": + serialized["responder_url"] = a["access_location"] + else: + raise NotImplementedError("Don't know how to handle AIA access method %s" % a["access_method"]) + elif e["extn_id"] == "authority_key_identifier": + pass + elif e["extn_id"] == "key_identifier": + pass + elif e["extn_id"] == "subject_alt_name": + serialized["subject_alt_name"], = e["extn_value"] + else: + raise NotImplementedError("Don't know how to handle extension %s" % e["extn_id"]) + yield serialized + logger.info("Logged in authority administrator %s from %s with %s" % ( req.context.get("user"), req.context.get("remote_addr"), req.context.get("user_agent"))) return dict( @@ -130,10 +159,12 @@ class SessionResource(AuthorityHandler): routers = [j[0] for j in self.authority.list_signed( common_name=config.SERVICE_ROUTERS)] ), + builder = dict( + profiles = config.IMAGE_BUILDER_PROFILES or None + ), authority = dict( - builder = dict( - profiles = config.IMAGE_BUILDER_PROFILES - ), + hostname = const.FQDN, + tokens = self.token_manager.list() if self.token_manager else None, tagging = [dict(name=t[0], type=t[1], title=t[2]) for t in config.TAG_TYPES], lease = dict( offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option @@ -145,32 +176,39 @@ class SessionResource(AuthorityHandler): distinguished_name = cert_to_dn(self.authority.certificate), md5sum = hashlib.md5(self.authority.certificate_buf).hexdigest(), blob = self.authority.certificate_buf.decode("ascii"), + organization = self.authority.certificate["tbs_certificate"]["subject"].native.get("organization_name"), + signed = self.authority.certificate["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None), + expires = self.authority.certificate["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) ), mailer = dict( name = config.MAILER_NAME, address = config.MAILER_ADDRESS ) if config.MAILER_ADDRESS else None, - machine_enrollment_subnets=config.MACHINE_ENROLLMENT_SUBNETS, user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED, user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES, events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN, requests=serialize_requests(self.authority.list_requests), signed=serialize_certificates(self.authority.list_signed), revoked=serialize_revoked(self.authority.list_revoked), - admin_users = User.objects.filter_admins(), - user_subnets = config.USER_SUBNETS or None, - autosign_subnets = config.AUTOSIGN_SUBNETS or None, - request_subnets = config.REQUEST_SUBNETS or None, - admin_subnets=config.ADMIN_SUBNETS or None, signature = dict( revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME, profiles = sorted([p.serialize() for p in config.PROFILES.values()], key=lambda p:p.get("slug")), - ) ), + authorization = dict( + admin_users = User.objects.filter_admins(), + + user_subnets = config.USER_SUBNETS or None, + autosign_subnets = config.AUTOSIGN_SUBNETS or None, + request_subnets = config.REQUEST_SUBNETS or None, + machine_enrollment_subnets=config.MACHINE_ENROLLMENT_SUBNETS or None, + admin_subnets=config.ADMIN_SUBNETS or None, + + ocsp_subnets = config.OCSP_SUBNETS or None, + crl_subnets = config.CRL_SUBNETS or None, + scep_subnets = config.SCEP_SUBNETS or None, + ), features=dict( - ocsp=bool(config.OCSP_SUBNETS), - crl=bool(config.CRL_SUBNETS), token=bool(config.TOKEN_URL), tagging=True, leases=True, diff --git a/certidude/api/token.py b/certidude/api/token.py index 38feba3..12c3c6c 100644 --- a/certidude/api/token.py +++ b/certidude/api/token.py @@ -1,11 +1,16 @@ +import click +import codecs import falcon import logging -import hashlib +import os +import string from asn1crypto import pem from asn1crypto.csr import CertificationRequest -from datetime import datetime +from datetime import datetime, timedelta from time import time -from certidude import mailer +from certidude import mailer, const +from certidude.tokens import TokenManager +from certidude.relational import RelationalMixin from certidude.decorators import serialize from certidude.user import User from certidude import config @@ -15,33 +20,25 @@ from .utils.firewall import login_required, authorize_admin logger = logging.getLogger(__name__) class TokenResource(AuthorityHandler): + def __init__(self, authority, manager): + AuthorityHandler.__init__(self, authority) + self.manager = manager + + def on_get(self, req, resp): + return + def on_put(self, req, resp): - # Consume token - now = time() - timestamp = req.get_param_as_int("t", required=True) - username = req.get_param("u", required=True) - user = User.objects.get(username) - csum = hashlib.sha256() - csum.update(config.TOKEN_SECRET) - csum.update(username.encode("ascii")) - csum.update(str(timestamp).encode("ascii")) - - margin = 300 # Tolerate 5 minute clock skew as Kerberos does - if csum.hexdigest() != req.get_param("c", required=True): - raise falcon.HTTPForbidden("Forbidden", "Invalid token supplied, did you copy-paste link correctly?") - if now < timestamp - margin: - raise falcon.HTTPForbidden("Forbidden", "Token not valid yet, are you sure server clock is correct?") - if now > timestamp + margin + config.TOKEN_LIFETIME: - raise falcon.HTTPForbidden("Forbidden", "Token expired") - - # At this point consider token to be legitimate + try: + username, mail, created, expires, profile = self.manager.consume(req.get_param("token", required=True)) + except RelationalMixin.DoesNotExist: + raise falcon.HTTPForbidden("Forbidden", "No such token or token expired") body = req.stream.read(req.content_length) header, _, der_bytes = pem.unarmor(body) csr = CertificationRequest.load(der_bytes) common_name = csr["certification_request_info"]["subject"].native["common_name"] assert common_name == username or common_name.startswith(username + "@"), "Invalid common name %s" % common_name try: - _, resp.body = self.authority._sign(csr, body, profile="default", + _, resp.body = self.authority._sign(csr, body, profile=config.PROFILES.get(profile), overwrite=config.TOKEN_OVERWRITE_PERMITTED) resp.set_header("Content-Type", "application/x-pem-file") logger.info("Autosigned %s as proven by token ownership", common_name) @@ -56,40 +53,7 @@ class TokenResource(AuthorityHandler): @login_required @authorize_admin def on_post(self, req, resp): - # Generate token - issuer = req.context.get("user") - username = req.get_param("username") - secondary = req.get_param("mail") - - if username: - # Otherwise try to look up user so we can derive their e-mail address - user = User.objects.get(username) - else: - # If no username is specified, assume it's intended for someone outside domain - username = "guest-%s" % hashlib.sha256(secondary.encode("ascii")).hexdigest()[-8:] - if not secondary: - raise - - timestamp = int(time()) - csum = hashlib.sha256() - csum.update(config.TOKEN_SECRET) - csum.update(username.encode("ascii")) - csum.update(str(timestamp).encode("ascii")) - args = "u=%s&t=%d&c=%s&i=%s" % (username, timestamp, csum.hexdigest(), issuer.name) - - # Token lifetime in local time, to select timezone: dpkg-reconfigure tzdata - token_created = datetime.fromtimestamp(timestamp) - token_expires = datetime.fromtimestamp(timestamp + config.TOKEN_LIFETIME) - try: - with open("/etc/timezone") as fh: - token_timezone = fh.read().strip() - except EnvironmentError: - token_timezone = None - url = "%s#%s" % (config.TOKEN_URL, args) - context = globals() - context.update(locals()) - mailer.send("token.md", to=user, **context) - return { - "token": args, - "url": url, - } + self.manager.issue( + issuer = req.context.get("user"), + subject = User.objects.get(req.get_param("username", required=True)), + subject_mail = req.get_param("mail")) diff --git a/certidude/api/utils/firewall.py b/certidude/api/utils/firewall.py index 0852f29..7622264 100644 --- a/certidude/api/utils/firewall.py +++ b/certidude/api/utils/firewall.py @@ -110,7 +110,7 @@ def authenticate(optional=False): if kerberized: if not req.auth.startswith("Negotiate "): raise falcon.HTTPBadRequest("Bad request", - "Bad header, expected Negotiate: %s" % req.auth) + "Bad header, expected Negotiate") os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB @@ -158,7 +158,7 @@ def authenticate(optional=False): else: if not req.auth.startswith("Basic "): - raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Basic: %s" % req.auth) + raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Basic") basic, token = req.auth.split(" ", 1) user, passwd = b64decode(token).decode("ascii").split(":", 1) diff --git a/certidude/authority.py b/certidude/authority.py index ef538b5..d9dd22a 100644 --- a/certidude/authority.py +++ b/certidude/authority.py @@ -13,26 +13,14 @@ from asn1crypto.csr import CertificationRequest from certbuilder import CertificateBuilder from certidude import config, push, mailer, const from certidude import errors -from certidude.common import cn_to_dn +from certidude.common import cn_to_dn, generate_serial, random from crlbuilder import CertificateListBuilder, pem_armor_crl from csrbuilder import CSRBuilder, pem_armor_csr from datetime import datetime, timedelta from jinja2 import Template -from random import SystemRandom from xattr import getxattr, listxattr, setxattr logger = logging.getLogger(__name__) -random = SystemRandom() - -try: - from time import time_ns -except ImportError: - from time import time - def time_ns(): - return int(time() * 10**9) # 64 bits integer, 32 ns bits - -def generate_serial(): - return time_ns() << 56 | random.randint(0, 2**56-1) # https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/ # https://jamielinux.com/docs/openssl-certificate-authority/ @@ -61,13 +49,14 @@ def self_enroll(skip_notify=False): self_public_key = asymmetric.load_public_key(path) private_key = asymmetric.load_private_key(config.SELF_KEY_PATH) except FileNotFoundError: # certificate or private key not found + click.echo("Generating private key for frontend: %s" % config.SELF_KEY_PATH) with open(config.SELF_KEY_PATH, 'wb') as fh: if public_key.algorithm == "ec": self_public_key, private_key = asymmetric.generate_pair("ec", curve=public_key.curve) elif public_key.algorithm == "rsa": self_public_key, private_key = asymmetric.generate_pair("rsa", bit_size=public_key.bit_size) else: - NotImplemented + raise NotImplemented("CA certificate public key algorithm %s not supported" % public_key.algorithm) fh.write(asymmetric.dump_private_key(private_key, None)) else: now = datetime.utcnow() @@ -84,10 +73,11 @@ def self_enroll(skip_notify=False): drop_privileges() assert os.getuid() != 0 and os.getgid() != 0 path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") - click.echo("Writing request to %s" % path) + click.echo("Writing certificate signing request for frontend: %s" % path) with open(path, "wb") as fh: fh.write(pem_armor_csr(request)) # Write CSR with certidude permissions authority.sign(common_name, skip_notify=skip_notify, skip_push=True, overwrite=True, profile=config.PROFILES["srv"]) + click.echo("Frontend certificate signed") sys.exit(0) else: os.waitpid(pid, 0) @@ -409,13 +399,15 @@ def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False builder.serial_number = generate_serial() now = datetime.utcnow() - builder.begin_date = now - timedelta(minutes=5) + builder.begin_date = now - const.CLOCK_SKEW_TOLERANCE builder.end_date = now + timedelta(days=profile.lifetime) builder.issuer = certificate builder.ca = profile.ca builder.key_usage = profile.key_usage builder.extended_key_usage = profile.extended_key_usage builder.subject_alt_domains = [common_name] + builder.ocsp_url = profile.responder_url + builder.crl_url = profile.revoked_url end_entity_cert = builder.build(private_key) end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert) diff --git a/certidude/cli.py b/certidude/cli.py index 01d7eb2..3e87a07 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -18,7 +18,7 @@ from certbuilder import CertificateBuilder, pem_armor_certificate from certidude import const from csrbuilder import CSRBuilder, pem_armor_csr from configparser import ConfigParser, NoOptionError -from certidude.common import apt, rpm, drop_privileges, selinux_fixup, cn_to_dn +from certidude.common import apt, rpm, drop_privileges, selinux_fixup, cn_to_dn, generate_serial from datetime import datetime, timedelta from glob import glob from ipaddress import ip_network @@ -51,7 +51,7 @@ def setup_client(prefix="client_", dh=False): authority = arguments.get("authority") b = os.path.join("/etc/certidude/authority", authority) if dh: - path = os.path.join(const.STORAGE_PATH, "dh.pem") + path = os.path.join("/etc/ssl/dhparam.pem") if not os.path.exists(path): rpm("openssl") apt("openssl") @@ -390,7 +390,6 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self): } if renew: # Do mutually authenticated TLS handshake - request_url = "https://%s:8443/api/request/" % authority_name kwargs["cert"] = certificate_path, key_path click.echo("Renewing using current keypair at %s %s" % kwargs["cert"]) else: @@ -417,8 +416,8 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self): kwargs["auth"] = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True) else: click.echo("Not using machine keytab") - request_url = "https://%s/api/request/" % authority_name + request_url = "https://%s:8443/api/request/" % authority_name if request_params: request_url = request_url + "?" + "&".join(request_params) submission = requests.post(request_url, **kwargs) @@ -580,7 +579,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self): nm_config.set("vpn", "key", key_path) nm_config.set("vpn", "cert", certificate_path) nm_config.set("vpn", "ca", authority_path) - nm_config.set("vpn", "tls-cipher", "TLS-%s-WITH-AES-128-GCM-SHA384" % ( + nm_config.set("vpn", "tls-cipher", "TLS-%s-WITH-AES-256-GCM-SHA384" % ( "ECDHE-ECDSA" if authority_public_key.algorithm == "ec" else "DHE-RSA")) nm_config.set("vpn", "cipher", "AES-128-GCM") nm_config.set("vpn", "auth", "SHA384") @@ -995,6 +994,10 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat default="/etc/nginx/sites-available/certidude.conf", type=click.File(mode="w", atomic=True, lazy=True), help="nginx site config for serving Certidude, /etc/nginx/sites-available/certidude by default") +@click.option("--tls-config", + default="/etc/nginx/conf.d/tls.conf", + type=click.File(mode="w", atomic=True, lazy=True), + help="TLS configuration file of nginx, /etc/nginx/conf.d/tls.conf by default") @click.option("--common-name", "-cn", default=const.FQDN, help="Common name of the server, %s by default" % const.FQDN) @click.option("--title", "-t", default="Certidude at %s" % const.FQDN, help="Common name of the certificate authority, 'Certidude at %s' by default" % const.FQDN) @click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 20 years by default") @@ -1008,7 +1011,7 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat @click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair") @click.option("--subordinate", is_flag=True, help="Set up subordinate CA instead of root CA") @fqdn_required -def certidude_setup_authority(username, kerberos_keytab, nginx_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, title, skip_assets, skip_packages, elliptic_curve, subordinate): +def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, title, skip_assets, skip_packages, elliptic_curve, subordinate): assert subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) in (b"trusty\n", b"xenial\n", b"bionic\n"), "Only Ubuntu 16.04 supported at the moment" assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root" @@ -1027,21 +1030,28 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev \ rsync attr wget unzip" click.echo("Running: %s" % cmd) - if os.system(cmd): sys.exit(254) - if os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam user-agents"): sys.exit(253) - if os.system("pip3 install -q --pre --upgrade python-ldap"): exit(252) + if os.system(cmd): + raise click.ClickException("Failed to install APT packages") + if os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam user-agents"): + raise click.ClickException("Failed to install Python packages") + if os.system("pip3 install -q --pre --upgrade python-ldap"): + raise click.ClickException("Failed to install python-ldap") if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"): click.echo("Enabling nginx PPA") - if os.system("add-apt-repository -y ppa:nginx/stable"): sys.exit(251) - if os.system("apt-get update -q"): sys.exit(250) - if os.system("apt-get install -y -q libnginx-mod-nchan"): sys.exit(249) + if os.system("add-apt-repository -y ppa:nginx/stable"): + raise click.ClickException("Failed to add nginx PPA") + if os.system("apt-get update -q"): + raise click.ClickException("Failed to update package lists") + if os.system("apt-get install -y -q libnginx-mod-nchan"): + raise click.ClickException("Failed to install nchan") else: click.echo("PPA for nginx already enabled") if not os.path.exists("/usr/sbin/nginx"): click.echo("Installing nginx from PPA") - if os.system("apt-get install -y -q nginx"): sys.exit(248) + if os.system("apt-get install -y -q nginx"): + raise click.ClickException("Failed to install nginx") else: click.echo("Web server nginx already installed") @@ -1049,7 +1059,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat os.symlink("/usr/bin/nodejs", "/usr/bin/node") # Generate secret for tokens - token_secret = ''.join(random.choice(string.ascii_letters + string.digits + '!@#$%^&*()') for i in range(50)) + token_url = "https://" + const.FQDN + "/#action=enroll&token=%(token)s&router=%(router)s&protocol=ovpn" template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "profile") click.echo("Using templates from %s" % template_path) @@ -1062,6 +1072,10 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat revoked_url = "http://%s/api/revoked/" % common_name click.echo("Setting revocation list URL to %s" % revoked_url) + responder_url = "http://%s/api/ocsp/" % common_name + click.echo("Setting OCSP responder URL to %s" % responder_url) + + # Expand variables assets_dir = os.path.join(directory, "assets") ca_key = os.path.join(directory, "ca_key.pem") @@ -1070,6 +1084,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat self_key = os.path.join(directory, "self_key.pem") sqlite_path = os.path.join(directory, "meta", "db.sqlite") distinguished_name = cn_to_dn("Certidude at %s" % common_name, common_name, o=organization, ou=organizational_unit) + dhparam_path = "/etc/ssl/dhparam.pem" # Builder variables dhgroup = "ecp384" if elliptic_curve else "modp2048" @@ -1080,8 +1095,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat except KeyError: cmd = "adduser", "--system", "--no-create-home", "--group", "certidude" if subprocess.call(cmd): - click.echo("Failed to create system user 'certidude'") - return 255 + raise click.ClickException("Failed to create system user 'certidude'") if os.path.exists(kerberos_keytab): click.echo("Service principal keytab found in '%s'" % kerberos_keytab) @@ -1114,6 +1128,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat letsencrypt_fullchain = "/etc/letsencrypt/live/%s/fullchain.pem" % common_name letsencrypt_privkey = "/etc/letsencrypt/live/%s/privkey.pem" % common_name letsencrypt = os.path.exists(letsencrypt_fullchain) + doc_path = os.path.join(os.path.realpath(os.path.dirname(os.path.dirname(__file__))), "doc") script_dir = os.path.join(os.path.realpath(os.path.dirname(__file__)), "templates", "script") @@ -1163,26 +1178,27 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat if skip_packages: click.echo("Not attempting to install packages from NPM as requested...") else: - cmd = "npm install --silent -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg" + cmd = "npm install --silent --no-optional -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg" click.echo("Installing JavaScript packages: %s" % cmd) - if os.system(cmd): sys.exit(230) if skip_assets: click.echo("Not attempting to assemble assets as requested...") else: # Copy fonts click.echo("Copying fonts...") - if os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir): sys.exit(229) + if os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir): + raise click.ClickException("Failed to copy fonts") # Compile nunjucks templates - cmd = 'nunjucks-precompile --include ".html$" --include ".ps1$" --include ".sh$" --include ".svg" %s > %s.part' % (static_path, bundle_js) + cmd = 'nunjucks-precompile --include "\.html$" --include "\.ps1$" --include "\.sh$" --include "\.svg$" --include "\.yml$" --include "\.conf$" --include "\.mobileconfig$" %s > %s.part' % (static_path, bundle_js) click.echo("Compiling templates: %s" % cmd) - if os.system(cmd): sys.exit(228) + if os.system(cmd): + raise click.ClickException("Failed to compile nunjucks templates") # Assemble bundle.js click.echo("Assembling %s" % bundle_js) with open(bundle_js + ".part", "a") as fh: - for pkg in "qrcode-svg/dist/qrcode.min.js", "jquery/dist/jquery.min.js", "timeago/*.js", "nunjucks/browser/nunjucks-slim.min.js", "tether/dist/js/*.min.js", "bootstrap/dist/js/*.min.js": + for pkg in "jquery/dist/jquery.min.js", "tether/dist/js/*.min.js", "bootstrap/dist/js/*.min.js", "node-forge/dist/forge.all.min.js", "qrcode-svg/dist/qrcode.min.js", "timeago/*.js", "nunjucks/browser/nunjucks-slim.min.js": for j in glob(os.path.join("/usr/local/lib/node_modules", pkg)): click.echo("- Merging: %s" % j) with open(j) as ih: @@ -1208,9 +1224,22 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat if not os.path.exists(const.CONFIG_DIR): click.echo("Creating %s" % const.CONFIG_DIR) os.makedirs(const.CONFIG_DIR) + if not os.path.exists(const.SCRIPT_DIR): + click.echo("Creating %s" % const.SCRIPT_DIR) + os.makedirs(const.SCRIPT_DIR) os.umask(0o177) # 600 + if not os.path.exists(dhparam_path): + cmd = "openssl", "dhparam", "-out", dhparam_path, ("1024" if os.getenv("TRAVIS") else str(const.KEY_SIZE)) + subprocess.check_call(cmd) + + if os.path.exists(tls_config.name): + click.echo("Configuration file %s already exists, not overwriting" % tls_config.name) + else: + tls_config.write(env.get_template("nginx-tls.conf").render(locals())) + click.echo("Generated %s" % tls_config.name) + if os.path.exists(const.SERVER_CONFIG_PATH): click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH) else: @@ -1227,6 +1256,14 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat fh.write(env.get_template("server/builder.conf").render(vars())) click.echo("File %s created" % const.BUILDER_CONFIG_PATH) + # Create image builder site script + if os.path.exists(const.BUILDER_SITE_SCRIPT): + click.echo("Image builder site customization script %s already exists, remove to regenerate" % const.BUILDER_SITE_SCRIPT) + else: + with open(const.BUILDER_SITE_SCRIPT, "w") as fh: + fh.write(env.get_template("server/site.sh").render(vars())) + click.echo("File %s created" % const.BUILDER_SITE_SCRIPT) + # Create signature profile config if os.path.exists(const.PROFILE_CONFIG_PATH): click.echo("Signature profile config %s already exists, remove to regenerate" % const.PROFILE_CONFIG_PATH) @@ -1235,20 +1272,16 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat fh.write(env.get_template("server/profile.conf").render(vars())) click.echo("File %s created" % const.PROFILE_CONFIG_PATH) - if not os.path.exists("/var/lib/certidude/builder"): - click.echo("Creating %s" % "/var/lib/certidude/builder") - os.makedirs("/var/lib/certidude/builder") - # Create subdirectories with 770 permissions os.umask(0o007) - for subdir in ("signed", "signed/by-serial", "requests", "revoked", "expired", "meta"): + for subdir in ("signed", "signed/by-serial", "requests", "revoked", "expired", "meta", "builder"): path = os.path.join(directory, subdir) if not os.path.exists(path): click.echo("Creating directory %s" % path) os.mkdir(path) else: click.echo("Directory already exists %s" % path) - assert os.stat(path).st_mode == 0o40770 + assert os.stat(path).st_mode == 0o40770, path # Create SQLite database file with correct permissions os.umask(0o117) @@ -1293,17 +1326,15 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat click.echo(" chmod 0644 %s" % ca_cert) click.echo() click.echo("To finish setup procedure run 'certidude setup authority' again") - sys.exit(1) + sys.exit(1) # stop this fork here with error # https://technet.microsoft.com/en-us/library/aa998840(v=exchg.141).aspx builder = CertificateBuilder(distinguished_name, public_key) builder.self_signed = True builder.ca = True - builder.serial_number = random.randint( - 0x100000000000000000000000000000000000000, - 0xfffffffffffffffffffffffffffffffffffffff) + builder.serial_number = generate_serial() - builder.begin_date = NOW - timedelta(minutes=5) + builder.begin_date = NOW - const.CLOCK_SKEW_TOLERANCE builder.end_date = NOW + timedelta(days=authority_lifetime) certificate = builder.build(private_key) @@ -1312,14 +1343,18 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat os.umask(0o137) with open(ca_cert, 'wb') as f: f.write(pem_armor_certificate(certificate)) + click.echo("Authority certificate written to: %s" % ca_cert) sys.exit(0) # stop this fork here else: + _, exitcode = os.waitpid(bootstrap_pid, 0) if exitcode: return 0 from certidude import authority authority.self_enroll(skip_notify=True) + assert os.path.exists(self_key) + assert os.path.exists(os.path.join(directory, "signed", const.FQDN) + ".pem") assert os.getuid() == 0 and os.getgid() == 0, "Enroll contaminated environment" assert os.stat(sqlite_path).st_mode == 0o100660 assert os.stat(ca_cert).st_mode == 0o100640 @@ -1343,6 +1378,11 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat click.echo(" openssl x509 -text -noout -in %s | less" % ca_cert) click.echo(" openssl rsa -check -in %s" % ca_key) click.echo(" openssl verify -CAfile %s %s" % (ca_cert, ca_cert)) + click.echo() + click.echo("To inspect logs and issued tokens:") + click.echo() + click.echo(" echo 'select * from log;' | sqlite3 /var/lib/certidude/meta/db.sqlite") + click.echo(" echo 'select * from token;' | sqlite3 /var/lib/certidude/meta/db.sqlite") return 0 @@ -1461,7 +1501,7 @@ def certidude_revoke(common_name, reason): @click.command("expire", help="Move expired certificates") def certidude_expire(): from certidude import authority, config - threshold = datetime.utcnow() - timedelta(minutes=5) # Kerberos tolerance + threshold = datetime.utcnow() - const.CLOCK_SKEW_TOLERANCE for common_name, path, buf, cert, signed, expires in authority.list_signed(): if expires < threshold: expired_path = os.path.join(config.EXPIRED_DIR, "%040x.pem" % cert.serial_number) @@ -1492,6 +1532,10 @@ def certidude_serve(port, listen, fork): from certidude import config + click.echo("OCSP responder subnets: %s" % config.OCSP_SUBNETS) + click.echo("CRL subnets: %s" % config.CRL_SUBNETS) + click.echo("SCEP subnets: %s" % config.SCEP_SUBNETS) + click.echo("Loading signature profiles:") for profile in config.PROFILES.values(): click.echo("- %s" % profile) @@ -1625,6 +1669,35 @@ def certidude_test(recipient): to=recipient ) +@click.command("list", help="List tokens") +def certidude_token_list(): + from certidude import config + from certidude.tokens import TokenManager + token_manager = TokenManager(config.TOKEN_DATABASE) + cols = "uuid", "expires", "subject", "state" + now = datetime.utcnow() + for token in token_manager.list(expired=True, used=True, token=True): + token["state"] = "used" if token.get("used") else ("valid" if token.get("expires") > now else "expired") + print(";".join([str(token.get(col)) for col in cols])) + +@click.command("purge", help="Purge tokens") +@click.option("-a", "--all", default=False, is_flag=True, help="Purge all not only expired tokens") +def certidude_token_purge(all): + from certidude import config + from certidude.tokens import TokenManager + token_manager = TokenManager(config.TOKEN_DATABASE) + print(token_manager.purge(all)) + +@click.command("issue", help="Issue token") +@click.option("-m", "--subject-mail", default=None, help="Subject e-mail override") +@click.argument("subject") +def certidude_token_issue(subject, subject_mail): + from certidude import config + from certidude.tokens import TokenManager + from certidude.user import User + token_manager = TokenManager(config.TOKEN_DATABASE) + token_manager.issue(None, User.objects.get(subject), subject_mail) + @click.group("strongswan", help="strongSwan helpers") def certidude_setup_strongswan(): pass @@ -1635,6 +1708,9 @@ def certidude_setup_openvpn(): pass @click.group("setup", help="Getting started section") def certidude_setup(): pass +@click.group("token", help="Token management") +def certidude_token(): pass + @click.group() def entry_point(): pass @@ -1649,6 +1725,10 @@ certidude_setup.add_command(certidude_setup_openvpn) certidude_setup.add_command(certidude_setup_strongswan) certidude_setup.add_command(certidude_setup_nginx) certidude_setup.add_command(certidude_setup_yubikey) +certidude_token.add_command(certidude_token_list) +certidude_token.add_command(certidude_token_purge) +certidude_token.add_command(certidude_token_issue) +entry_point.add_command(certidude_token) entry_point.add_command(certidude_setup) entry_point.add_command(certidude_serve) entry_point.add_command(certidude_enroll) diff --git a/certidude/common.py b/certidude/common.py index f543476..ca7e16c 100644 --- a/certidude/common.py +++ b/certidude/common.py @@ -2,6 +2,16 @@ import os import click import subprocess +from random import SystemRandom + +random = SystemRandom() + +try: + from time import time_ns +except ImportError: + from time import time + def time_ns(): + return int(time() * 10**9) # 64 bits integer, 32 ns bits MAPPING = dict( common_name="CN", @@ -122,3 +132,6 @@ def pip(packages): pip.main(['install'] + packages.split(" ")) return True +def generate_serial(): + return time_ns() << 56 | random.randint(0, 2**56-1) + diff --git a/certidude/config.py b/certidude/config.py index 89bf619..f7ee9c3 100644 --- a/certidude/config.py +++ b/certidude/config.py @@ -23,6 +23,7 @@ LDAP_AUTHENTICATION_URI = cp.get("authentication", "ldap uri") LDAP_GSSAPI_CRED_CACHE = cp.get("accounts", "ldap gssapi credential cache") LDAP_ACCOUNTS_URI = cp.get("accounts", "ldap uri") LDAP_BASE = cp.get("accounts", "ldap base") +LDAP_MAIL_ATTRIBUTE = cp.get("accounts", "ldap mail attribute") USER_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "user subnets").split(" ") if j]) @@ -71,8 +72,7 @@ USER_MULTIPLE_CERTIFICATES = { REQUEST_SUBMISSION_ALLOWED = cp.getboolean("authority", "request submission allowed") AUTHORITY_CERTIFICATE_URL = cp.get("signature", "authority certificate url") -AUTHORITY_CRL_URL = cp.get("signature", "revoked url") -AUTHORITY_OCSP_URL = cp.get("signature", "responder url") +AUTHORITY_CRL_URL = "http://%s/api/revoked" % const.FQDN REVOCATION_LIST_LIFETIME = cp.getint("signature", "revocation list lifetime") @@ -88,6 +88,8 @@ USERS_GROUP = cp.get("authorization", "posix user group") ADMIN_GROUP = cp.get("authorization", "posix admin group") LDAP_USER_FILTER = cp.get("authorization", "ldap user filter") LDAP_ADMIN_FILTER = cp.get("authorization", "ldap admin filter") +LDAP_COMPUTER_FILTER = cp.get("authorization", "ldap computer filter") + if "%s" not in LDAP_USER_FILTER: raise ValueError("No placeholder %s for username in 'ldap user filter'") if "%s" not in LDAP_ADMIN_FILTER: raise ValueError("No placeholder %s for username in 'ldap admin filter'") @@ -95,9 +97,9 @@ TAG_TYPES = [j.split("/", 1) + [cp.get("tagging", j)] for j in cp.options("taggi # Tokens TOKEN_URL = cp.get("token", "url") -TOKEN_LIFETIME = cp.getint("token", "lifetime") * 60 # Convert minutes to seconds -TOKEN_SECRET = cp.get("token", "secret").encode("ascii") - +TOKEN_BACKEND = cp.get("token", "backend") +TOKEN_LIFETIME = timedelta(minutes=cp.getint("token", "lifetime")) # Convert minutes to seconds +TOKEN_DATABASE = cp.get("token", "database") # TODO: Check if we don't have base or servers # The API call for looking up scripts uses following directory as root @@ -115,11 +117,13 @@ PROFILES = dict([(key, SignatureProfile(key, profile_config.get(key, "key usage"), profile_config.get(key, "extended key usage"), profile_config.get(key, "common name"), - )) for key in profile_config.sections()]) + profile_config.get(key, "revoked url"), + profile_config.get(key, "responder url") +)) for key in profile_config.sections() if profile_config.getboolean(key, "enabled")]) cp2 = configparser.RawConfigParser() cp2.readfp(open(const.BUILDER_CONFIG_PATH, "r")) -IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections()] +IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections() if cp2.getboolean(j, "enabled")] TOKEN_OVERWRITE_PERMITTED=True diff --git a/certidude/const.py b/certidude/const.py index 9a4c435..e43a6fe 100644 --- a/certidude/const.py +++ b/certidude/const.py @@ -3,17 +3,21 @@ import click import os import socket import sys +from datetime import timedelta KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096 CURVE_NAME = "secp384r1" RE_FQDN = "^(([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])?$" RE_HOSTNAME = "^[a-z0-9]([a-z0-9\-_]{0,61}[a-z0-9])?$" RE_COMMON_NAME = "^[A-Za-z0-9\-\.\_@]+$" +CLOCK_SKEW_TOLERANCE = timedelta(minutes=5) # Kerberos-like clock skew tolerance RUN_DIR = "/run/certidude" CONFIG_DIR = "/etc/certidude" SERVER_CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf") BUILDER_CONFIG_PATH = os.path.join(CONFIG_DIR, "builder.conf") +SCRIPT_DIR = os.path.join(CONFIG_DIR, "script") +BUILDER_SITE_SCRIPT = os.path.join(SCRIPT_DIR, "site.sh") PROFILE_CONFIG_PATH = os.path.join(CONFIG_DIR, "profile.conf") CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf") SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf") diff --git a/certidude/profile.py b/certidude/profile.py index 8538123..26f4392 100644 --- a/certidude/profile.py +++ b/certidude/profile.py @@ -4,7 +4,7 @@ from datetime import timedelta from certidude import const class SignatureProfile(object): - def __init__(self, slug, title, ou, ca, lifetime, key_usage, extended_key_usage, common_name): + def __init__(self, slug, title, ou, ca, lifetime, key_usage, extended_key_usage, common_name, revoked_url, responder_url): self.slug = slug self.title = title self.ou = ou or None @@ -12,6 +12,9 @@ class SignatureProfile(object): self.lifetime = lifetime self.key_usage = set(key_usage.split(" ")) if key_usage else set() self.extended_key_usage = set(extended_key_usage.split(" ")) if extended_key_usage else set() + self.responder_url = responder_url + self.revoked_url = revoked_url + if common_name.startswith("^"): self.common_name = common_name elif common_name == "RE_HOSTNAME": @@ -39,7 +42,7 @@ class SignatureProfile(object): def serialize(self): return dict([(key, getattr(self,key)) for key in ( - "slug", "title", "ou", "ca", "lifetime", "key_usage", "extended_key_usage", "common_name")]) + "slug", "title", "ou", "ca", "lifetime", "key_usage", "extended_key_usage", "common_name", "responder_url", "revoked_url")]) def __repr__(self): bits = [] @@ -47,6 +50,10 @@ class SignatureProfile(object): bits.append("%d years" % (self.lifetime / 365)) if self.lifetime % 365: bits.append("%d days" % (self.lifetime % 365)) - return "%s (title=%s, ca=%s, ou=%s, lifetime=%s, key_usage=%s, extended_key_usage=%s, common_name=%s)" % ( - self.slug, self.title, self.ca, self.ou, " ".join(bits), self.key_usage, self.extended_key_usage, self.common_name) + return "%s (title=%s, ca=%s, ou=%s, lifetime=%s, key_usage=%s, extended_key_usage=%s, common_name=%s, responder_url=%s, revoked_url=%s)" % ( + self.slug, self.title, self.ca, self.ou, " ".join(bits), + self.key_usage, self.extended_key_usage, + repr(self.common_name), + repr(self.responder_url), + repr(self.revoked_url)) diff --git a/certidude/relational.py b/certidude/relational.py index e152f4f..dbaeb35 100644 --- a/certidude/relational.py +++ b/certidude/relational.py @@ -14,6 +14,9 @@ class RelationalMixin(object): SQL_CREATE_TABLES = "" + class DoesNotExist(Exception): + pass + def __init__(self, uri): self.uri = urlparse(uri) @@ -29,7 +32,8 @@ class RelationalMixin(object): if self.uri.netloc: raise ValueError("Malformed database URI %s" % self.uri) import sqlite3 - conn = sqlite3.connect(self.uri.path) + conn = sqlite3.connect(self.uri.path, + detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) else: raise NotImplementedError("Unsupported database scheme %s, currently only mysql://user:pass@host/database or sqlite:///path/to/database.sqlite is supported" % o.scheme) @@ -74,7 +78,6 @@ class RelationalMixin(object): conn.close() return rowid - def iterfetch(self, query, *args): conn = self.sql_connect() cursor = conn.cursor() @@ -86,3 +89,24 @@ class RelationalMixin(object): cursor.close() conn.close() return tuple(g()) + + def get(self, query, *args): + conn = self.sql_connect() + cursor = conn.cursor() + cursor.execute(query, args) + row = cursor.fetchone() + cursor.close() + conn.close() + if not row: + raise self.DoesNotExist("No matches for query '%s' with parameters %s" % (query, repr(args))) + return row + + def execute(self, query, *args): + conn = self.sql_connect() + cursor = conn.cursor() + cursor.execute(query, args) + affected_rows = cursor.rowcount + cursor.close() + conn.commit() + conn.close() + return affected_rows diff --git a/certidude/sql/sqlite/log_tables.sql b/certidude/sql/sqlite/log_tables.sql index f5dba49..17b59b3 100644 --- a/certidude/sql/sqlite/log_tables.sql +++ b/certidude/sql/sqlite/log_tables.sql @@ -1,4 +1,5 @@ create table if not exists log ( + id integer primary key autoincrement, created datetime, facility varchar(30), level int, diff --git a/certidude/sql/sqlite/token_issue.sql b/certidude/sql/sqlite/token_issue.sql new file mode 100644 index 0000000..5b65342 --- /dev/null +++ b/certidude/sql/sqlite/token_issue.sql @@ -0,0 +1,17 @@ +insert into token ( + created, + expires, + uuid, + issuer, + subject, + mail, + profile +) values ( + ?, + ?, + ?, + ?, + ?, + ?, + ? +); diff --git a/certidude/sql/sqlite/token_tables.sql b/certidude/sql/sqlite/token_tables.sql new file mode 100644 index 0000000..c39387d --- /dev/null +++ b/certidude/sql/sqlite/token_tables.sql @@ -0,0 +1,13 @@ +create table if not exists token ( + id integer primary key autoincrement, + created datetime, + used datetime, + expires datetime, + uuid char(32), + issuer char(30), + subject varchar(30), + mail varchar(128), + profile varchar(10), + + constraint unique_uuid unique(uuid) +) diff --git a/certidude/static/img/ubuntu-01-edit-connections.png b/certidude/static/img/ubuntu-01-edit-connections.png new file mode 100644 index 0000000..2a124f4 Binary files /dev/null and b/certidude/static/img/ubuntu-01-edit-connections.png differ diff --git a/certidude/static/img/ubuntu-02-network-connections.png b/certidude/static/img/ubuntu-02-network-connections.png new file mode 100644 index 0000000..665b887 Binary files /dev/null and b/certidude/static/img/ubuntu-02-network-connections.png differ diff --git a/certidude/static/img/ubuntu-03-import-saved-config.png b/certidude/static/img/ubuntu-03-import-saved-config.png new file mode 100644 index 0000000..fc5a2ef Binary files /dev/null and b/certidude/static/img/ubuntu-03-import-saved-config.png differ diff --git a/certidude/static/img/ubuntu-04-select-file.png b/certidude/static/img/ubuntu-04-select-file.png new file mode 100644 index 0000000..e40727f Binary files /dev/null and b/certidude/static/img/ubuntu-04-select-file.png differ diff --git a/certidude/static/img/ubuntu-05-profile-imported.png b/certidude/static/img/ubuntu-05-profile-imported.png new file mode 100644 index 0000000..4e6f722 Binary files /dev/null and b/certidude/static/img/ubuntu-05-profile-imported.png differ diff --git a/certidude/static/img/ubuntu-06-ipv4-settings.png b/certidude/static/img/ubuntu-06-ipv4-settings.png new file mode 100644 index 0000000..a7a9317 Binary files /dev/null and b/certidude/static/img/ubuntu-06-ipv4-settings.png differ diff --git a/certidude/static/img/ubuntu-07-disable-default-route.png b/certidude/static/img/ubuntu-07-disable-default-route.png new file mode 100644 index 0000000..8da2dab Binary files /dev/null and b/certidude/static/img/ubuntu-07-disable-default-route.png differ diff --git a/certidude/static/img/ubuntu-08-activate-connection.png b/certidude/static/img/ubuntu-08-activate-connection.png new file mode 100644 index 0000000..815533b Binary files /dev/null and b/certidude/static/img/ubuntu-08-activate-connection.png differ diff --git a/certidude/static/img/windows-01-download-openvpn.png b/certidude/static/img/windows-01-download-openvpn.png new file mode 100644 index 0000000..0016834 Binary files /dev/null and b/certidude/static/img/windows-01-download-openvpn.png differ diff --git a/certidude/static/img/windows-02-install-openvpn.png b/certidude/static/img/windows-02-install-openvpn.png new file mode 100644 index 0000000..a484dd4 Binary files /dev/null and b/certidude/static/img/windows-02-install-openvpn.png differ diff --git a/certidude/static/img/windows-03-move-config-file.png b/certidude/static/img/windows-03-move-config-file.png new file mode 100644 index 0000000..1246734 Binary files /dev/null and b/certidude/static/img/windows-03-move-config-file.png differ diff --git a/certidude/static/img/windows-04-connect.png b/certidude/static/img/windows-04-connect.png new file mode 100644 index 0000000..7920383 Binary files /dev/null and b/certidude/static/img/windows-04-connect.png differ diff --git a/certidude/static/img/windows-05-connected.png b/certidude/static/img/windows-05-connected.png new file mode 100644 index 0000000..28aa223 Binary files /dev/null and b/certidude/static/img/windows-05-connected.png differ diff --git a/certidude/static/index.html b/certidude/static/index.html index ba769e8..3f8a85e 100644 --- a/certidude/static/index.html +++ b/certidude/static/index.html @@ -20,20 +20,14 @@ diff --git a/certidude/static/js/certidude.js b/certidude/static/js/certidude.js index c596d05..8a60fe7 100644 --- a/certidude/static/js/certidude.js +++ b/certidude/static/js/certidude.js @@ -1,15 +1,8 @@ 'use strict'; -const KEYWORDS = [ - ["Android", "android"], - ["iPhone", "iphone"], - ["iPad", "ipad"], - ["Ubuntu", "ubuntu"], - ["Fedora", "fedora"], - ["Linux", "linux"], - ["Macintosh", "mac"], -]; +const KEY_SIZE = 2048; +const DEVICE_KEYWORDS = ["Android", "iPhone", "iPad", "Windows", "Ubuntu", "Fedora", "Mac", "Linux"]; jQuery.timeago.settings.allowFuture = true; @@ -17,38 +10,217 @@ function normalizeCommonName(j) { return j.replace("@", "--").split(".").join("-"); // dafuq ?! } +function onShowAll() { + var options = document.querySelectorAll(".option"); + for (i = 0; i < options.length; i++) { + options[i].style.display = "block"; + } +} + +function onKeyGen() { + if (window.navigator.userAgent.indexOf(" Edge/") >= 0) { + $("#enroll .loader-container").hide(); + $("#enroll .edge-broken").show(); + return; + } + + window.keys = forge.pki.rsa.generateKeyPair(KEY_SIZE); + console.info('Key-pair created.'); + + // Device identifier + var dig = forge.md.sha384.create(); + dig.update(window.navigator.userAgent); + + var prefix = "unknown"; + for (i in DEVICE_KEYWORDS) { + var keyword = DEVICE_KEYWORDS[i]; + if (window.navigator.userAgent.indexOf(keyword) >= 0) { + prefix = keyword.toLowerCase(); + break; + } + } + + window.identifier = prefix + "-" + dig.digest().toHex().substring(0, 8); + console.info("Device identifier:", identifier); + + window.common_name = query.subject + "@" + identifier; + + window.csr = forge.pki.createCertificationRequest(); + csr.publicKey = keys.publicKey; + csr.setSubject([{ + name: 'commonName', value: common_name + }]); + + csr.sign(keys.privateKey, forge.md.sha384.create()); + console.info('Certification request created'); + + + $("#enroll .loader-container").hide(); + + var prefix = null; + for (i in DEVICE_KEYWORDS) { + var keyword = DEVICE_KEYWORDS[i]; + if (window.navigator.userAgent.indexOf(keyword) >= 0) { + prefix = keyword.toLowerCase(); + break; + } + } + + if (prefix == null) { + $(".option").show(); + return; + } + + var protocols = query.protocols.split(","); + console.info("Showing snippets for:", protocols); + for (var j = 0; j < protocols.length; j++) { + var options = document.querySelectorAll(".option." + protocols[j] + "." + prefix); + for (i = 0; i < options.length; i++) { + options[i].style.display = "block"; + } + } +} + +function onEnroll(encoding) { + console.info("User agent:", window.navigator.userAgent); + var xhr = new XMLHttpRequest(); + xhr.open('GET', "/api/certificate"); + xhr.onload = function() { + if (xhr.status === 200) { + var ca = forge.pki.certificateFromPem(xhr.responseText); + console.info("Got CA certificate:"); + var xhr2 = new XMLHttpRequest(); + xhr2.open("PUT", "/api/token/?token=" + query.token ); + xhr2.onload = function() { + if (xhr2.status === 200) { + var a = document.createElement("a"); + var cert = forge.pki.certificateFromPem(xhr2.responseText); + console.info("Got signed certificate:", xhr2.responseText); + var p12 = forge.pkcs12.toPkcs12Asn1( + keys.privateKey, [cert, ca], "", {algorithm: '3des'}); + + switch(encoding) { + case 'p12': + var buf = forge.asn1.toDer(p12).getBytes(); + var mimetype = "application/x-pkcs12" + a.download = query.router + ".p12"; + break + case 'sswan': + var buf = JSON.stringify({ + uuid: "a061d140-d3f9-4db7-b2f8-32d6703f4618", + name: identifier, + type: "ikev2-cert", + 'ike-proposal': 'aes256-sha384-prfsha384-modp2048', + 'esp-proposal': 'aes128gcm16-aes128gmac-modp2048', + remote: { addr: query.router }, + local: { p12: forge.util.encode64(forge.asn1.toDer(p12).getBytes()) } + }); + console.info("Buf is:", buf); + var mimetype = "application/vnd.strongswan.profile" + a.download = query.router + ".sswan"; + break + case 'ovpn': + var buf = nunjucks.render('snippets/openvpn-client.conf', { + session: { + authority: { + certificate: { + common_name: "Certidude at " + window.location.hostname, + algorithm: "rsa" + } + }, + service: { + protocols: query.protocols.split(","), + routers: [query.router], + } + }, + key: forge.pki.privateKeyToPem(keys.privateKey), + cert: xhr2.responseText, + ca: xhr.responseText + }); + var mimetype = "application/x-openvpn-profile"; + a.download = query.router + ".ovpn"; + break + case 'mobileconfig': + var p12 = forge.pkcs12.toPkcs12Asn1( + keys.privateKey, [cert, ca], "1234", {algorithm: '3des'}); + var buf = nunjucks.render('snippets/ios.mobileconfig', { + session: { + authority: { + certificate: { + common_name: "Certidude at " + window.location.hostname, + algorithm: "rsa" + } + } + }, + common_name: common_name, + gateway: query.router, + p12: forge.util.encode64(forge.asn1.toDer(p12).getBytes()), + ca: forge.util.encode64(forge.asn1.toDer(forge.pki.certificateToAsn1(ca)).getBytes()) + }); + var mimetype = "application/x-apple-aspen-config"; + a.download = query.router + ".mobileconfig"; + break + } + a.href = "data:" + mimetype + ";base64," + forge.util.encode64(buf); + console.info("Offering bundle for download"); + document.body.appendChild(a); // Firefox needs this! + a.click(); + } else { + if (xhr2.status == 403) { alert("Token used or expired"); } + console.info('Request failed. Returned status of ' + xhr2.status); + try { + var r = JSON.parse(xhr2.responseText); + console.info("Server said: " + r.title); + console.info(r.description); + } catch(e) { + console.info("Server said: " + xhr2.statusText); + } + } + }; + xhr2.send(forge.pki.certificationRequestToPem(csr)); + } + } + xhr.send(); +} + function onHashChanged() { - var query = {}; + window.query = {}; var a = location.hash.substring(1).split('&'); for (var i = 0; i < a.length; i++) { var b = a[i].split('='); query[decodeURIComponent(b[0])] = decodeURIComponent(b[1] || ''); } - if (query.columns) { query.columns = parseInt(query.columns) }; - - if (query.columns < 2 || query.columns > 4) { - query.columns = 2; - } - console.info("Hash is now:", query); if (window.location.protocol != "https:") { $.get("/api/certificate/", function(blob) { $("#view-dashboard").html(env.render('views/insecure.html', { window: window, - authority_name: window.location.hostname, - session: { authority: { certificate: { blob: blob }}} + session: { authority: { + hostname: window.location.hostname, + certificate: { blob: blob }}} })); }); } else { - loadAuthority(query); + if (query.action == "enroll") { + $("#view-dashboard").html(env.render('views/enroll.html')); + var options = document.querySelectorAll(".option"); + for (i = 0; i < options.length; i++) { + options[i].style.display = "none"; + } + setTimeout(onKeyGen, 100); + console.info("Generating key pair..."); + } else { + loadAuthority(query); + } } } -function onTagClicked(tag) { - var cn = $(tag).attr("data-cn"); - var id = $(tag).attr("title"); - var value = $(tag).html(); +function onTagClicked(e) { + e.preventDefault(); + var cn = $(e.target).attr("data-cn"); + var id = $(e.target).attr("title"); + var value = $(e.target).html(); var updated = prompt("Enter new tag or clear to remove the tag", value); if (updated == "") { $(event.target).addClass("disabled"); @@ -57,7 +229,7 @@ function onTagClicked(tag) { url: "/api/signed/" + cn + "/tag/" + id + "/" }); } else if (updated && updated != value) { - $(tag).addClass("disabled"); + $(e.target).addClass("disabled"); $.ajax({ method: "PUT", url: "/api/signed/" + cn + "/tag/" + id + "/", @@ -77,9 +249,10 @@ function onTagClicked(tag) { return false; } -function onNewTagClicked(menu) { - var cn = $(menu).attr("data-cn"); - var key = $(menu).attr("data-key"); +function onNewTagClicked(e) { + e.preventDefault(); + var cn = $(e.target).attr("data-cn"); + var key = $(e.target).attr("data-key"); var value = prompt("Enter new " + key + " tag for " + cn); if (!value) return; if (value.length == 0) return; @@ -101,6 +274,7 @@ function onNewTagClicked(menu) { alert(e); } }); + return false; } function onTagFilterChanged() { @@ -121,6 +295,7 @@ function onLogEntry (e) { message: e.message, severity: e.severity, fresh: e.fresh, + keywords: e.message.toLowerCase().split(/,?[ <>/]+/).join("|") } })); } @@ -270,7 +445,7 @@ function onServerStopped() { } -function onSendToken() { +function onIssueToken() { $.ajax({ method: "POST", url: "/api/token/", @@ -316,20 +491,16 @@ function loadAuthority(query) { console.info("Loaded:", session); $("#login").hide(); - - if (!query.columns) { - query.columns = 2; - } + $("#search").show(); /** * Render authority views **/ $("#view-dashboard").html(env.render('views/authority.html', { session: session, - window: window, - columns: query.columns, - column_width: 12 / query.columns, - authority_name: window.location.hostname })); + window: window + })); + $("time").timeago(); if (session.authority) { $("#log input").each(function(i, e) { @@ -414,12 +585,8 @@ function loadAuthority(query) { $("#search").on("keyup", function() { if (window.searchTimeout) { clearTimeout(window.searchTimeout); } window.searchTimeout = setTimeout(function() { $(window).trigger("search"); }, 500); - console.info("Setting timeout", window.searchTimeout); - }); - console.log("Features enabled:", session.features); - if (session.request_submission_allowed) { $("#request_submit").click(function() { $(this).addClass("busy"); @@ -442,11 +609,9 @@ function loadAuthority(query) { alert(e); } }); - }); } - $("nav .nav-link.dashboard").removeClass("disabled").click(function() { $("#column-requests").show(); $("#column-signed").show(); @@ -458,31 +623,36 @@ function loadAuthority(query) { * Fetch log entries */ if (session.features.logging) { - if (query.columns == 4) { + if ($("#column-log:visible").length) { loadLog(); - } else { - $("nav .nav-link.log").removeClass("disabled").click(function() { - $("#column-requests").show(); - $("#column-signed").show(); - $("#column-revoked").show(); - $("#column-log").hide(); - }); } + $("nav .nav-link.log").removeClass("disabled").click(function() { + loadLog(); + $("#column-requests").show(); + $("#column-signed").show(); + $("#column-revoked").show(); + $("#column-log").hide(); + }); + } else { + console.info("Log disabled"); } } }); } function loadLog() { - if (window.log_initialized) return; + if (window.log_initialized) { + console.info("Log already loaded"); + return; + } + console.info("Loading log..."); window.log_initialized = true; $.ajax({ method: "GET", - url: "/api/log/", + url: "/api/log/?limit=100", dataType: "json", success: function(entries, status, xhr) { console.info("Got", entries.length, "log entries"); - console.info("j=", entries.length-1); for (var j = entries.length-1; j--; ) { onLogEntry(entries[j]); }; diff --git a/certidude/static/snippets/ansible-site.yml b/certidude/static/snippets/ansible-site.yml new file mode 100644 index 0000000..5963939 --- /dev/null +++ b/certidude/static/snippets/ansible-site.yml @@ -0,0 +1,15 @@ +- hosts: {% for router in session.service.routers %} + {{ router }}{% endfor %} + + roles: + - role: certidude + authority_name: {{ session.authority.hostname }} + + - role: ipsec_mesh + mesh_name: mymesh + authority_name: {{ session.authority.hostname }} + ike: aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! + esp: aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! + auto: start + nodes:{% for router in session.service.routers %} + {{ router }}: 172.27.{{ loop.index }}.0/24{% endfor %} diff --git a/certidude/static/snippets/certidude-client.sh b/certidude/static/snippets/certidude-client.sh index 5da9069..2c9b29f 100644 --- a/certidude/static/snippets/certidude-client.sh +++ b/certidude/static/snippets/certidude-client.sh @@ -1,25 +1,24 @@ pip3 install git+https://github.com/laurivosandi/certidude/ mkdir -p /etc/certidude/{client.conf.d,services.conf.d} -cat << EOF > /etc/certidude/client.conf.d/{{ authority_name }}.conf -[{{ authority_name }}] + +cat << \EOF > /etc/certidude/client.conf.d/{{ session.authority.hostname }}.conf +[{{ session.authority.hostname }}] trigger = interface up common name = $HOSTNAME system wide = true EOF -cat << EOF > /etc/certidude/services.conf.d/{{ authority_name }}.conf -{% for router in session.service.routers %}{% if "ikev2" in session.service.protocols %} +cat << EOF > /etc/certidude/services.conf.d/{{ session.authority.hostname }}.conf{% for router in session.service.routers %}{% if "ikev2" in session.service.protocols %} [IPSec to {{ router }}] -authority = {{ authority_name }} +authority = {{ session.authority.hostname }} service = network-manager/strongswan remote = {{ router }} {% endif %}{% if "openvpn" in session.service.protocols %} [OpenVPN to {{ router }}] -authority = {{ authority_name }} +authority = {{ session.authority.hostname }} service = network-manager/openvpn remote = {{ router }} -{% endif %}{% endfor %} -EOF +{% endif %}{% endfor %}EOF certidude enroll diff --git a/certidude/static/snippets/gateway-updown.sh b/certidude/static/snippets/gateway-updown.sh index 6d17e61..a89d3fb 100644 --- a/certidude/static/snippets/gateway-updown.sh +++ b/certidude/static/snippets/gateway-updown.sh @@ -1,8 +1,8 @@ # Create VPN gateway up/down script for reporting client IP addresses to CA -cat <<\EOF > /etc/certidude/authority/{{ authority_name }}/updown +cat <<\EOF > /etc/certidude/authority/{{ session.authority.hostname }}/updown #!/bin/sh -CURL="curl -m 3 -f --key /etc/certidude/authority/{{ authority_name }}/host_key.pem --cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem --cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem https://{{ authority_name }}:8443/api/lease/" +CURL="curl -m 3 -f --key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem --cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem https://{{ session.authority.hostname }}:8443/api/lease/" case $PLUTO_VERB in up-client) $CURL --data-urlencode "outer_address=$PLUTO_PEER" --data-urlencode "inner_address=$PLUTO_PEER_SOURCEIP" --data-urlencode "client=$PLUTO_PEER_ID" ;; @@ -15,5 +15,5 @@ case $script_type in esac EOF -chmod +x /etc/certidude/authority/{{ authority_name }}/updown +chmod +x /etc/certidude/authority/{{ session.authority.hostname }}/updown diff --git a/certidude/static/snippets/ios.mobileconfig b/certidude/static/snippets/ios.mobileconfig new file mode 100644 index 0000000..77d3f61 --- /dev/null +++ b/certidude/static/snippets/ios.mobileconfig @@ -0,0 +1,97 @@ + + + + + PayloadDisplayName + {{ gateway }} + + PayloadIdentifier + org.example.vpn2 + PayloadUUID + 9f93912b-5fd2-4455-99fd-13b9a47b4581 + PayloadType + Configuration + PayloadVersion + 1 + PayloadContent + + + PayloadIdentifier + org.example.vpn2.conf1 + PayloadUUID + 29e4456d-3f03-4f15-b46f-4225d89465b7 + PayloadType + com.apple.vpn.managed + PayloadVersion + 1 + UserDefinedName + {{ gateway }} + VPNType + IKEv2 + IKEv2 + + RemoteAddress + {{ gateway }} + RemoteIdentifier + {{ gateway }} + LocalIdentifier + {{ common_name }} + ServerCertificateIssuerCommonName + {{ session.authority.certificate.common_name }} + ServerCertificateCommonName + {{ gateway }} + AuthenticationMethod + Certificate + IKESecurityAssociationParameters + + EncryptionAlgorithm + AES-256 + IntegrityAlgorithm + SHA2-384 + DiffieHellmanGroup + 14 + + ChildSecurityAssociationParameters + + EncryptionAlgorithm + AES-128-GCM + IntegrityAlgorithm + SHA2-256 + DiffieHellmanGroup + 14 + + EnablePFS + 1 + PayloadCertificateUUID + d60488c6-328e-4944-9c8d-61db8095c865 + + + + PayloadIdentifier + ee.k-space.ca2.client + PayloadUUID + d60488c6-328e-4944-9c8d-61db8095c865 + PayloadType + com.apple.security.pkcs12 + PayloadVersion + 1 + PayloadContent + {{ p12 }} + + + PayloadIdentifier + org.example.ca + PayloadUUID + 64988b2c-33e0-4adf-a432-6fbcae543408 + PayloadType + com.apple.security.root + PayloadVersion + 1 + + PayloadContent + {{ ca }} + + + + + diff --git a/certidude/static/snippets/openvpn-client.conf b/certidude/static/snippets/openvpn-client.conf new file mode 100644 index 0000000..c6d2e3c --- /dev/null +++ b/certidude/static/snippets/openvpn-client.conf @@ -0,0 +1,35 @@ +client +nobind{% for router in session.service.routers %} +remote {{ router }}{% endfor %} +proto tcp-client +port 443 +tls-version-min 1.2 +tls-cipher TLS-{% if session.authority.certificate.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-WITH-AES-256-GCM-SHA384 +cipher AES-128-GCM +auth SHA384 +mute-replay-warnings +reneg-sec 0 +remote-cert-tls server +dev tun +persist-tun +persist-key +{% if ca %} + +{{ ca }} + +{% else %}ca /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem{% endif %} +{% if key %} + +{{ key }} + +{% else %}key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem{% endif %} +{% if cert %} + +{{ cert }} + +{% else %}cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem{% endif %} + +# To enable dynamic DNS server update on Ubuntu, uncomment these +#script-security 2 +#up /etc/openvpn/update-resolv-conf +#down /etc/openvpn/update-resolv-conf diff --git a/certidude/static/snippets/openvpn-client.sh b/certidude/static/snippets/openvpn-client.sh index e888987..ba25878 100644 --- a/certidude/static/snippets/openvpn-client.sh +++ b/certidude/static/snippets/openvpn-client.sh @@ -2,26 +2,19 @@ which apt && apt install openvpn which dnf && dnf install openvpn -cat > /etc/openvpn/{{ authority_name }}.conf << EOF -client -nobind -{% for router in session.service.routers %} -remote {{ router }} 1194 udp -remote {{ router }} 443 tcp-client -{% endfor %} -tls-version-min 1.2 -tls-cipher TLS-{% if session.authority.certificate.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-WITH-AES-128-GCM-SHA384 -cipher AES-128-GCM -auth SHA384 -mute-replay-warnings -reneg-sec 0 -remote-cert-tls server -dev tun -persist-tun -persist-key -ca /etc/certidude/authority/{{ authority_name }}/ca_cert.pem -key /etc/certidude/authority/{{ authority_name }}/host_key.pem -cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem +# Create OpenVPN configuration file +cat > /etc/openvpn/{{ session.authority.hostname }}.conf << EOF +{% include "snippets/openvpn-client.conf" %} EOF +# Restart OpenVPN service systemctl restart openvpn +{# + +Some notes: + +- Ubuntu 16.04 ships OpenVPN 2.3 which doesn't support AES-128-GCM +- NetworkManager's OpenVPN profile importer doesn't understand multiple remotes +- Tunnelblick and OpenVPN Connect apps don't have a method to update CRL + +#} diff --git a/certidude/static/snippets/openwrt-openvpn.sh b/certidude/static/snippets/openwrt-openvpn.sh index ddca731..8767478 100644 --- a/certidude/static/snippets/openwrt-openvpn.sh +++ b/certidude/static/snippets/openwrt-openvpn.sh @@ -65,9 +65,9 @@ for section in s2c_tcp s2c_udp; do # Common paths uci set openvpn.$section.script_security=2 uci set openvpn.$section.client_connect='/etc/certidude/updown' -uci set openvpn.$section.key='/etc/certidude/authority/{{ authority_name }}/host_key.pem' -uci set openvpn.$section.cert='/etc/certidude/authority/{{ authority_name }}/host_cert.pem' -uci set openvpn.$section.ca='/etc/certidude/authority/{{ authority_name }}/ca_cert.pem' +uci set openvpn.$section.key='/etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem' +uci set openvpn.$section.cert='/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem' +uci set openvpn.$section.ca='/etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem' {% if session.authority.certificate.algorithm != "ec" %}uci set openvpn.$section.dh='/etc/certidude/dh.pem'{% endif %} uci set openvpn.$section.enabled=1 diff --git a/certidude/static/snippets/renew.sh b/certidude/static/snippets/renew.sh index 233e698..aea67b1 100644 --- a/certidude/static/snippets/renew.sh +++ b/certidude/static/snippets/renew.sh @@ -1,7 +1,7 @@ -curl -f -L -H "Content-type: application/pkcs10" \ - --cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \ - --key /etc/certidude/authority/{{ authority_name }}/host_key.pem \ - --cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem \ - --data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \ - -o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \ - 'https://{{ authority_name }}:8443/api/request/?wait=yes' +curl --cert-status -f -L -H "Content-type: application/pkcs10" \ + --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \ + --key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \ + --cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \ + --data-binary @/etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \ + -o /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \ + 'https://{{ session.authority.hostname }}:8443/api/request/?wait=yes' diff --git a/certidude/static/snippets/request-client.sh b/certidude/static/snippets/request-client.sh index e28b875..d9791e5 100644 --- a/certidude/static/snippets/request-client.sh +++ b/certidude/static/snippets/request-client.sh @@ -1,15 +1,11 @@ +# Use short hostname as common name test -e /sbin/uci && NAME=$(uci get system.@system[0].hostname) test -e /bin/hostname && NAME=$(hostname) test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname) -{% include "snippets/update-trust.sh" %} - {% include "snippets/request-common.sh" %} - -curl -f -L -H "Content-type: application/pkcs10" \ ---data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \ --o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \ -'http://{{ authority_name }}/api/request/?wait=yes&autosign=yes' - - - +# Submit CSR and save signed certificate +curl --cert-status -f -L -H "Content-type: application/pkcs10" \ + --data-binary @/etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \ + -o /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \ + 'http://{{ session.authority.hostname }}/api/request/?wait=yes&autosign=yes' diff --git a/certidude/static/snippets/request-common.sh b/certidude/static/snippets/request-common.sh index 0d49112..8f8da47 100644 --- a/certidude/static/snippets/request-common.sh +++ b/certidude/static/snippets/request-common.sh @@ -1,14 +1,16 @@ -echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ authority_name }}/ca_cert.pem | md5sum -c \ - || rm -fv /etc/certidude/authority/{{ authority_name }}/*.pem +# Delete CA certificate if checksum doesn't match +echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem | md5sum -c \ + || rm -fv /etc/certidude/authority/{{ session.authority.hostname }}/*.pem {% include "snippets/store-authority.sh" %} -test -e /etc/certidude/authority/{{ authority_name }}/host_key.pem \ +{% include "snippets/update-trust.sh" %} +# Generate private key +test -e /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \ || {% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \ - -out /etc/certidude/authority/{{ authority_name }}/host_key.pem{% else %}openssl genrsa \ - -out /etc/certidude/authority/{{ authority_name }}/host_key.pem 2048{% endif %} -test -e /etc/certidude/authority/{{ authority_name }}/host_req.pem \ + -out /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem{% else %}openssl genrsa \ + -out /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem 2048{% endif %} +test -e /etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \ || openssl req -new -sha384 -subj "/CN=$NAME" \ - -key /etc/certidude/authority/{{ authority_name }}/host_key.pem \ - -out /etc/certidude/authority/{{ authority_name }}/host_req.pem + -key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \ + -out /etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem echo "If CSR submission fails, you can copy paste it to Certidude:" -cat /etc/certidude/authority/{{ authority_name }}/host_req.pem - +cat /etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem diff --git a/certidude/static/snippets/request-server.sh b/certidude/static/snippets/request-server.sh index f2d66b8..d7e6a80 100644 --- a/certidude/static/snippets/request-server.sh +++ b/certidude/static/snippets/request-server.sh @@ -1,13 +1,12 @@ +# Use fully qualified name test -e /sbin/uci && NAME=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs) test -e /bin/hostname && NAME=$(hostname -f) test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname) -{% include "snippets/update-trust.sh" %} - {% include "snippets/request-common.sh" %} - -curl -f -L -H "Content-type: application/pkcs10" \ - --cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \ - --data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \ - -o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \ - 'https://{{ authority_name }}:8443/api/request/?wait=yes' +# Submit CSR and save signed certificate +curl --cert-status -f -L -H "Content-type: application/pkcs10" \ + --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \ + --data-binary @/etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \ + -o /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \ + 'https://{{ session.authority.hostname }}:8443/api/request/?wait=yes' diff --git a/certidude/static/snippets/store-authority.sh b/certidude/static/snippets/store-authority.sh index c417957..13983f4 100644 --- a/certidude/static/snippets/store-authority.sh +++ b/certidude/static/snippets/store-authority.sh @@ -1,5 +1,5 @@ -mkdir -p /etc/certidude/authority/{{ authority_name }}/ -test -e /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \ - || cat << EOF > /etc/certidude/authority/{{ authority_name }}/ca_cert.pem +# Save CA certificate +mkdir -p /etc/certidude/authority/{{ session.authority.hostname }}/ +test -e /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \ + || cat << EOF > /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem {{ session.authority.certificate.blob }}EOF - diff --git a/certidude/static/snippets/strongswan-client.sh b/certidude/static/snippets/strongswan-client.sh index 97b1932..2c3bd43 100644 --- a/certidude/static/snippets/strongswan-client.sh +++ b/certidude/static/snippets/strongswan-client.sh @@ -1,10 +1,10 @@ cat > /etc/ipsec.conf << EOF +config setup + strictcrlpolicy=yes -ca {{ authority_name }} +ca {{ session.authority.hostname }} auto=add - cacert=/etc/certidude/authority/{{ authority_name }}/ca_cert.pem -{% if session.features.crl %} crluri=http://{{ authority_name }}/api/revoked/{% endif %} -{% if session.features.ocsp %} ocspuri=http://{{ authority_name }}/api/ocsp/{% endif %} + cacert=/etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem conn client-to-site auto=start @@ -12,7 +12,7 @@ conn client-to-site rightsubnet=0.0.0.0/0 rightca="{{ session.authority.certificate.distinguished_name }}" left=%defaultroute - leftcert=/etc/certidude/authority/{{ authority_name }}/host_cert.pem + leftcert=/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem leftsourceip=%config leftca="{{ session.authority.certificate.distinguished_name }}" keyexchange=ikev2 @@ -21,9 +21,8 @@ conn client-to-site closeaction=restart ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! - EOF -echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ authority_name }}.pem" > /etc/ipsec.secrets +echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ session.authority.hostname }}.pem" > /etc/ipsec.secrets -ipsec restart +ipsec restart apparmor diff --git a/certidude/static/snippets/strongswan-patching.sh b/certidude/static/snippets/strongswan-patching.sh index 1bed2a7..6538b75 100644 --- a/certidude/static/snippets/strongswan-patching.sh +++ b/certidude/static/snippets/strongswan-patching.sh @@ -6,11 +6,12 @@ test -e /etc/strongswan && test -e /etc/ipsec.d || ln -s strongswan/ipsec.d /etc test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets # Set SELinux context -chcon --type=home_cert_t /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /etc/ipsec.d/cacerts/{{ authority_name }}.pem -chcon --type=home_cert_t /etc/certidude/authority/{{ authority_name }}/host_cert.pem /etc/ipsec.d/certs/{{ authority_name }}.pem -chcon --type=home_cert_t /etc/certidude/authority/{{ authority_name }}/host_key.pem /etc/ipsec.d/private/{{ authority_name }}.pem +chcon --type=home_cert_t /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem /etc/ipsec.d/cacerts/{{ session.authority.hostname }}.pem +chcon --type=home_cert_t /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem /etc/ipsec.d/certs/{{ session.authority.hostname }}.pem +chcon --type=home_cert_t /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem /etc/ipsec.d/private/{{ session.authority.hostname }}.pem # Patch AppArmor cat << EOF > /etc/apparmor.d/local/usr.lib.ipsec.charon -/etc/certidude/authority/** +/etc/certidude/authority/** r, EOF +systemctl restart diff --git a/certidude/static/snippets/strongswan-server.sh b/certidude/static/snippets/strongswan-server.sh index 76d9c6b..6bce914 100644 --- a/certidude/static/snippets/strongswan-server.sh +++ b/certidude/static/snippets/strongswan-server.sh @@ -4,19 +4,17 @@ config setup strictcrlpolicy=yes uniqueids=yes -ca {{ authority_name }} +ca {{ session.authority.hostname }} auto=add - cacert=/etc/certidude/authority/{{ authority_name }}/ca_cert.pem -{% if session.features.crl %} crluri=http://{{ authority_name }}/api/revoked/{% endif %} -{% if session.features.ocsp %} ocspuri=http://{{ authority_name }}/api/ocsp/{% endif %} + cacert=/etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem -conn default-{{ authority_name }} +conn default-{{ session.authority.hostname }} ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! left=$(uci get network.wan.ipaddr) # Bind to this IP address leftid={{ session.service.routers | first }} - leftupdown=/etc/certidude/authority/{{ authority_name }}/updown - leftcert=/etc/certidude/authority/{{ authority_name }}/host_cert.pem + leftupdown=/etc/certidude/authority/{{ session.authority.hostname }}/updown + leftcert=/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem leftsubnet=$(uci get network.lan.ipaddr | cut -d . -f 1-3).0/24 # Subnets pushed to roadwarriors leftdns=$(uci get network.lan.ipaddr) # IP of DNS server advertised to roadwarriors leftca="{{ session.authority.certificate.distinguished_name }}" @@ -27,15 +25,15 @@ conn default-{{ authority_name }} conn site-to-clients auto=add - also=default-{{ authority_name }} + also=default-{{ session.authority.hostname }} conn site-to-client1 auto=ignore - also=default-{{ authority_name }} + also=default-{{ session.authority.hostname }} rightid="CN=*, OU=IP Camera, O=*, DC=*, DC=*, DC=*" rightsourceip=172.21.0.1 EOF -echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ authority_name }}/host_key.pem" > /etc/ipsec.secrets +echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem" > /etc/ipsec.secrets diff --git a/certidude/static/snippets/update-trust.sh b/certidude/static/snippets/update-trust.sh index f7d3360..d78545a 100644 --- a/certidude/static/snippets/update-trust.sh +++ b/certidude/static/snippets/update-trust.sh @@ -1,7 +1,9 @@ +# Insert into Fedora trust store. Applies to curl, Firefox, Chrome, Chromium test -e /etc/pki/ca-trust/source/anchors \ - && ln -s /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /etc/pki/ca-trust/source/anchors/{{ authority_name }} \ + && ln -s /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem /etc/pki/ca-trust/source/anchors/{{ session.authority.hostname }} \ && update-ca-trust -test -e /usr/local/share/ca-certificates/ \ - && ln -s /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /usr/local/share/ca-certificates/{{ authority_name }}.crt \ - && update-ca-certificates +# Insert into Ubuntu trust store, only applies to curl +test -e /usr/local/share/ca-certificates/ \ + && ln -s /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem /usr/local/share/ca-certificates/{{ session.authority.hostname }}.crt \ + && update-ca-certificates diff --git a/certidude/static/snippets/windows.ps1 b/certidude/static/snippets/windows.ps1 index 95c68aa..c797ad7 100644 --- a/certidude/static/snippets/windows.ps1 +++ b/certidude/static/snippets/windows.ps1 @@ -1,7 +1,6 @@ # Install CA certificate @" -{{ session.authority.certificate.blob }} -"@ | Out-File ca_cert.pem +{{ session.authority.certificate.blob }}"@ | Out-File ca_cert.pem {% if session.authority.certificate.algorithm == "ec" %} Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root {% else %} @@ -25,25 +24,25 @@ KeyAlgorithm = ECDSA_P384 KeyLength = 2048 {% endif %}"@ | Out-File req.inf C:\Windows\system32\certreq.exe -new -f -q req.inf host_csr.pem -Invoke-WebRequest -TimeoutSec 900 -Uri 'https://{{ authority_name }}:8443/api/request/?wait=yes&autosign=yes' -InFile host_csr.pem -ContentType application/pkcs10 -Method POST -MaximumRedirection 3 -OutFile host_cert.pem +Invoke-WebRequest -TimeoutSec 900 -Uri 'https://{{ session.authority.hostname }}:8443/api/request/?wait=yes&autosign=yes' -InFile host_csr.pem -ContentType application/pkcs10 -Method POST -MaximumRedirection 3 -OutFile host_cert.pem # Import certificate {% if session.authority.certificate.algorithm == "ec" %}Import-Certificate -FilePath host_cert.pem -CertStoreLocation Cert:\LocalMachine\My {% else %}C:\Windows\system32\certutil.exe -addstore My host_cert.pem {% endif %} -# Set up IPSec VPN tunnel -Remove-VpnConnection -AllUserConnection -Force k-space + +{% for router in session.service.routers %} +# Set up IPSec VPN tunnel to {{ router }} +Remove-VpnConnection -AllUserConnection -Force "IPSec to {{ router }}" Add-VpnConnection ` - -Name k-space ` - -ServerAddress guests.k-space.ee ` + -Name "IPSec to {{ router }}" ` + -ServerAddress {{ router }} ` -AuthenticationMethod MachineCertificate ` -SplitTunneling ` -TunnelType ikev2 ` -PassThru -AllUserConnection - -# Security hardening Set-VpnConnectionIPsecConfiguration ` - -ConnectionName k-space ` + -ConnectionName "IPSec to {{ router }}" ` -AuthenticationTransformConstants GCMAES128 ` -CipherTransformConstants GCMAES128 ` -EncryptionMethod AES256 ` @@ -51,6 +50,8 @@ Set-VpnConnectionIPsecConfiguration ` -DHGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}Group14{% endif %} ` -PfsGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}PFS2048{% endif %} ` -PassThru -AllUserConnection -Force +{% endfor %} + {# AuthenticationTransformConstants - ESP integrity algorithm, one of: None MD596 SHA196 SHA256128 GCMAES128 GCMAES192 GCMAES256 CipherTransformConstants - ESP symmetric cipher, one of: DES DES3 AES128 AES192 AES256 GCMAES128 GCMAES192 GCMAES256 diff --git a/certidude/static/views/authority.html b/certidude/static/views/authority.html index 28d4acc..3c3544a 100644 --- a/certidude/static/views/authority.html +++ b/certidude/static/views/authority.html @@ -5,51 +5,97 @@ -
-
@@ -133,10 +178,10 @@
-
+

Signed certificates

-

Authority administration allowed for - {% for user in session.authority.admin_users %}{{ user.given_name }} {{user.surname }}{% if not loop.last %}, {% endif %}{% endfor %} from {% if "0.0.0.0/0" in session.authority.admin_subnets %}anywhere{% else %} - {% for subnet in session.authority.admin_subnets %}{{ subnet }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}. +

Authority administration + {% if session.authority.certificate.organization %}of {{ session.authority.certificate.organization }}{% endif %} + allowed for + {% for user in session.authorization.admin_users %}{{ user.given_name }} {{user.surname }}{% if not loop.last %}, {% endif %}{% endfor %} from {% if "0.0.0.0/0" in session.authorization.admin_subnets %}anywhere{% else %} + {% for subnet in session.authorization.admin_subnets %}{{ subnet }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}. + Authority valid from + + until + . Authority certificate can be downloaded from here. Following certificates have been signed:

@@ -159,7 +210,7 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/ {% endfor %}
-
+
{% if session.authority %} {% if session.features.token %}

Tokens

@@ -174,51 +225,89 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/ - +

+ +

Issued tokens:

+
    + {% for token in session.authority.tokens %} +
  • + {{ token.subject }} + {% if token.issuer %}{% if token.issuer != token.subject %}by {{ token.issuer }}{% else %}by himself{% endif %}{% else %}via shell{% endif %}, + expires + +
  • + {% endfor %} +
+
{% endif %} -

Pending requests

+ {% if session.authorization.request_subnets %} +

Pending requests

-

Use Certidude client to apply for a certificate. +

Use Certidude client to apply for a certificate. - {% if not session.authority.request_subnets %} - Request submission disabled. - {% elif "0.0.0.0/0" in session.authority.request_subnets %} - Request submission is enabled. - {% else %} - Request submission allowed from - {% for subnet in session.authority.request_subnets %} - {{ subnet }}{% if not loop.last %}, {% endif %} - {% endfor %}. - {% endif %} + {% if not session.authorization.request_subnets %} + Request submission disabled. + {% elif "0.0.0.0/0" in session.authorization.request_subnets %} + Request submission is enabled. + {% else %} + Request submission allowed from + {% for subnet in session.authorization.request_subnets %} + {{ subnet }}{% if not loop.last %}, {% endif %} + {% endfor %}. + {% endif %} - See here for more information on manual signing request upload. + See here for more information on manual signing request upload. - {% if session.authority.autosign_subnets %} - {% if "0.0.0.0/0" in session.authority.autosign_subnets %} - All requests are automatically signed. + {% if session.authorization.autosign_subnets %} + {% if "0.0.0.0/0" in session.authorization.autosign_subnets %} + All requests are automatically signed. + {% else %} + Requests from + {% for subnet in session.authorization.autosign_subnets %} + {{ subnet }}{% if not loop.last %}, {% endif %} + {% endfor %} + are automatically signed. + {% endif %} + {% endif %} + + {% if session.authorization.scep_subnets %} + To enroll via SCEP from + {% if "0.0.0.0/0" in session.authorization.scep_subnets %} + anywhere {% else %} - Requests from - {% for subnet in session.authority.autosign_subnets %} - {{ subnet }}{% if not loop.last %}, {% endif %} - {% endfor %} - are automatically signed. + {% for subnet in session.authorization.scep_subnets %} + {{ subnet }}{% if not loop.last %}, {% endif %} + {% endfor %} {% endif %} + use http://{{ session.authority.hostname }}/cgi-bin/pkiclient.exe as the enrollment URL. + {% endif %} + +

+
+ {% for request in session.authority.requests | sort(attribute="submitted", reverse=true) %} + {% include "views/request.html" %} + {% endfor %} +
{% endif %} -

-
- {% for request in session.authority.requests | sort(attribute="submitted", reverse=true) %} - {% include "views/request.html" %} - {% endfor %} -
- {% if columns >= 3 %} + + {% if session.builder.profiles %} +

LEDE imagebuilder

+

Hit a link to generate machine specific image. Note that this might take couple minutes to finish.

+
    + {% for name, title, filename in session.builder.profiles %} +
  • {{ title }}
  • + {% endfor %} +
+ {% endif %} +
-
- {% endif %} +
+

Revoked certificates

Following certificates have been revoked{% if session.features.crl %}, for more information click here{% endif %}.

@@ -227,7 +316,7 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/ {% include "views/revoked.html" %} {% endfor %}
-
+

Loading logs, this might take a while...

@@ -235,14 +324,15 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/
diff --git a/certidude/static/views/enroll.html b/certidude/static/views/enroll.html new file mode 100644 index 0000000..e715d80 --- /dev/null +++ b/certidude/static/views/enroll.html @@ -0,0 +1,238 @@ + + + + +
+
+
+

Generating RSA keypair, this will take a while...

+
+ + + +
+
+
+

Ubuntu 16.04+

+

Install OpenVPN plugin for NetworkManager by executing following two command in the terminal: + +

# Ubuntu 16.04 ships with older OpenVPN 2.3, to support newer ciphers add OpenVPN's repo
+if [ $(lsb_relase -cs) == "xenial" ]; then
+  wget -O - https://swupdate.openvpn.net/repos/repo-public.gpg|apt-key add -
+  echo "deb http://build.openvpn.net/debian/openvpn/release/2.4 xenial main" > /etc/apt/sources.list.d/openvpn-aptrepo.list
+  apt update
+  apt install openvpn
+fi
+
+sudo apt install -y network-manager-openvpn-gnome
+sudo systemctl restart network-manager
+
+ +

+ Fetch OpenVPN profile + +

+ +
+

Open up network connections:

+

+

Hit Add button:

+

+

Select Import a saved VPN configuration...:

+

+

Select downloaded file:

+

+

Once profile is successfully imported following dialog appears:

+

+

By default all traffic is routed via VPN gateway, route only intranet subnets to the gateway select Routes... under IPv4 Settings:

+

+

Check Use this connection only for resources on its network:

+

+

To activate the connection select it under VPN Connections:

+

+
+
+
+
+ + +
+
+
+

Fedora

+

Install OpenVPN plugin for NetworkManager by running following two commands:

+
dnf install NetworkManager-openvpn-gnome
+systemctl restart NetworkManager
+ Right click in the NetworkManager icon, select network settings. Hit the + button and select Import from file..., select the downloaded .ovpn file. + Remove the .ovpn file from the Downloads folder.

+ Fetch OpenVPN profile +
+
+
+ +
+
+
+

Windows

+

+ Import PKCS#12 container to your machine trust store. + Import VPN connection profile by moving the downloaded .pbk file to +

%userprofile%\AppData\Roaming\Microsoft\Network\Connections\PBK
+ or +
C:\ProgramData\Microsoft\Network\Connections\Pbk

+ Fetch PKCS#12 container + Fetch VPN profile +
+
+
+ +
+
+
+

Windows

+

+ Install OpenVPN community edition client. + Move the downloaded .ovpn file to C:\Program Files\OpenVPN\config and + right click in the system tray on OpenVPN icon and select Connect from the menu. + For finishing touch adjust the file permissions so only local + administrator can read that file, remove regular user access to the file. +

+ Get OpenVPN community edition + Fetch OpenVPN profile + + +
+

Download OpenVPN from the link supplied above:

+

+ +

Install OpenVPN:

+

+ +

Move the configuraiton file downloaded from the second button above:

+

+ +

Connect from system tray:

+

+ +

Connection is successfully configured:

+

+
+
+
+
+ +
+
+
+

Mac OS X

+

Download Tunnelblick. Tap on the button above and import the profile.

+ Get Tunnelblick + Fetch OpenVPN profile +
+
+
+ +
+
+
+

iPhone/iPad

+

Install OpenVPN Connect app, tap on the button below.

+ Get OpenVPN Connect app + Fetch OpenVPN profile +
+
+
+ +
+
+
+

iPhone/iPad

+

+ Tap the button below, you'll be prompted about configuration profile, tap Allow. + Hit Install in the top-right corner. + Enter your passcode to unlock trust store. + Tap Install and confirm by hitting Install. + Where password for the certificate is prompted, enter 1234. + Hit Done. Go to Settings, open VPN submenu and tap on the VPN profile to connect. +

+ Fetch VPN profile +
+
+
+ +
+
+
+

Mac OS X

+

+ Click on the button below, you'll be prompted about configuration profile, tap Allow. + Hit Install in the top-right corner. + Enter your passcode to unlock trust store. + Tap Install and confirm by hitting Install. + Where password for the certificate is prompted, enter 1234. + Hit Done. Go to Settings, open VPN submenu and tap on the VPN profile to connect. +

+ Fetch VPN profile +
+
+
+ +
+
+
+

Android

+

Intall OpenVPN Connect app on your device. + Tap on the downloaded .ovpn file, OpenVPN Connect should prompt for import. + Hit Accept and then Connect. + Remember to delete any remaining .ovpn files under the Downloads. +

+ Get OpenVPN Connect app + Fetch OpenVPN profile +
+
+
+ +
+
+
+

Android

+

+ Install strongSwan Client app on your device. + Tap on the downloaded .sswan file, StrongSwan Client should prompt for import. + Hit Import certificate from VPN profile and then Import in the top-right corner. + Remember to delete any remaining .sswan files under the Downloads. +

+ Get strongSwan VPN Client app + Fetch StrongSwan profile +
+
+
+ + +
diff --git a/certidude/static/views/insecure.html b/certidude/static/views/insecure.html index 5a13985..fb7c872 100644 --- a/certidude/static/views/insecure.html +++ b/certidude/static/views/insecure.html @@ -1,5 +1,5 @@

You're viewing this page over insecure channel. - You can give it a try and connect over HTTPS, + You can give it a try and connect over HTTPS, if that succeeds all subsequents accesses of this page will go over HTTPS.

diff --git a/certidude/static/views/logentry.html b/certidude/static/views/logentry.html index be126fb..af85363 100644 --- a/certidude/static/views/logentry.html +++ b/certidude/static/views/logentry.html @@ -1,4 +1,4 @@ -

  • +
  • {{ entry.message }} diff --git a/certidude/static/views/request.html b/certidude/static/views/request.html index 86689b0..4b44222 100644 --- a/certidude/static/views/request.html +++ b/certidude/static/views/request.html @@ -40,8 +40,8 @@

    Use following to fetch the signing request:

    -
    wget http://{{ window.location.hostname }}/api/request/{{ request.common_name }}/
    -curl http://{{ window.location.hostname }}/api/request/{{ request.common_name }}/ \
    +        
    wget http://{{ session.authority.hostname }}/api/request/{{ request.common_name }}/
    +curl http://{{ session.authority.hostname }}/api/request/{{ request.common_name }}/ \
       | openssl req -text -noout
    diff --git a/certidude/static/views/revoked.html b/certidude/static/views/revoked.html index 36c2b0d..ea534dc 100644 --- a/certidude/static/views/revoked.html +++ b/certidude/static/views/revoked.html @@ -29,15 +29,15 @@

    To fetch certificate:

    -
    wget http://{{ window.location.hostname }}/api/revoked/{{ certificate.serial }}/
    -curl http://{{ window.location.hostname }}/api/revoked/{{ certificate.serial }}/ \
    +          
    wget http://{{ session.authority.hostname }}/api/revoked/{{ certificate.serial }}/
    +curl http://{{ session.authority.hostname }}/api/revoked/{{ certificate.serial }}/ \
       | openssl x509 -text -noout

    To perform online certificate status request

    -
    curl http://{{ window.location.hostname }}/api/certificate/ > session.pem
    +        
    curl http://{{ session.authority.hostname }}/api/certificate/ > session.pem
     openssl ocsp -issuer session.pem -CAfile session.pem \
    -  -url http://{{ window.location.hostname }}/api/ocsp/ \
    +  -url http://{{ session.authority.hostname }}/api/ocsp/ \
       -serial 0x{{ certificate.serial }}

    diff --git a/certidude/static/views/signed.html b/certidude/static/views/signed.html index bef6aad..224ce8e 100644 --- a/certidude/static/views/signed.html +++ b/certidude/static/views/signed.html @@ -56,7 +56,7 @@

    {% if session.authority.tagging %} -