From b19e163a82e713017f37f736f35a98608167af75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauri=20V=C3=B5sandi?= Date: Sat, 6 May 2017 21:07:41 +0000 Subject: [PATCH] tests: Attempt to set up nchan as part of unittests --- .travis.yml | 4 ++ certidude/api/__init__.py | 2 +- certidude/api/request.py | 6 +- certidude/authority.py | 12 ++-- certidude/cli.py | 81 ++++++++++++++------------- certidude/common.py | 5 ++ certidude/config.py | 5 -- certidude/const.py | 2 +- certidude/errors.py | 3 + certidude/templates/server/nginx.conf | 2 +- tests/test_cli.py | 31 ++++++++-- 11 files changed, 92 insertions(+), 61 deletions(-) diff --git a/.travis.yml b/.travis.yml index e90f744..2b78140 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,9 @@ after_success: virtualenv: system_site_packages: true install: + - sudo add-apt-repository ppa:nginx/stable -y + - sudo apt update + - sudo apt install libnginx-mod-nchan - echo "127.0.0.1 localhost" | sudo tee /etc/hosts - echo "127.0.1.1 ca.example.lan ca" | sudo tee -a /etc/hosts - echo "127.0.0.1 vpn.example.lan vpn" | sudo tee -a /etc/hosts @@ -32,4 +35,5 @@ addons: hostname: ca apt: packages: + - software-properties-common - python-configparser diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index 8945e16..1d8b4d5 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -137,7 +137,7 @@ class StaticResource(object): def __call__(self, req, resp): path = os.path.realpath(os.path.join(self.root, req.path[1:])) if not path.startswith(self.root): - raise falcon.HTTPForbidden + raise falcon.HTTPBadRequest() if os.path.isdir(path): path = os.path.join(path, "index.html") diff --git a/certidude/api/request.py b/certidude/api/request.py index 524fd98..50c0bbe 100644 --- a/certidude/api/request.py +++ b/certidude/api/request.py @@ -172,7 +172,7 @@ class RequestDetailResource(object): try: _, buf, _ = authority.get_request(cn) - except EnvironmentError: + except errors.RequestDoesNotExist: logger.warning(u"Failed to serve non-existant request %s to %s", cn, req.context.get("remote_addr")) raise falcon.HTTPNotFound() @@ -227,8 +227,8 @@ class RequestDetailResource(object): try: authority.delete_request(cn) # Logging implemented in the function above - except EnvironmentError as e: - resp.body = "No certificate CN=%s found" % cn + except errors.RequestDoesNotExist as e: + resp.body = "No certificate signing request for %s found" % cn logger.warning(u"User %s failed to delete signing request %s from %s, reason: %s", req.context["user"], cn, req.context.get("remote_addr"), e) raise falcon.HTTPNotFound() diff --git a/certidude/authority.py b/certidude/authority.py index 5b1354b..8119f82 100644 --- a/certidude/authority.py +++ b/certidude/authority.py @@ -33,9 +33,12 @@ def get_request(common_name): if not re.match(RE_HOSTNAME, common_name): raise ValueError("Invalid common name %s" % repr(common_name)) path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") - with open(path) as fh: - buf = fh.read() - return path, buf, x509.load_pem_x509_csr(buf, default_backend()) + try: + with open(path) as fh: + buf = fh.read() + return path, buf, x509.load_pem_x509_csr(buf, default_backend()) + except EnvironmentError: + raise errors.RequestDoesNotExist("Certificate signing request file %s does not exist" % path) def get_signed(common_name): if not re.match(RE_HOSTNAME, common_name): @@ -210,8 +213,7 @@ def delete_request(common_name): if not re.match(RE_HOSTNAME, common_name): raise ValueError("Invalid common name") - path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") - _, buf, csr = get_request(common_name) + path, buf, csr = get_request(common_name) os.unlink(path) # Publish event at CA channel diff --git a/certidude/cli.py b/certidude/cli.py index 7ba657c..e4980d5 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -92,9 +92,10 @@ def setup_client(prefix="client_", dh=False): @click.option("-nw", "--no-wait", default=False, is_flag=True, help="Return immideately if server doesn't autosign") def certidude_request(fork, renew, no_wait): # Here let's try to avoid compiling packages from scratch - rpm("openssl") # TODO - apt("openssl python-cryptography python-jinja2") # Native packages on Ubuntu 16.04 - pip("cryptography jinja2") # Mac OS X, should be skipped on Ubuntu + rpm("openssl") or \ + apt("openssl python-cryptography python-jinja2") or \ + pip("cryptography jinja2") + import requests from jinja2 import Environment, PackageLoader env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True) @@ -270,16 +271,6 @@ def certidude_request(fork, renew, no_wait): "%s/ipsec.conf" % const.STRONGSWAN_PREFIX) break - # Regenerate /etc/ipsec.secrets - with open("%s/ipsec.secrets.part" % const.STRONGSWAN_PREFIX, "w") as fh: - for filename in os.listdir("%s/ipsec.d/private" % const.STRONGSWAN_PREFIX): - if not filename.endswith(".pem"): - continue - fh.write(": RSA %s/ipsec.d/private/%s\n" % (const.STRONGSWAN_PREFIX, filename)) - os.rename( - "%s/ipsec.secrets.part" % const.STRONGSWAN_PREFIX, - "%s/ipsec.secrets" % const.STRONGSWAN_PREFIX) - # Attempt to reload config or start if it's not running if os.path.exists("/usr/sbin/strongswan"): # wtf fedora if os.system("strongswan update"): @@ -319,7 +310,8 @@ def certidude_request(fork, renew, no_wait): nm_config.set("vpn", "remote-cert-tls", "server") # Assert TLS Server flag of X.509 certificate nm_config.set("vpn", "remote", service_config.get(endpoint, "remote")) nm_config.set("vpn", "port", str(endpoint_port)) - nm_config.set("vpn", "proto", endpoint_proto) + if endpoint_proto == "tcp": + nm_config.set("vpn", "proto-tcp", "yes") nm_config.set("vpn", "key", endpoint_key_path) nm_config.set("vpn", "cert", endpoint_certificate_path) nm_config.set("vpn", "ca", endpoint_authority_path) @@ -498,7 +490,7 @@ def certidude_setup_nginx(authority, common_name, site_config, tls_config, verif @click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME) @click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default") @click.option("--config", "-o", - default="/etc/openvpn/client-to-site.conf", + default="/etc/openvpn/client-to-site.conf", # TODO: created initially disabled conf type=click.File(mode="w", atomic=True, lazy=True), help="OpenVPN configuration file") @setup_client() @@ -580,19 +572,24 @@ def certidude_setup_strongswan_server(authority, common_name, subnet, route, **p # Create corresponding section to /etc/ipsec.conf from ipsecparse import loads - config = loads(open("%s/ipsec.conf" % const.STRONGSWAN_PREFIX).read()) - config["conn", authority] = dict( + ipsec_conf = loads(open("%s/ipsec.conf" % const.STRONGSWAN_PREFIX).read()) + ipsec_conf["ca", authority] = dict( + auto="add", + cacert=paths.get("authority_path")) + ipsec_conf["conn", authority] = dict( leftcert=paths.get("certificate_path"), leftsubnet=",".join(route), right="%any", rightsourceip=str(subnet), closeaction="restart", auto="ignore") - with open("%s/ipsec.conf.part" % const.STRONGSWAN_PREFIX, "w") as fh: - fh.write(config.dumps()) - os.rename( - "%s/ipsec.conf.part" % const.STRONGSWAN_PREFIX, - "%s/ipsec.conf" % const.STRONGSWAN_PREFIX) + with open("%s/ipsec.conf" % const.STRONGSWAN_PREFIX, "w") as fh: + fh.write(ipsec_conf.dumps()) + with open("%s/ipsec.secrets" % const.STRONGSWAN_PREFIX, "a") as fh: + fh.write(": RSA %s\n" % paths.get("key_path")) + if os.path.exists("/etc/apparmor.d/local"): + with open("/etc/apparmor.d/local/usr.lib.ipsec.charon", "w") as fh: + fh.write(os.path.join(const.STORAGE_PATH, "**") + " r,\n") # TODO: dedup! click.echo() click.echo("If you're running Ubuntu make sure you're not affected by #1505222") @@ -606,8 +603,7 @@ def certidude_setup_strongswan_server(authority, common_name, subnet, route, **p @setup_client() def certidude_setup_strongswan_client(authority, remote, common_name, **paths): # Install dependencies - apt("strongswan") - rpm("strongswan") + apt("strongswan") or rpm("strongswan") pip("ipsecparse") # Create corresponding section in /etc/certidude/services.conf @@ -629,24 +625,29 @@ def certidude_setup_strongswan_client(authority, remote, common_name, **paths): # Create corresponding section in /etc/ipsec.conf from ipsecparse import loads - config = loads(open('%s/ipsec.conf' % const.STRONGSWAN_PREFIX).read()) - config["conn", remote] = dict( + ipsec_conf = loads(open('%s/ipsec.conf' % const.STRONGSWAN_PREFIX).read()) + ipsec_conf["ca", authority] = dict( + auto="add", + cacert=paths.get("authority_path")) + ipsec_conf["conn", remote] = dict( leftsourceip="%config", left="%defaultroute", leftcert=paths.get("certificate_path"), rightid="%any", right=remote, - #rightsubnet=route, + rightsubnet="0.0.0.0/0", # To allow anything suggested by gateway keyexchange="ikev2", keyingtries="300", dpdaction="restart", closeaction="restart", auto="ignore") - with open("%s/ipsec.conf.part" % const.STRONGSWAN_PREFIX, "w") as fh: - fh.write(config.dumps()) - os.rename( - "%s/ipsec.conf.part" % const.STRONGSWAN_PREFIX, - "%s/ipsec.conf" % const.STRONGSWAN_PREFIX) + with open("%s/ipsec.conf" % const.STRONGSWAN_PREFIX, "w") as fh: + fh.write(ipsec_conf.dumps()) + with open("%s/ipsec.secrets" % const.STRONGSWAN_PREFIX, "a") as fh: + fh.write(": RSA %s\n" % paths.get("key_path")) + if os.path.exists("/etc/apparmor.d/local"): + with open("/etc/apparmor.d/local/usr.lib.ipsec.charon", "w") as fh: + fh.write(os.path.join(const.STORAGE_PATH, "**") + " r,\n") click.echo("Generated section %s in %s" % (authority, const.CLIENT_CONFIG_PATH)) click.echo("Run 'certidude request' to request certificates and to enable services") @@ -800,21 +801,23 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, 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: + if os.path.exists("/etc/nginx"): + listen = "127.0.1.1" port = "8080" click.echo("Generating: %s" % nginx_config.name) nginx_config.write(env.get_template("server/nginx.conf").render(vars())) + nginx_config.close() 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!") + os.system("service nginx restart") + else: + click.echo("Directory /etc/nginx does not exist, hence not creating nginx configuration") + click.echo("Remember to install/configure nchan capable nginx instead of regular nginx!") + listen = "0.0.0.0" + port = "80" if os.path.exists("/etc/systemd"): if os.path.exists("/etc/systemd/system/certidude.service"): diff --git a/certidude/common.py b/certidude/common.py index 95719da..16c6a33 100644 --- a/certidude/common.py +++ b/certidude/common.py @@ -40,6 +40,8 @@ def apt(packages): cmd = ["/usr/bin/apt-get", "install", "-yqq"] + packages.split(" ") click.echo("Running: %s" % " ".join(cmd)) subprocess.call(cmd) + return True + return False def rpm(packages): @@ -50,10 +52,13 @@ def rpm(packages): cmd = ["/usr/bin/dnf", "install", "-y"] + packages.split(" ") click.echo("Running: %s" % " ".join(cmd)) subprocess.call(cmd) + return True + return False def pip(packages): click.echo("Running: pip install %s" % packages) import pip pip.main(['install'] + packages.split(" ")) + return True diff --git a/certidude/config.py b/certidude/config.py index 526a797..e4345f8 100644 --- a/certidude/config.py +++ b/certidude/config.py @@ -71,11 +71,6 @@ EVENT_SOURCE_SUBSCRIBE = cp.get("push", "event source subscribe") LONG_POLL_PUBLISH = cp.get("push", "long poll publish") LONG_POLL_SUBSCRIBE = cp.get("push", "long poll subscribe") -if const.DOMAIN == "example.lan": # TODO: include nginx setup in Travis - EVENT_SOURCE_PUBLISH = "" - LONG_POLL_PUBLISH = "" - LONG_POLL_SUBSCRIBE = "//nonexistant/lp/sub/%s" - LOGGING_BACKEND = cp.get("logging", "backend") if "whitelist" == AUTHORIZATION_BACKEND: diff --git a/certidude/const.py b/certidude/const.py index 3fb893c..b3b6310 100644 --- a/certidude/const.py +++ b/certidude/const.py @@ -15,7 +15,7 @@ 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" -STORAGE_PATH = "/var/lib/certidude" +STORAGE_PATH = "/var/lib/certidude/" try: FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] diff --git a/certidude/errors.py b/certidude/errors.py index 8dcc649..403024b 100644 --- a/certidude/errors.py +++ b/certidude/errors.py @@ -2,6 +2,9 @@ class RequestExists(Exception): pass +class RequestDoesNotExist(Exception): + pass + class FatalError(Exception): """ Exception to be raised when user intervention is required diff --git a/certidude/templates/server/nginx.conf b/certidude/templates/server/nginx.conf index 54224ab..a92d91d 100644 --- a/certidude/templates/server/nginx.conf +++ b/certidude/templates/server/nginx.conf @@ -23,7 +23,7 @@ server { root {{static_path}}; location /api/ { - proxy_pass http://127.0.0.1{% if port != 80 %}:{{ port }}{% endif %}/api/; + proxy_pass http://127.0.1.1{% if port != 80 %}:{{ port }}{% endif %}/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_connect_timeout 600; diff --git a/tests/test_cli.py b/tests/test_cli.py index d2e52fd..bbccdba 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -72,6 +72,12 @@ def clean_client(): if filename.endswith(".pem"): os.unlink(os.path.join("/tmp/ca.example.lan", filename)) + # Reset IPsec stuff + with open("/etc/ipsec.conf", "w") as fh: # TODO: make compatible with Fedora + pass + with open("/etc/ipsec.secrets", "w") as fh: # TODO: make compatible with Fedora + pass + def test_cli_setup_authority(): import os @@ -110,12 +116,13 @@ def test_cli_setup_authority(): os.unlink("/etc/nginx/sites-available/ca.conf") if os.path.exists("/etc/nginx/sites-enabled/ca.conf"): os.unlink("/etc/nginx/sites-enabled/ca.conf") + if os.path.exists("/etc/nginx/sites-available/certidude.conf"): + os.unlink("/etc/nginx/sites-available/certidude.conf") + if os.path.exists("/etc/nginx/sites-enabled/certidude.conf"): + os.unlink("/etc/nginx/sites-enabled/certidude.conf") if os.path.exists("/etc/nginx/conf.d/tls.conf"): os.unlink("/etc/nginx/conf.d/tls.conf") - with open("/etc/ipsec.conf", "w") as fh: # TODO: make compatible with Fedora - pass - # Remove OpenVPN stuff if os.path.exists("/etc/openvpn"): for filename in os.listdir("/etc/openvpn"): @@ -167,7 +174,7 @@ def test_cli_setup_authority(): 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']) + result = runner.invoke(cli, ['serve', '-p', '8080', '-l', '127.0.1.1']) assert not result.exception, result.output return @@ -210,7 +217,7 @@ def test_cli_setup_authority(): r = requests.get("http://ca.example.lan/nonexistant.html") assert r.status_code == 404, r.text r = requests.get("http://ca.example.lan/../nonexistant.html") - assert r.status_code == 403, r.text + assert r.status_code == 400, r.text r = client().simulate_get("/") assert r.status_code == 200, r.text @@ -219,7 +226,7 @@ def test_cli_setup_authority(): r = client().simulate_get("/nonexistant.html") assert r.status_code == 404, r.text r = client().simulate_get("/../nonexistant.html") - assert r.status_code == 403, r.text + assert r.status_code == 400, r.text # Test request submission buf = generate_csr(cn=u"test") @@ -232,6 +239,7 @@ def test_cli_setup_authority(): headers={"content-type":"application/pkcs10"}) assert r.status_code == 202 # success assert "Stored request " in inbox.pop(), inbox + assert os.path.exists("/var/lib/certidude/ca.example.lan/requests/test.pem") # Test request deletion r = client().simulate_delete("/api/request/test/") @@ -242,6 +250,7 @@ def test_cli_setup_authority(): r = client().simulate_delete("/api/request/test/", headers={"User-Agent":UA_FEDORA_FIREFOX, "Authorization":admintoken}) assert r.status_code == 403, r.text # CSRF prevented + assert os.path.exists("/var/lib/certidude/ca.example.lan/requests/test.pem") r = client().simulate_delete("/api/request/test/", headers={"Authorization":admintoken}) assert r.status_code == 200, r.text @@ -632,6 +641,7 @@ def test_cli_setup_authority(): assert not result.exception, result.output assert "Writing certificate to:" in result.output, result.output assert os.path.exists("/tmp/ca.example.lan/server_cert.pem") + assert os.path.exists("/etc/openvpn/site-to-client.conf") # Reset config os.unlink("/etc/certidude/client.conf") @@ -649,6 +659,7 @@ def test_cli_setup_authority(): result = runner.invoke(cli, ["request", "--no-wait"]) assert not result.exception, result.output assert "Writing certificate to:" in result.output, result.output + assert os.path.exists("/etc/openvpn/client-to-site.conf") # TODO: assert key, req, cert paths were included correctly in OpenVPN config # TODO: test client verification with curl @@ -664,6 +675,7 @@ def test_cli_setup_authority(): result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec.example.lan", "ca.example.lan"]) assert not result.exception, result.output + assert open("/etc/ipsec.secrets").read() == ": RSA /tmp/ca.example.lan/server_key.pem\n" result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec.example.lan", "ca.example.lan"]) assert not result.exception, result.output # client conf already exists, remove to regenerate @@ -766,6 +778,13 @@ def test_cli_setup_authority(): with open("/run/certidude/server.pid") as fh: os.kill(int(fh.read()), 1) + # Note: STORAGE_PATH was mangled above, hence it's /tmp not /var/lib/certidude + assert open("/etc/apparmor.d/local/usr.lib.ipsec.charon").read() == "/tmp/** r,\n" + assert len(inbox) == 0, inbox # Make sure all messages were checked os.waitpid(server_pid, 0) + + os.system("service nginx stop") + os.system("service openvpn stop") + os.system("ipsec stop")