diff --git a/.gitignore b/.gitignore index a493858..287fc8d 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,7 @@ lextab.py yacctab.py .pytest_cache *~ +certidude/static/coverage/ +*.tar +*.bz2 +*.gz diff --git a/README.rst b/README.rst index ae790ea..45703aa 100644 --- a/README.rst +++ b/README.rst @@ -301,8 +301,8 @@ Clone the repository: .. code:: bash - git clone https://github.com/laurivosandi/certidude - cd certidude + git clone https://github.com/laurivosandi/certidude /srv/certidude + cd /srv/certidude Install dependencies as shown above and additionally: @@ -316,15 +316,17 @@ To install the package from the source tree: pip3 install -e . -To run tests and measure code coverage grab a clean VM or container: +To run tests and measure code coverage grab a clean VM or container, +set hostname to ca.example.lan, export environment variable COVERAGE_PROCESS_START globally and run: .. code:: bash pip3 install codecov pytest-cov - rm .coverage* - COVERAGE_FILE=/tmp/.coverage TRAVIS=1 coverage run --parallel-mode --source certidude -m py.test tests --capture=sys + rm /tmp/.coverage* + COVERAGE_PROCESS_START=/srv/certidude/.coveragerc py.test tests --capture=sys coverage combine coverage report + coverage html -i To uninstall: @@ -342,7 +344,7 @@ 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 + apt install 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 @@ -363,6 +365,8 @@ Transfer certidude-server.tar or certidude-client.tar to the target machine and Proceed to bootstrap authority without installing packages or assembling assets: +.. code:: bash + certidude setup authority --skip-packages --skip-assets [--elliptic-curve] [--organization "Mycorp LLC"] Note it's highly recommended to enable nginx PPA in the target machine diff --git a/certidude/api/utils/firewall.py b/certidude/api/utils/firewall.py index 7622264..9d34dee 100644 --- a/certidude/api/utils/firewall.py +++ b/certidude/api/utils/firewall.py @@ -231,7 +231,7 @@ def authorize_server(func): def wrapped(resource, req, resp, *args, **kwargs): buf = req.get_header("X-SSL-CERT") if not buf: - logger.info("No TLS certificate presented to access administrative API call") + logger.info("No TLS certificate presented to access administrative API call from %s" % req.context.get("remote_addr")) raise falcon.HTTPForbidden("Forbidden", "Machine not authorized to perform the operation") header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii")) diff --git a/certidude/cli.py b/certidude/cli.py index 3e87a07..1280e92 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -24,6 +24,16 @@ from glob import glob from ipaddress import ip_network from oscrypto import asymmetric +try: + import coverage + cov = coverage.process_startup() + if cov: + click.echo("Enabling coverage tracking") + else: + click.echo("Coverage tracking not requested") +except ImportError: + pass + logger = logging.getLogger(__name__) # http://www.mad-hacking.net/documentation/linux/security/ssl-tls/creating-ca.xml @@ -55,7 +65,7 @@ def setup_client(prefix="client_", dh=False): if not os.path.exists(path): rpm("openssl") apt("openssl") - cmd = "openssl", "dhparam", "-out", path, ("1024" if os.getenv("TRAVIS") else "2048") + cmd = "openssl", "dhparam", "-out", path, str(const.KEY_SIZE) subprocess.check_call(cmd) arguments["dhparam_path"] = path @@ -1008,10 +1018,11 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat @click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN) @click.option("--skip-assets", is_flag=True, help="Don't attempt to assemble JS/CSS/font assets") @click.option("--skip-packages", is_flag=True, help="Don't attempt to install apt/pip/npm packages") +@click.option("--packages-only", is_flag=True, help="Install only apt/pip/npm packages") @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, tls_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, packages_only): 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" @@ -1055,8 +1066,11 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_confi else: click.echo("Web server nginx already installed") - if not os.path.exists("/usr/bin/node"): - os.symlink("/usr/bin/nodejs", "/usr/bin/node") + if not os.path.exists("/usr/bin/node"): + os.symlink("/usr/bin/nodejs", "/usr/bin/node") + + if packages_only: + return # Generate secret for tokens token_url = "https://" + const.FQDN + "/#action=enroll&token=%(token)s&router=%(router)s&protocol=ovpn" @@ -1150,8 +1164,9 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_confi with open("/etc/systemd/system/certidude.service", "w") as fh: fh.write(env.get_template("server/systemd.service").render(vars())) click.echo("File /etc/systemd/system/certidude.service created") + os.system("systemctl daemon-reload") else: - click.echo("Not systemd based OS, don't know how to set up initscripts") + raise NotImplementedError("Not systemd based OS, don't know how to set up initscripts") # Set umask to 0022 os.umask(0o022) @@ -1231,7 +1246,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_confi 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)) + cmd = "openssl", "dhparam", "-out", dhparam_path, str(const.KEY_SIZE) subprocess.check_call(cmd) if os.path.exists(tls_config.name): @@ -1362,15 +1377,6 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_confi assert os.stat("/etc/nginx/sites-available/certidude.conf").st_mode == 0o100600 assert os.stat("/etc/certidude/server.conf").st_mode == 0o100600 - click.echo("Enabling and starting Certidude backend") - os.system("systemctl enable certidude") - os.system("systemctl restart certidude") - click.echo("Enabling and starting nginx") - os.system("systemctl enable nginx") - os.system("systemctl start nginx") - os.system("systemctl reload nginx") - click.echo() - click.echo("To enable e-mail notifications install Postfix as sattelite system and set mailer address in %s" % const.SERVER_CONFIG_PATH) click.echo() click.echo("Use following commands to inspect the newly created files:") @@ -1383,6 +1389,15 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_confi 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") + click.echo() + click.echo("Enabling Certidude backend and nginx...") + os.system("systemctl enable certidude") + os.system("systemctl enable nginx") + click.echo("To (re)start services:") + click.echo() + click.echo(" systemctl restart certidude") + click.echo(" systemctl restart nginx") + click.echo() return 0 @@ -1598,14 +1613,6 @@ def certidude_serve(port, listen, fork): with open(const.SERVER_PID_PATH, "w") as pidfile: pidfile.write("%d\n" % pid) - def cleanup_handler(*args): - push.publish("server-stopped") - logger.debug("Shutting down Certidude") - sys.exit(0) # TODO: use another code, needs test refactor - - import signal - signal.signal(signal.SIGTERM, cleanup_handler) # Handle SIGTERM from systemd - push.publish("server-started") logger.debug("Started Certidude at %s", const.FQDN) @@ -1613,7 +1620,10 @@ def certidude_serve(port, listen, fork): try: httpd.serve_forever() except KeyboardInterrupt: - cleanup_handler() # FIXME + click.echo("Caught Ctrl-C, exiting...") + push.publish("server-stopped") + logger.debug("Shutting down Certidude") + return @click.command("yubikey", help="Set up Yubikey as client authentication token") @@ -1676,7 +1686,7 @@ def certidude_token_list(): 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): + for token in token_manager.list(expired=True, used=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])) diff --git a/certidude/const.py b/certidude/const.py index e43a6fe..2782067 100644 --- a/certidude/const.py +++ b/certidude/const.py @@ -5,7 +5,7 @@ import socket import sys from datetime import timedelta -KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096 +KEY_SIZE = 1024 if os.getenv("COVERAGE_PROCESS_START") 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])?$" diff --git a/certidude/static/502.json b/certidude/static/502.json new file mode 100644 index 0000000..4f302ba --- /dev/null +++ b/certidude/static/502.json @@ -0,0 +1,4 @@ +{ + "title": "502 Bad Gateway", + "description": "It seems the server had bit of a hiccup, perhaps this helps: systemctl restart certidude && journalctl -f" +} diff --git a/certidude/static/js/certidude.js b/certidude/static/js/certidude.js index 8a60fe7..8a9dd33 100644 --- a/certidude/static/js/certidude.js +++ b/certidude/static/js/certidude.js @@ -40,7 +40,7 @@ function onKeyGen() { } } - window.identifier = prefix + "-" + dig.digest().toHex().substring(0, 8); + window.identifier = prefix + "-" + dig.digest().toHex().substring(0, 5); console.info("Device identifier:", identifier); window.common_name = query.subject + "@" + identifier; @@ -79,9 +79,21 @@ function onKeyGen() { options[i].style.display = "block"; } } + $(".option.any").show(); } function onEnroll(encoding) { + console.info("Service name:", query.title); + var md = forge.md.md5.create(); + md.update(query.title); + var digest = md.digest().toHex(); + var service_uuid = digest.substring(0, 8) + "-" + + digest.substring(8, 12) + "-" + + digest.substring(12, 16) + "-" + + digest.substring(16,20) + "-" + + digest.substring(20) + console.info("Service UUID:", service_uuid); + console.info("User agent:", window.navigator.userAgent); var xhr = new XMLHttpRequest(); xhr.open('GET', "/api/certificate"); @@ -103,62 +115,45 @@ function onEnroll(encoding) { case 'p12': var buf = forge.asn1.toDer(p12).getBytes(); var mimetype = "application/x-pkcs12" - a.download = query.router + ".p12"; + a.download = query.title + ".p12"; break case 'sswan': var buf = JSON.stringify({ - uuid: "a061d140-d3f9-4db7-b2f8-32d6703f4618", - name: identifier, + uuid: service_uuid, + name: query.title, type: "ikev2-cert", 'ike-proposal': 'aes256-sha384-prfsha384-modp2048', - 'esp-proposal': 'aes128gcm16-aes128gmac-modp2048', + 'esp-proposal': 'aes128gcm16-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"; + a.download = query.title + ".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], - } - }, + session: session, key: forge.pki.privateKeyToPem(keys.privateKey), cert: xhr2.responseText, ca: xhr.responseText }); var mimetype = "application/x-openvpn-profile"; - a.download = query.router + ".ovpn"; + a.download = query.title + ".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" - } - } - }, + session: session, + title: query.title, 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"; + a.download = query.title + ".mobileconfig"; break } a.href = "data:" + mimetype + ";base64," + forge.util.encode64(buf); @@ -193,27 +188,55 @@ function onHashChanged() { 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, - session: { authority: { - hostname: window.location.hostname, - certificate: { blob: blob }}} - })); - }); - } else { - 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"; + $.get({ + method: "GET", + url: "/api/certificate", + error: function(response) { + if (response.responseJSON) { + var msg = response.responseJSON + } else { + var msg = { title: "Error " + response.status, description: response.statusText } } - setTimeout(onKeyGen, 100); - console.info("Generating key pair..."); - } else { - loadAuthority(query); + $("#view-dashboard").html(env.render('views/error.html', { message: msg })); + }, + success: function(blob) { + window.session = { + authority: { + hostname: window.location.hostname, + certificate: { + common_name: "Certidude at " + window.location.hostname, + algorithm: "rsa", + blob: blob + } + }, + service: { + title: query.title ? query.title : query.router, + protocols: query.protocols ? query.protocols.split(",") : null, + routers: query.router ? [query.router] : null, + } + } + + if (window.location.protocol != "https:") { + $("#view-dashboard").html(env.render('views/insecure.html', {session:session})); + } else { + if (query.action == "enroll") { + $("#view-dashboard").html(env.render('views/enroll.html', { + session:session, + token: query.token, + })); + 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(e) { diff --git a/certidude/static/snippets/ios.mobileconfig b/certidude/static/snippets/ios.mobileconfig index 77d3f61..36ad9f5 100644 --- a/certidude/static/snippets/ios.mobileconfig +++ b/certidude/static/snippets/ios.mobileconfig @@ -8,7 +8,7 @@ PayloadIdentifier org.example.vpn2 PayloadUUID - 9f93912b-5fd2-4455-99fd-13b9a47b4581 + {{ service_uuid }} PayloadType Configuration PayloadVersion diff --git a/certidude/static/snippets/windows.ps1 b/certidude/static/snippets/windows.ps1 index 578d1ef..a286050 100644 --- a/certidude/static/snippets/windows.ps1 +++ b/certidude/static/snippets/windows.ps1 @@ -26,7 +26,7 @@ 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://{{ 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 +Invoke-WebRequest -TimeoutSec 900 -Uri 'https://{{ session.authority.hostname }}:8443/api/{% if token %}token/?uuid={{ token }}{% else %}request/?wait=yes&autosign=yes{% endif %}' -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 diff --git a/certidude/static/views/authority.html b/certidude/static/views/authority.html index 3c3544a..584033a 100644 --- a/certidude/static/views/authority.html +++ b/certidude/static/views/authority.html @@ -231,14 +231,9 @@ curl http://{{ session.authority.hostname }}/api/revoked/?wait=yes -L -H "Accept

Issued tokens:

-