1
0
mirror of https://github.com/laurivosandi/certidude synced 2024-12-22 16:25:17 +00:00

tests: Handle forking

This commit is contained in:
Lauri Võsandi 2017-05-03 07:04:52 +00:00
parent 8f9da9c2f1
commit 649863a77e
9 changed files with 256 additions and 211 deletions

View File

@ -20,7 +20,10 @@ script:
- sudo useradd adminbot -G sudo -p '$1$PBkf5waA$n9EV6WJ7PS6lyGWkgeTPf1' - sudo useradd adminbot -G sudo -p '$1$PBkf5waA$n9EV6WJ7PS6lyGWkgeTPf1'
- sudo useradd userbot -G users -p '$1$PBkf5waA$n9EV6WJ7PS6lyGWkgeTPf1' - sudo useradd userbot -G users -p '$1$PBkf5waA$n9EV6WJ7PS6lyGWkgeTPf1'
- sudo adduser --system --no-create-home --group certidude - 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: cache:
directories: directories:
- $HOME/.cache/pip - $HOME/.cache/pip

View File

@ -353,7 +353,17 @@ To install the package from the source:
.. code:: bash .. 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: To uninstall:

View File

@ -17,18 +17,6 @@ from certidude import const, config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CertificateStatusResource(object):
"""
openssl ocsp -issuer CAcert_class1.pem -serial 0x<serial no in hex> -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): class CertificateAuthorityResource(object):
def on_get(self, req, resp): def on_get(self, req, resp):
@ -195,7 +183,6 @@ def certidude_app(log_handlers=[]):
app.req_options.auto_parse_form_urlencoded = True app.req_options.auto_parse_form_urlencoded = True
# Certificate authority API calls # Certificate authority API calls
app.add_route("/api/ocsp/", CertificateStatusResource())
app.add_route("/api/certificate/", CertificateAuthorityResource()) app.add_route("/api/certificate/", CertificateAuthorityResource())
app.add_route("/api/revoked/", RevocationListResource()) app.add_route("/api/revoked/", RevocationListResource())
app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource()) app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource())

View File

@ -96,7 +96,7 @@ def signer_exec(cmd, *bits):
sock.sendall(b"\n\n") sock.sendall(b"\n\n")
buf = sock.recv(8192) buf = sock.recv(8192)
if not buf: if not buf:
raise raise Exception("Connection lost")
return buf return buf

View File

@ -15,8 +15,9 @@ import subprocess
import sys import sys
from configparser import ConfigParser, NoOptionError, NoSectionError from configparser import ConfigParser, NoOptionError, NoSectionError
from certidude.helpers import certidude_request_certificate 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 datetime import datetime, timedelta
from time import sleep
import const import const
logger = logging.getLogger(__name__) 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) click.echo("Using templates from %s" % template_path)
if not directory: if not directory:
if os.getuid(): directory = os.path.join("/var/lib/certidude", common_name)
directory = os.path.join(os.path.expanduser("~/.certidude"), common_name)
else:
directory = os.path.join("/var/lib/certidude", common_name)
click.echo("Placing authority files in %s" % directory) click.echo("Placing authority files in %s" % directory)
certificate_url = "http://%s/api/certificate/" % common_name 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_key = os.path.join(directory, "ca_key.pem")
ca_crt = os.path.join(directory, "ca_crt.pem") ca_crt = os.path.join(directory, "ca_crt.pem")
if os.getuid() == 0: try:
try: pwd.getpwnam("certidude")
pwd.getpwnam("certidude") click.echo("User 'certidude' already exists")
click.echo("User 'certidude' already exists") except KeyError:
except KeyError: cmd = "adduser", "--system", "--no-create-home", "--group", "certidude"
cmd = "adduser", "--system", "--no-create-home", "--group", "certidude" if subprocess.call(cmd):
if subprocess.call(cmd): click.echo("Failed to create system user 'certidude'")
click.echo("Failed to create system user 'certidude'") return 255
return 255
if os.path.exists(kerberos_keytab): if os.path.exists(kerberos_keytab):
click.echo("Service principal keytab found in '%s'" % 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)
else: 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): if not os.path.exists(const.CONFIG_DIR):
click.echo("Creating %s" % 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.argument("common_name")
@click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN") @click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN")
def certidude_sign(common_name, overwrite): def certidude_sign(common_name, overwrite):
drop_privileges()
from certidude import authority from certidude import authority
cert = authority.sign(common_name, overwrite) cert = authority.sign(common_name, overwrite)
@ -1128,6 +1124,7 @@ def certidude_sign(common_name, overwrite):
@click.command("revoke", help="Revoke certificate") @click.command("revoke", help="Revoke certificate")
@click.argument("common_name") @click.argument("common_name")
def certidude_revoke(common_name): def certidude_revoke(common_name):
drop_privileges()
from certidude import authority from certidude import authority
authority.revoke(common_name) authority.revoke(common_name)
@ -1144,6 +1141,7 @@ def certidude_cron():
os.rename(path, expired_path) os.rename(path, expired_path)
click.echo("Moved %s to %s" % (path, expired_path)) click.echo("Moved %s to %s" % (path, expired_path))
@click.command("serve", help="Run server") @click.command("serve", help="Run server")
@click.option("-p", "--port", default=80, help="Listen port") @click.option("-p", "--port", default=80, help="Listen port")
@click.option("-l", "--listen", default="0.0.0.0", help="Listen address") @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): def certidude_serve(port, listen, fork):
from setproctitle import setproctitle from setproctitle import setproctitle
from certidude.signer import SignServer from certidude.signer import SignServer
from certidude import const from certidude import authority, const
click.echo("Using configuration from: %s" % const.CONFIG_PATH) click.echo("Using configuration from: %s" % const.CONFIG_PATH)
log_handlers = [] log_handlers = []
from certidude import config from certidude import config
@ -1166,10 +1165,7 @@ def certidude_serve(port, listen, fork):
# TODO: umask! # TODO: umask!
import pwd
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
restricted_groups = []
restricted_groups.append(gid)
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
rh = RotatingFileHandler("/var/log/certidude.log", maxBytes=1048576*5, backupCount=5) rh = RotatingFileHandler("/var/log/certidude.log", maxBytes=1048576*5, backupCount=5)
rh.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")) 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 Spawn signer process
""" """
child_pid = os.fork() if os.path.exists(const.SIGNER_SOCKET_PATH):
os.unlink(const.SIGNER_SOCKET_PATH)
if child_pid: if not os.fork():
pass
else:
click.echo("Signer process spawned with PID %d at %s" % (os.getpid(), const.SIGNER_SOCKET_PATH)) click.echo("Signer process spawned with PID %d at %s" % (os.getpid(), const.SIGNER_SOCKET_PATH))
setproctitle("[signer]") setproctitle("[signer]")
@ -1199,21 +1194,30 @@ def certidude_serve(port, listen, fork):
server = SignServer() server = SignServer()
# Drop privileges # Drop privileges
if not os.getuid(): _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
os.chown(const.SIGNER_SOCKET_PATH, uid, gid) os.chown(const.SIGNER_SOCKET_PATH, uid, gid)
os.chmod(const.SIGNER_SOCKET_PATH, 0770) os.chmod(const.SIGNER_SOCKET_PATH, 0770)
click.echo("Dropping privileges of signer") click.echo("Dropping privileges of signer")
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("nobody") _, _, uid, gid, gecos, root, shell = pwd.getpwnam("nobody")
os.setgroups([]) os.setgroups([])
os.setgid(gid) os.setgid(gid)
os.setuid(uid) os.setuid(uid)
else:
click.echo("Not dropping privileges of signer process")
asyncore.loop() try:
asyncore.loop()
except asyncore.ExitNow:
pass
click.echo("Signer was shut down")
return 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" % click.echo("Users subnets: %s" %
", ".join([str(j) for j in config.USER_SUBNETS])) ", ".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)) click.echo("Serving API at %s:%d" % (listen, port))
from wsgiref.simple_server import make_server, WSGIServer from wsgiref.simple_server import make_server, WSGIServer
from SocketServer import ThreadingMixIn, ForkingMixIn from SocketServer import ForkingMixIn
from certidude.api import certidude_app from certidude.api import certidude_app
class ThreadingWSGIServer(ForkingMixIn, WSGIServer): class ThreadingWSGIServer(ForkingMixIn, WSGIServer):
@ -1251,13 +1255,6 @@ def certidude_serve(port, listen, fork):
if os.path.exists("/etc/cron.hourly/certidude"): if os.path.exists("/etc/cron.hourly/certidude"):
os.system("/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: if config.EVENT_SOURCE_PUBLISH:
from certidude.push import EventSourceLogHandler from certidude.push import EventSourceLogHandler
log_handlers.append(EventSourceLogHandler()) log_handlers.append(EventSourceLogHandler())
@ -1281,17 +1278,13 @@ def certidude_serve(port, listen, fork):
atexit.register(exit_handler) atexit.register(exit_handler)
logger.debug("Started Certidude at %s", const.FQDN) logger.debug("Started Certidude at %s", const.FQDN)
drop_privileges()
# Drop privileges def quit_handler(*args, **kwargs):
os.setgroups(restricted_groups) click.echo("Shutting down HTTP server...")
os.setgid(gid) import threading
os.setuid(uid) threading.Thread(target=httpd.shutdown).start()
signal.signal(signal.SIGHUP, quit_handler)
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)
httpd.serve_forever() httpd.serve_forever()

View File

@ -3,6 +3,27 @@ import os
import click import click
import subprocess 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): def ip_network(j):
import ipaddress import ipaddress
return ipaddress.ip_network(unicode(j)) return ipaddress.ip_network(unicode(j))

View File

@ -6,16 +6,15 @@ import sys
KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096 KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096
RUN_DIR = "/run/certidude" 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") CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf")
CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf") CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf")
SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.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_PID_PATH = os.path.join(RUN_DIR, "server.pid")
SERVER_LOG_PATH = os.path.join(CONFIG_DIR, "server.log") if os.getuid() else "/var/log/certidude-server.log" SERVER_LOG_PATH = "/var/log/certidude-server.log"
SIGNER_SOCKET_PATH = os.path.join(CONFIG_DIR, "signer.sock") if os.getuid() else "/run/certidude/signer.sock" SIGNER_SOCKET_PATH = "/run/certidude/signer.sock"
SIGNER_PID_PATH = os.path.join(CONFIG_DIR if os.getuid() else RUN_DIR, "signer.pid") SIGNER_PID_PATH = os.path.join(RUN_DIR, "signer.pid")
SIGNER_LOG_PATH = os.path.join(CONFIG_DIR, "signer.log") if os.getuid() else "/var/log/certidude-signer.log" SIGNER_LOG_PATH = "/var/log/certidude-signer.log"
try: try:
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]

View File

@ -55,8 +55,14 @@ class SignHandler(asynchat.async_chat):
self.send(crl.public_bytes(Encoding.PEM)) self.send(crl.public_bytes(Encoding.PEM))
elif cmd == "ocsp-request": elif cmd == "ping":
NotImplemented # TODO: Implement OCSP self.send("pong")
self.close()
elif cmd == "exit":
self.send("ok")
self.close()
raise asyncore.ExitNow()
elif cmd == "sign-request": elif cmd == "sign-request":
# Only common name and public key are used from request # Only common name and public key are used from request

View File

@ -1,10 +1,10 @@
import subprocess
import pwd import pwd
from click.testing import CliRunner from click.testing import CliRunner
from datetime import datetime, timedelta from datetime import datetime, timedelta
from time import sleep
import pytest import pytest
import shutil
# pkill py && rm -Rfv ~/.certidude && TRAVIS=1 py.test tests import os
runner = CliRunner() runner = CliRunner()
@ -33,8 +33,11 @@ def generate_csr(cn=None):
return buf return buf
def test_cli_setup_authority(): def test_cli_setup_authority():
import shutil
import os 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"): if os.path.exists("/run/certidude/signer.pid"):
with open("/run/certidude/signer.pid") as fh: with open("/run/certidude/signer.pid") as fh:
try: try:
@ -69,7 +72,12 @@ def test_cli_setup_authority():
from certidude import const from certidude import const
result = runner.invoke(cli, ['setup', 'authority']) result = runner.invoke(cli, ['setup', 'authority'])
os.setgid(0) # Restore GID
os.umask(0022)
assert not result.exception, result.output assert not result.exception, result.output
assert os.getuid() == 0 and os.getgid() == 0, "Serve dropped permissions incorrectly!"
from certidude import config, authority from certidude import config, authority
assert authority.ca_cert.serial_number >= 0x100000000000000000000000000000000000000 assert authority.ca_cert.serial_number >= 0x100000000000000000000000000000000000000
@ -79,19 +87,20 @@ def test_cli_setup_authority():
# Start server before any signing operations are performed # Start server before any signing operations are performed
config.CERTIFICATE_RENEWAL_ALLOWED = True 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 import requests
# Test CA certificate fetch # Test CA certificate fetch
buf = open("/var/lib/certidude/ca.example.lan/ca_crt.pem").read() 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") r = requests.get("http://ca.example.lan/api/certificate")
assert r.status_code == 200 assert r.status_code == 200
assert r.headers.get('content-type') == "application/x-x509-ca-cert" 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 # Check that we can retrieve empty CRL
assert authority.export_crl(), "Failed to export 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 assert r.status_code == 200, r.text
@ -116,13 +125,10 @@ def test_cli_setup_authority():
assert not result.exception, result.output assert not result.exception, result.output
# Test static # 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") 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 # Test request submission
buf = generate_csr(cn=u"test") 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 assert r.status_code == 202 # already exists, same keypair so it's ok
r = client().simulate_post("/api/request/", r = client().simulate_post("/api/request/",
query_string="wait=1", query_string="wait=true",
body=buf, body=buf,
headers={"content-type":"application/pkcs10"}) headers={"content-type":"application/pkcs10"})
assert r.status_code == 303 # redirect to long poll 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"}) r = client().simulate_get("/api/request/nonexistant/", headers={"Accept":"application/json"})
assert r.status_code == 404 # nonexistant common names 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, body=buf,
headers={"content-type":"application/pkcs10"}) headers={"content-type":"application/pkcs10"})
assert r.status_code == 200 # autosign successful assert r.status_code == 200 # autosign successful
@ -176,17 +183,16 @@ def test_cli_setup_authority():
# Test command line interface # Test command line interface
result = runner.invoke(cli, ['list', '-srv']) result = runner.invoke(cli, ['list', '-srv'])
assert not result.exception, result.output 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 # Test session API call
r = client().simulate_get("/api/", headers={"Authorization":usertoken}) r = client().simulate_get("/api/", headers={"Authorization":usertoken})
@ -203,120 +209,110 @@ def test_cli_setup_authority():
r = client().simulate_get("/api/signed/nonexistant/") r = client().simulate_get("/api/signed/nonexistant/")
assert r.status_code == 404, r.text 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.status_code == 200, r.text
assert r.headers.get('content-type') == "application/x-pem-file" 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.status_code == 200, r.text
assert r.headers.get('content-type') == "application/json" 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 assert r.status_code == 415, r.text
# Test revocations API call # Test revocations API call
r = client().simulate_get("/api/revoked/", r = client().simulate_get("/api/revoked/",
headers={"Accept":"application/x-pem-file"}) headers={"Accept":"application/x-pem-file"})
assert r.status_code == 200, r.text 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 = 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.headers.get('content-type') == "application/x-pem-file" assert r.headers.get('content-type') == "application/x-pem-file"
r = client().simulate_get("/api/revoked/") r = client().simulate_get("/api/revoked/")
assert r.status_code == 200, r.text assert r.status_code == 200, r.text
assert r.headers.get('content-type') == "application/x-pkcs7-crl" 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/", r = client().simulate_get("/api/revoked/",
headers={"Accept":"text/plain"}) headers={"Accept":"text/plain"})
assert r.status_code == 415, r.text 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"}) headers={"Accept":"application/x-pem-file"})
assert r.status_code == 303, r.text assert r.status_code == 303, r.text
# Test attribute fetching API call # 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 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 assert r.status_code == 404, r.text
# Insert lease as if VPN gateway had submitted it # Insert lease as if VPN gateway had submitted it
path, _, _ = authority.get_signed("test2") path, _, _ = authority.get_signed("test")
from xattr import setxattr from xattr import setxattr
setxattr(path, "user.lease.address", b"127.0.0.1") setxattr(path, "user.lease.address", b"127.0.0.1")
setxattr(path, "user.lease.last_seen", b"random") 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 assert r.status_code == 200, r.text
# Test lease retrieval # 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 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 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.status_code == 200, r.text
assert r.headers.get('content-type') == "application/json; charset=UTF-8" assert r.headers.get('content-type') == "application/json; charset=UTF-8"
# Tags should not be visible anonymously # 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 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 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 assert r.status_code == 200, r.text
# Tags can be added only by admin # 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 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}) headers={"Authorization":usertoken})
assert r.status_code == 403, r.text 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", body="key=other&value=something",
headers={"content-type": "application/x-www-form-urlencoded", "Authorization":admintoken}) headers={"content-type": "application/x-www-form-urlencoded", "Authorization":admintoken})
assert r.status_code == 200, r.text assert r.status_code == 200, r.text
# Tags can be overwritten only by admin # 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 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}) headers={"Authorization":usertoken})
assert r.status_code == 403, r.text 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", body="value=else",
headers={"content-type": "application/x-www-form-urlencoded", "Authorization":admintoken}) headers={"content-type": "application/x-www-form-urlencoded", "Authorization":admintoken})
assert r.status_code == 200, r.text assert r.status_code == 200, r.text
# Tags can be deleted only by admin # 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 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}) headers={"Authorization":usertoken})
assert r.status_code == 403, r.text 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}) headers={"content-type": "application/x-www-form-urlencoded", "Authorization":admintoken})
assert r.status_code == 200, r.text assert r.status_code == 200, r.text
# Test revocation # Test revocation
r = client().simulate_delete("/api/signed/test2/") r = client().simulate_delete("/api/signed/test/")
assert r.status_code == 401, r.text assert r.status_code == 401, r.text
r = client().simulate_delete("/api/signed/test2/", r = client().simulate_delete("/api/signed/test/",
headers={"Authorization":usertoken}) headers={"Authorization":usertoken})
assert r.status_code == 403, r.text assert r.status_code == 403, r.text
r = client().simulate_delete("/api/signed/test2/", r = client().simulate_delete("/api/signed/test/",
headers={"Authorization":admintoken}) headers={"Authorization":admintoken})
assert r.status_code == 200, r.text 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 # Log can be read only by admin
@ -351,7 +347,8 @@ def test_cli_setup_authority():
query_string="u=userbot&t=1493184342&c=ac9b71421d5741800c5a4905b20c1072594a2df863e60ba836464888786bf2a6", query_string="u=userbot&t=1493184342&c=ac9b71421d5741800c5a4905b20c1072594a2df863e60ba836464888786bf2a6",
headers={"content-type": "application/x-www-form-urlencoded", "Authorization":admintoken}) headers={"content-type": "application/x-www-form-urlencoded", "Authorization":admintoken})
assert r2.status_code == 403 # invalid checksum 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) " 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"}) "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 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.status_code == 200 # token consumed by anyone on unknown device
assert r2.headers.get('content-type') == "application/x-pkcs12" assert r2.headers.get('content-type') == "application/x-pkcs12"
result = runner.invoke(cli, ['setup', 'openvpn', 'server', "-cn", "vpn.example.lan", "ca.example.lan"]) result = runner.invoke(cli, ['setup', 'openvpn', 'server', "-cn", "vpn.example.lan", "ca.example.lan"])
assert not result.exception, result.output assert not result.exception, result.output
@ -379,9 +375,39 @@ def test_cli_setup_authority():
# pregen dhparam # pregen dhparam
result = runner.invoke(cli, ["request", "--no-wait"]) 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()) 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"]) result = runner.invoke(cli, ["request", "--no-wait"])
assert not result.exception, result.output assert not result.exception, result.output
result = runner.invoke(cli, ["request", "--renew"]) result = runner.invoke(cli, ["request", "--renew"])
assert not result.exception, result.output 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)