diff --git a/.travis.yml b/.travis.yml index f34ff7b..973696b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,10 @@ script: - sudo useradd adminbot -G sudo -p '$1$PBkf5waA$n9EV6WJ7PS6lyGWkgeTPf1' - sudo useradd userbot -G users -p '$1$PBkf5waA$n9EV6WJ7PS6lyGWkgeTPf1' - sudo adduser --system --no-create-home --group certidude - - sudo py.test -s -v --cov-report xml --cov=certidude tests/ + - sudo coverage run --parallel-mode --source certidude -m py.test tests + - sudo coverage combine + - sudo coverage report + - sudo coverage xml cache: directories: - $HOME/.cache/pip diff --git a/README.rst b/README.rst index 7bd4fc6..078499a 100644 --- a/README.rst +++ b/README.rst @@ -353,7 +353,17 @@ To install the package from the source: .. code:: bash - python setup.py install --single-version-externally-managed --root / + pip install -e . + +To run tests and measure code coverage grab a clean VM or container: + +.. code:: bash + + pip install codecov pytest-cov + rm .coverage* + TRAVIS=1 coverage run --parallel-mode --source certidude -m py.test tests + coverage combine + coverage report To uninstall: diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index 82466f5..393cb10 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -17,18 +17,6 @@ from certidude import const, config logger = logging.getLogger(__name__) -class CertificateStatusResource(object): - """ - openssl ocsp -issuer CAcert_class1.pem -serial 0x -url http://localhost -CAfile cacert_both.pem - """ - def on_post(self, req, resp): - ocsp_request = req.stream.read(req.content_length) - for component in decoder.decode(ocsp_request): - click.echo(component) - resp.append_header("Content-Type", "application/ocsp-response") - resp.status = falcon.HTTP_200 - raise NotImplementedError() - class CertificateAuthorityResource(object): def on_get(self, req, resp): @@ -195,7 +183,6 @@ def certidude_app(log_handlers=[]): app.req_options.auto_parse_form_urlencoded = True # Certificate authority API calls - app.add_route("/api/ocsp/", CertificateStatusResource()) app.add_route("/api/certificate/", CertificateAuthorityResource()) app.add_route("/api/revoked/", RevocationListResource()) app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource()) diff --git a/certidude/authority.py b/certidude/authority.py index 115e983..b0fea43 100644 --- a/certidude/authority.py +++ b/certidude/authority.py @@ -96,7 +96,7 @@ def signer_exec(cmd, *bits): sock.sendall(b"\n\n") buf = sock.recv(8192) if not buf: - raise + raise Exception("Connection lost") return buf diff --git a/certidude/cli.py b/certidude/cli.py index bb67d39..89e7e3d 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -15,8 +15,9 @@ import subprocess import sys from configparser import ConfigParser, NoOptionError, NoSectionError from certidude.helpers import certidude_request_certificate -from certidude.common import expand_paths, ip_address, ip_network, apt, rpm, pip +from certidude.common import expand_paths, ip_address, ip_network, apt, rpm, pip, drop_privileges from datetime import datetime, timedelta +from time import sleep import const logger = logging.getLogger(__name__) @@ -815,10 +816,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, click.echo("Using templates from %s" % template_path) if not directory: - if os.getuid(): - directory = os.path.join(os.path.expanduser("~/.certidude"), common_name) - else: - directory = os.path.join("/var/lib/certidude", common_name) + directory = os.path.join("/var/lib/certidude", common_name) click.echo("Placing authority files in %s" % directory) certificate_url = "http://%s/api/certificate/" % common_name @@ -831,77 +829,74 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, ca_key = os.path.join(directory, "ca_key.pem") ca_crt = os.path.join(directory, "ca_crt.pem") - if os.getuid() == 0: - try: - pwd.getpwnam("certidude") - click.echo("User 'certidude' already exists") - except KeyError: - cmd = "adduser", "--system", "--no-create-home", "--group", "certidude" - if subprocess.call(cmd): - click.echo("Failed to create system user 'certidude'") - return 255 + try: + pwd.getpwnam("certidude") + click.echo("User 'certidude' already exists") + except KeyError: + cmd = "adduser", "--system", "--no-create-home", "--group", "certidude" + if subprocess.call(cmd): + click.echo("Failed to create system user 'certidude'") + return 255 - if os.path.exists(kerberos_keytab): - click.echo("Service principal keytab found in '%s'" % kerberos_keytab) - else: - click.echo("To use 'kerberos' authentication backend join the domain and create service principal with:") - click.echo() - click.echo(" KRB5_KTNAME=FILE:%s net ads keytab add HTTP -P" % kerberos_keytab) - click.echo(" chown %s %s" % (username, kerberos_keytab)) - click.echo() - - if os.path.exists("/etc/krb5.keytab") and os.path.exists("/etc/samba/smb.conf"): - # Fetch Kerberos ticket for system account - cp = ConfigParser() - cp.read("/etc/samba/smb.conf") - realm = cp.get("global", "realm") - domain = realm.lower() - name = cp.get("global", "netbios name") - - base = ",".join(["dc=" + j for j in domain.split(".")]) - if not os.path.exists("/etc/cron.hourly/certidude"): - with open("/etc/cron.hourly/certidude", "w") as fh: - fh.write(env.get_template("server/cronjob").render(vars())) - os.chmod("/etc/cron.hourly/certidude", 0o755) - click.echo("Created /etc/cron.hourly/certidude for automatic LDAP service ticket renewal, inspect and adjust accordingly") - os.system("/etc/cron.hourly/certidude") - else: - click.echo("Warning: /etc/krb5.keytab or /etc/samba/smb.conf not found, Kerberos unconfigured") - - static_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), "static") - certidude_path = sys.argv[0] - - # Push server config generation - if not os.path.exists("/etc/nginx") or os.getenv("TRAVIS"): - click.echo("Directory /etc/nginx does not exist, hence not creating nginx configuration") - listen = "0.0.0.0" - port = "80" - else: - port = "8080" - click.echo("Generating: %s" % nginx_config.name) - nginx_config.write(env.get_template("server/nginx.conf").render(vars())) - if not os.path.exists("/etc/nginx/sites-enabled/certidude.conf"): - os.symlink("../sites-available/certidude.conf", "/etc/nginx/sites-enabled/certidude.conf") - click.echo("Symlinked %s -> /etc/nginx/sites-enabled/" % nginx_config.name) - if os.path.exists("/etc/nginx/sites-enabled/default"): - os.unlink("/etc/nginx/sites-enabled/default") - if not push_server: - click.echo("Remember to install nchan capable nginx instead of regular nginx!") - - if os.path.exists("/etc/systemd"): - if os.path.exists("/etc/systemd/system/certidude.service"): - click.echo("File /etc/systemd/system/certidude.service already exists, remove to regenerate") - else: - 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") - else: - click.echo("Not systemd based OS, don't know how to set up initscripts") - - _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") - os.setgid(gid) + if os.path.exists(kerberos_keytab): + click.echo("Service principal keytab found in '%s'" % kerberos_keytab) else: - click.echo("Not root, skipping user and system config creation") + click.echo("To use 'kerberos' authentication backend join the domain and create service principal with:") + click.echo() + click.echo(" KRB5_KTNAME=FILE:%s net ads keytab add HTTP -P" % kerberos_keytab) + click.echo(" chown %s %s" % (username, kerberos_keytab)) + click.echo() + + if os.path.exists("/etc/krb5.keytab") and os.path.exists("/etc/samba/smb.conf"): + # Fetch Kerberos ticket for system account + cp = ConfigParser() + cp.read("/etc/samba/smb.conf") + realm = cp.get("global", "realm") + domain = realm.lower() + name = cp.get("global", "netbios name") + + base = ",".join(["dc=" + j for j in domain.split(".")]) + if not os.path.exists("/etc/cron.hourly/certidude"): + with open("/etc/cron.hourly/certidude", "w") as fh: + fh.write(env.get_template("server/cronjob").render(vars())) + os.chmod("/etc/cron.hourly/certidude", 0o755) + click.echo("Created /etc/cron.hourly/certidude for automatic LDAP service ticket renewal, inspect and adjust accordingly") + os.system("/etc/cron.hourly/certidude") + else: + click.echo("Warning: /etc/krb5.keytab or /etc/samba/smb.conf not found, Kerberos unconfigured") + + static_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), "static") + certidude_path = sys.argv[0] + + # Push server config generation + if not os.path.exists("/etc/nginx") or os.getenv("TRAVIS"): + click.echo("Directory /etc/nginx does not exist, hence not creating nginx configuration") + listen = "0.0.0.0" + port = "80" + else: + port = "8080" + click.echo("Generating: %s" % nginx_config.name) + nginx_config.write(env.get_template("server/nginx.conf").render(vars())) + if not os.path.exists("/etc/nginx/sites-enabled/certidude.conf"): + os.symlink("../sites-available/certidude.conf", "/etc/nginx/sites-enabled/certidude.conf") + click.echo("Symlinked %s -> /etc/nginx/sites-enabled/" % nginx_config.name) + if os.path.exists("/etc/nginx/sites-enabled/default"): + os.unlink("/etc/nginx/sites-enabled/default") + if not push_server: + click.echo("Remember to install nchan capable nginx instead of regular nginx!") + + if os.path.exists("/etc/systemd"): + if os.path.exists("/etc/systemd/system/certidude.service"): + click.echo("File /etc/systemd/system/certidude.service already exists, remove to regenerate") + else: + 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") + else: + click.echo("Not systemd based OS, don't know how to set up initscripts") + + _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") + os.setgid(gid) if not os.path.exists(const.CONFIG_DIR): click.echo("Creating %s" % const.CONFIG_DIR) @@ -1121,6 +1116,7 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign @click.argument("common_name") @click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN") def certidude_sign(common_name, overwrite): + drop_privileges() from certidude import authority cert = authority.sign(common_name, overwrite) @@ -1128,6 +1124,7 @@ def certidude_sign(common_name, overwrite): @click.command("revoke", help="Revoke certificate") @click.argument("common_name") def certidude_revoke(common_name): + drop_privileges() from certidude import authority authority.revoke(common_name) @@ -1144,6 +1141,7 @@ def certidude_cron(): os.rename(path, expired_path) click.echo("Moved %s to %s" % (path, expired_path)) + @click.command("serve", help="Run server") @click.option("-p", "--port", default=80, help="Listen port") @click.option("-l", "--listen", default="0.0.0.0", help="Listen address") @@ -1151,9 +1149,10 @@ def certidude_cron(): def certidude_serve(port, listen, fork): from setproctitle import setproctitle from certidude.signer import SignServer - from certidude import const + from certidude import authority, const click.echo("Using configuration from: %s" % const.CONFIG_PATH) + log_handlers = [] from certidude import config @@ -1166,10 +1165,7 @@ def certidude_serve(port, listen, fork): # TODO: umask! - import pwd - _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") - restricted_groups = [] - restricted_groups.append(gid) + from logging.handlers import RotatingFileHandler rh = RotatingFileHandler("/var/log/certidude.log", maxBytes=1048576*5, backupCount=5) rh.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")) @@ -1180,11 +1176,10 @@ def certidude_serve(port, listen, fork): Spawn signer process """ - child_pid = os.fork() + if os.path.exists(const.SIGNER_SOCKET_PATH): + os.unlink(const.SIGNER_SOCKET_PATH) - if child_pid: - pass - else: + if not os.fork(): click.echo("Signer process spawned with PID %d at %s" % (os.getpid(), const.SIGNER_SOCKET_PATH)) setproctitle("[signer]") @@ -1199,21 +1194,30 @@ def certidude_serve(port, listen, fork): server = SignServer() # Drop privileges - if not os.getuid(): - os.chown(const.SIGNER_SOCKET_PATH, uid, gid) - os.chmod(const.SIGNER_SOCKET_PATH, 0770) + _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") + os.chown(const.SIGNER_SOCKET_PATH, uid, gid) + os.chmod(const.SIGNER_SOCKET_PATH, 0770) - click.echo("Dropping privileges of signer") - _, _, uid, gid, gecos, root, shell = pwd.getpwnam("nobody") - os.setgroups([]) - os.setgid(gid) - os.setuid(uid) - else: - click.echo("Not dropping privileges of signer process") + click.echo("Dropping privileges of signer") + _, _, uid, gid, gecos, root, shell = pwd.getpwnam("nobody") + os.setgroups([]) + os.setgid(gid) + os.setuid(uid) - asyncore.loop() + try: + asyncore.loop() + except asyncore.ExitNow: + pass + click.echo("Signer was shut down") return - + click.echo("Waiting for signer to start up") + time_left = 2.0 + delay = 0.1 + while not os.path.exists(const.SIGNER_SOCKET_PATH) and time_left > 0: + sleep(delay) + time_left -= delay + assert authority.signer_exec("ping") == "pong" + click.echo("Signer alive") click.echo("Users subnets: %s" % ", ".join([str(j) for j in config.USER_SUBNETS])) @@ -1230,7 +1234,7 @@ def certidude_serve(port, listen, fork): click.echo("Serving API at %s:%d" % (listen, port)) from wsgiref.simple_server import make_server, WSGIServer - from SocketServer import ThreadingMixIn, ForkingMixIn + from SocketServer import ForkingMixIn from certidude.api import certidude_app class ThreadingWSGIServer(ForkingMixIn, WSGIServer): @@ -1251,13 +1255,6 @@ def certidude_serve(port, listen, fork): if os.path.exists("/etc/cron.hourly/certidude"): os.system("/etc/cron.hourly/certidude") - # PAM needs access to /etc/shadow - if config.AUTHENTICATION_BACKENDS == {"pam"}: - import grp - name, passwd, num, mem = grp.getgrnam("shadow") - click.echo("Adding current user to shadow group due to PAM authentication backend") - restricted_groups.append(num) - if config.EVENT_SOURCE_PUBLISH: from certidude.push import EventSourceLogHandler log_handlers.append(EventSourceLogHandler()) @@ -1281,17 +1278,13 @@ def certidude_serve(port, listen, fork): atexit.register(exit_handler) logger.debug("Started Certidude at %s", const.FQDN) + drop_privileges() - # Drop privileges - os.setgroups(restricted_groups) - os.setgid(gid) - os.setuid(uid) - - click.echo("Switched to user %s (uid=%d, gid=%d); member of groups %s" % - ("certidude", os.getuid(), os.getgid(), ", ".join([str(j) for j in os.getgroups()]))) - - os.umask(0o007) - + def quit_handler(*args, **kwargs): + click.echo("Shutting down HTTP server...") + import threading + threading.Thread(target=httpd.shutdown).start() + signal.signal(signal.SIGHUP, quit_handler) httpd.serve_forever() diff --git a/certidude/common.py b/certidude/common.py index b4a6134..2d0db0d 100644 --- a/certidude/common.py +++ b/certidude/common.py @@ -3,6 +3,27 @@ import os import click import subprocess +def drop_privileges(): + from certidude import config + import pwd + _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") + restricted_groups = [] + restricted_groups.append(gid) + + # PAM needs access to /etc/shadow + if config.AUTHENTICATION_BACKENDS == {"pam"}: + import grp + name, passwd, num, mem = grp.getgrnam("shadow") + click.echo("Adding current user to shadow group due to PAM authentication backend") + restricted_groups.append(num) + + os.setgroups(restricted_groups) + os.setgid(gid) + os.setuid(uid) + click.echo("Switched to user %s (uid=%d, gid=%d); member of groups %s" % + ("certidude", os.getuid(), os.getgid(), ", ".join([str(j) for j in os.getgroups()]))) + os.umask(0o007) + def ip_network(j): import ipaddress return ipaddress.ip_network(unicode(j)) diff --git a/certidude/const.py b/certidude/const.py index fd7e96c..e258009 100644 --- a/certidude/const.py +++ b/certidude/const.py @@ -6,16 +6,15 @@ import sys KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096 RUN_DIR = "/run/certidude" -CONFIG_DIR = os.path.expanduser("~/.certidude") if os.getuid() else "/etc/certidude" +CONFIG_DIR = "/etc/certidude" CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf") - CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf") SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf") -SERVER_PID_PATH = os.path.join(CONFIG_DIR if os.getuid() else RUN_DIR, "server.pid") -SERVER_LOG_PATH = os.path.join(CONFIG_DIR, "server.log") if os.getuid() else "/var/log/certidude-server.log" -SIGNER_SOCKET_PATH = os.path.join(CONFIG_DIR, "signer.sock") if os.getuid() else "/run/certidude/signer.sock" -SIGNER_PID_PATH = os.path.join(CONFIG_DIR if os.getuid() else RUN_DIR, "signer.pid") -SIGNER_LOG_PATH = os.path.join(CONFIG_DIR, "signer.log") if os.getuid() else "/var/log/certidude-signer.log" +SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid") +SERVER_LOG_PATH = "/var/log/certidude-server.log" +SIGNER_SOCKET_PATH = "/run/certidude/signer.sock" +SIGNER_PID_PATH = os.path.join(RUN_DIR, "signer.pid") +SIGNER_LOG_PATH = "/var/log/certidude-signer.log" try: FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] diff --git a/certidude/signer.py b/certidude/signer.py index 4456d5a..0a61445 100644 --- a/certidude/signer.py +++ b/certidude/signer.py @@ -55,8 +55,14 @@ class SignHandler(asynchat.async_chat): self.send(crl.public_bytes(Encoding.PEM)) - elif cmd == "ocsp-request": - NotImplemented # TODO: Implement OCSP + elif cmd == "ping": + self.send("pong") + self.close() + + elif cmd == "exit": + self.send("ok") + self.close() + raise asyncore.ExitNow() elif cmd == "sign-request": # Only common name and public key are used from request diff --git a/tests/test_cli.py b/tests/test_cli.py index 442d087..8f09c32 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,10 +1,10 @@ -import subprocess import pwd from click.testing import CliRunner from datetime import datetime, timedelta +from time import sleep import pytest - -# pkill py && rm -Rfv ~/.certidude && TRAVIS=1 py.test tests +import shutil +import os runner = CliRunner() @@ -33,8 +33,11 @@ def generate_csr(cn=None): return buf def test_cli_setup_authority(): - import shutil import os + import sys + + assert os.getuid() == 0, "Run tests as root in a clean VM or container" + if os.path.exists("/run/certidude/signer.pid"): with open("/run/certidude/signer.pid") as fh: try: @@ -69,7 +72,12 @@ def test_cli_setup_authority(): from certidude import const result = runner.invoke(cli, ['setup', 'authority']) + os.setgid(0) # Restore GID + os.umask(0022) + assert not result.exception, result.output + assert os.getuid() == 0 and os.getgid() == 0, "Serve dropped permissions incorrectly!" + from certidude import config, authority assert authority.ca_cert.serial_number >= 0x100000000000000000000000000000000000000 @@ -79,19 +87,20 @@ def test_cli_setup_authority(): # Start server before any signing operations are performed config.CERTIFICATE_RENEWAL_ALLOWED = True - result = runner.invoke(cli, ['serve', '-f', '-p', '80', '-l', '127.0.1.1']) - assert not result.exception, result.output + + server_pid = os.fork() + if not server_pid: + # Fork to prevent umask, setuid, setgid side effects + result = runner.invoke(cli, ['serve', '-p', '80', '-l', '127.0.1.1']) + assert not result.exception, result.output + return + + sleep(1) # Wait for serve to start up import requests # Test CA certificate fetch buf = open("/var/lib/certidude/ca.example.lan/ca_crt.pem").read() - - r = client().simulate_get("/api/certificate") - assert r.status_code == 200 - assert r.headers.get('content-type') == "application/x-x509-ca-cert" - assert r.text == buf - r = requests.get("http://ca.example.lan/api/certificate") assert r.status_code == 200 assert r.headers.get('content-type') == "application/x-x509-ca-cert" @@ -107,7 +116,7 @@ def test_cli_setup_authority(): # Check that we can retrieve empty CRL assert authority.export_crl(), "Failed to export CRL" - r = client().simulate_get("/api/revoked/") + r = requests.get("http://ca.example.lan/api/revoked/") assert r.status_code == 200, r.text @@ -116,13 +125,10 @@ def test_cli_setup_authority(): assert not result.exception, result.output # Test static - r = client().simulate_get("/nonexistant.html") - assert r.status_code == 404, r.text - r = client().simulate_get("/index.html") - assert r.status_code == 200, r.text r = requests.get("http://ca.example.lan/index.html") - assert r.status_code == 200, "server responded %s, logs say %s" % (r.text, open("/var/log/certidude.log").read()) - + assert r.status_code == 200, r.text # if this breaks certidude serve has no read access to static folder + r = requests.get("http://ca.example.lan/nonexistant.html") + assert r.status_code == 404, r.text # Test request submission buf = generate_csr(cn=u"test") @@ -141,7 +147,7 @@ def test_cli_setup_authority(): assert r.status_code == 202 # already exists, same keypair so it's ok r = client().simulate_post("/api/request/", - query_string="wait=1", + query_string="wait=true", body=buf, headers={"content-type":"application/pkcs10"}) assert r.status_code == 303 # redirect to long poll @@ -165,7 +171,8 @@ def test_cli_setup_authority(): r = client().simulate_get("/api/request/nonexistant/", headers={"Accept":"application/json"}) assert r.status_code == 404 # nonexistant common names - r = client().simulate_post("/api/request/", query_string="autosign=1", + r = client().simulate_post("/api/request/", + query_string="autosign=1", body=buf, headers={"content-type":"application/pkcs10"}) assert r.status_code == 200 # autosign successful @@ -176,17 +183,16 @@ def test_cli_setup_authority(): # Test command line interface result = runner.invoke(cli, ['list', '-srv']) assert not result.exception, result.output - result = runner.invoke(cli, ['sign', 'test', '-o']) - assert not result.exception, result.output - result = runner.invoke(cli, ['revoke', 'test']) - assert not result.exception, result.output - authority.generate_ovpn_bundle(u"test2") - authority.generate_pkcs12_bundle(u"test3") - result = runner.invoke(cli, ['list', '-srv']) - assert not result.exception, result.output - result = runner.invoke(cli, ['cron']) - assert not result.exception, result.output + # Some commands have side effects (setuid, setgid etc) + child_pid = os.fork() + if not child_pid: + result = runner.invoke(cli, ['sign', 'test', '-o']) + assert not result.exception, result.output + return + else: + os.waitpid(child_pid, 0) + assert os.getuid() == 0 and os.getgid() == 0, "Serve dropped permissions incorrectly!" # Test session API call r = client().simulate_get("/api/", headers={"Authorization":usertoken}) @@ -203,120 +209,110 @@ def test_cli_setup_authority(): r = client().simulate_get("/api/signed/nonexistant/") assert r.status_code == 404, r.text - r = client().simulate_get("/api/signed/test2/") + r = client().simulate_get("/api/signed/test/") assert r.status_code == 200, r.text assert r.headers.get('content-type') == "application/x-pem-file" - r = client().simulate_get("/api/signed/test2/", headers={"Accept":"application/json"}) + r = client().simulate_get("/api/signed/test/", headers={"Accept":"application/json"}) assert r.status_code == 200, r.text assert r.headers.get('content-type') == "application/json" - r = client().simulate_get("/api/signed/test2/", headers={"Accept":"text/plain"}) + r = client().simulate_get("/api/signed/test/", headers={"Accept":"text/plain"}) assert r.status_code == 415, r.text # Test revocations API call r = client().simulate_get("/api/revoked/", headers={"Accept":"application/x-pem-file"}) - assert r.status_code == 200, r.text - assert r.headers.get('content-type') == "application/x-pem-file" - - r = requests.get("http://ca.example.lan/api/revoked/", - headers={"Accept":"application/x-pem-file"}) - assert r.status_code == 200, "Server responded with %s, server logs say %s" % (r.text, open("/var/log/certidude.log").read()) + assert r.status_code == 200, r.text # if this breaks certidude serve has no access to signer socket assert r.headers.get('content-type') == "application/x-pem-file" r = client().simulate_get("/api/revoked/") assert r.status_code == 200, r.text assert r.headers.get('content-type') == "application/x-pkcs7-crl" - r = requests.get("http://ca.example.lan/api/revoked/") - assert r.status_code == 200, r.text - assert r.headers.get('content-type') == "application/x-pkcs7-crl" - r = client().simulate_get("/api/revoked/", headers={"Accept":"text/plain"}) assert r.status_code == 415, r.text - r = client().simulate_get("/api/revoked/", query_string="wait=true", + r = client().simulate_get("/api/revoked/", + query_string="wait=true", headers={"Accept":"application/x-pem-file"}) assert r.status_code == 303, r.text # Test attribute fetching API call - r = client().simulate_get("/api/signed/test2/attr/") + r = client().simulate_get("/api/signed/test/attr/") assert r.status_code == 403, r.text - r = client().simulate_get("/api/signed/test2/lease/", headers={"Authorization":admintoken}) + r = client().simulate_get("/api/signed/test/lease/", headers={"Authorization":admintoken}) assert r.status_code == 404, r.text # Insert lease as if VPN gateway had submitted it - path, _, _ = authority.get_signed("test2") + path, _, _ = authority.get_signed("test") from xattr import setxattr setxattr(path, "user.lease.address", b"127.0.0.1") setxattr(path, "user.lease.last_seen", b"random") - r = client().simulate_get("/api/signed/test2/attr/") + r = client().simulate_get("/api/signed/test/attr/") assert r.status_code == 200, r.text # Test lease retrieval - r = client().simulate_get("/api/signed/test2/lease/") + r = client().simulate_get("/api/signed/test/lease/") assert r.status_code == 401, r.text - r = client().simulate_get("/api/signed/test2/lease/", headers={"Authorization":usertoken}) + r = client().simulate_get("/api/signed/test/lease/", headers={"Authorization":usertoken}) assert r.status_code == 403, r.text - r = client().simulate_get("/api/signed/test2/lease/", headers={"Authorization":admintoken}) + r = client().simulate_get("/api/signed/test/lease/", headers={"Authorization":admintoken}) assert r.status_code == 200, r.text assert r.headers.get('content-type') == "application/json; charset=UTF-8" # Tags should not be visible anonymously - r = client().simulate_get("/api/signed/test2/tag/") + r = client().simulate_get("/api/signed/test/tag/") assert r.status_code == 401, r.text - r = client().simulate_get("/api/signed/test2/tag/", headers={"Authorization":usertoken}) + r = client().simulate_get("/api/signed/test/tag/", headers={"Authorization":usertoken}) assert r.status_code == 403, r.text - r = client().simulate_get("/api/signed/test2/tag/", headers={"Authorization":admintoken}) + r = client().simulate_get("/api/signed/test/tag/", headers={"Authorization":admintoken}) assert r.status_code == 200, r.text # Tags can be added only by admin - r = client().simulate_post("/api/signed/test2/tag/") + r = client().simulate_post("/api/signed/test/tag/") assert r.status_code == 401, r.text - r = client().simulate_post("/api/signed/test2/tag/", + r = client().simulate_post("/api/signed/test/tag/", headers={"Authorization":usertoken}) assert r.status_code == 403, r.text - r = client().simulate_post("/api/signed/test2/tag/", + r = client().simulate_post("/api/signed/test/tag/", body="key=other&value=something", headers={"content-type": "application/x-www-form-urlencoded", "Authorization":admintoken}) assert r.status_code == 200, r.text # Tags can be overwritten only by admin - r = client().simulate_put("/api/signed/test2/tag/other/") + r = client().simulate_put("/api/signed/test/tag/other/") assert r.status_code == 401, r.text - r = client().simulate_put("/api/signed/test2/tag/other/", + r = client().simulate_put("/api/signed/test/tag/other/", headers={"Authorization":usertoken}) assert r.status_code == 403, r.text - r = client().simulate_put("/api/signed/test2/tag/other/", + r = client().simulate_put("/api/signed/test/tag/other/", body="value=else", headers={"content-type": "application/x-www-form-urlencoded", "Authorization":admintoken}) assert r.status_code == 200, r.text # Tags can be deleted only by admin - r = client().simulate_delete("/api/signed/test2/tag/else/") + r = client().simulate_delete("/api/signed/test/tag/else/") assert r.status_code == 401, r.text - r = client().simulate_delete("/api/signed/test2/tag/else/", + r = client().simulate_delete("/api/signed/test/tag/else/", headers={"Authorization":usertoken}) assert r.status_code == 403, r.text - r = client().simulate_delete("/api/signed/test2/tag/else/", + r = client().simulate_delete("/api/signed/test/tag/else/", headers={"content-type": "application/x-www-form-urlencoded", "Authorization":admintoken}) assert r.status_code == 200, r.text # Test revocation - r = client().simulate_delete("/api/signed/test2/") + r = client().simulate_delete("/api/signed/test/") assert r.status_code == 401, r.text - r = client().simulate_delete("/api/signed/test2/", + r = client().simulate_delete("/api/signed/test/", headers={"Authorization":usertoken}) assert r.status_code == 403, r.text - r = client().simulate_delete("/api/signed/test2/", + r = client().simulate_delete("/api/signed/test/", headers={"Authorization":admintoken}) assert r.status_code == 200, r.text - result = runner.invoke(cli, ['revoke', 'test3']) - assert not result.exception, result.output # Log can be read only by admin @@ -351,7 +347,8 @@ def test_cli_setup_authority(): query_string="u=userbot&t=1493184342&c=ac9b71421d5741800c5a4905b20c1072594a2df863e60ba836464888786bf2a6", headers={"content-type": "application/x-www-form-urlencoded", "Authorization":admintoken}) assert r2.status_code == 403 # invalid checksum - r2 = client().simulate_get("/api/token/", query_string=r.content, + r2 = client().simulate_get("/api/token/", + query_string=r.content, headers={"User-Agent":"Mozilla/5.0 (X11; Fedora; Linux x86_64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36"}) assert r2.status_code == 200 # token consumed by anyone on Fedora @@ -362,7 +359,6 @@ def test_cli_setup_authority(): assert r2.status_code == 200 # token consumed by anyone on unknown device assert r2.headers.get('content-type') == "application/x-pkcs12" - result = runner.invoke(cli, ['setup', 'openvpn', 'server', "-cn", "vpn.example.lan", "ca.example.lan"]) assert not result.exception, result.output @@ -379,9 +375,39 @@ def test_cli_setup_authority(): # pregen dhparam result = runner.invoke(cli, ["request", "--no-wait"]) assert not result.exception, "server responded %s, server logs say %s" % (result.output, open("/var/log/certidude.log").read()) - result = runner.invoke(cli, ['sign', 'vpn.example.lan']) - assert not result.exception, result.output + + child_pid = os.fork() + if not child_pid: + result = runner.invoke(cli, ['sign', 'vpn.example.lan']) + assert not result.exception, result.output + return + else: + os.waitpid(child_pid, 0) + result = runner.invoke(cli, ["request", "--no-wait"]) assert not result.exception, result.output result = runner.invoke(cli, ["request", "--renew"]) assert not result.exception, result.output + + # Test revocation on command-line + child_pid = os.fork() + if not child_pid: + result = runner.invoke(cli, ['revoke', 'vpn.example.lan']) + assert not result.exception, result.output + return + else: + os.waitpid(child_pid, 0) + + result = runner.invoke(cli, ['list', '-srv']) + assert not result.exception, result.output + result = runner.invoke(cli, ['cron']) + assert not result.exception, result.output + + # Shut down signer + assert authority.signer_exec("exit") == "ok" + + # Shut down server + with open("/run/certidude/server.pid") as fh: + os.kill(int(fh.read()), 1) + + os.waitpid(server_pid, 0)