mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-30 17:09:19 +00:00 
			
		
		
		
	tests: Handle forking
This commit is contained in:
		| @@ -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 | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								README.rst
									
									
									
									
									
								
							| @@ -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: | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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()) | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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,9 +816,6 @@ 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(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) |     click.echo("Placing authority files in %s" % directory) | ||||||
|  |  | ||||||
| @@ -831,7 +829,6 @@ 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") | ||||||
| @@ -900,8 +897,6 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, | |||||||
|  |  | ||||||
|     _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") |     _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") | ||||||
|     os.setgid(gid) |     os.setgid(gid) | ||||||
|     else: |  | ||||||
|         click.echo("Not root, skipping user and system config creation") |  | ||||||
|  |  | ||||||
|     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,7 +1194,7 @@ 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) | ||||||
|  |  | ||||||
| @@ -1208,12 +1203,21 @@ def certidude_serve(port, listen, fork): | |||||||
|         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") |  | ||||||
|  |  | ||||||
|  |         try: | ||||||
|             asyncore.loop() |             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() | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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)) | ||||||
|   | |||||||
| @@ -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] | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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']) |  | ||||||
|  |     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 |         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 | ||||||
|  |  | ||||||
|  |     # Some commands have side effects (setuid, setgid etc) | ||||||
|  |     child_pid = os.fork() | ||||||
|  |     if not child_pid: | ||||||
|         result = runner.invoke(cli, ['sign', 'test', '-o']) |         result = runner.invoke(cli, ['sign', 'test', '-o']) | ||||||
|         assert not result.exception, result.output |         assert not result.exception, result.output | ||||||
|     result = runner.invoke(cli, ['revoke', 'test']) |         return | ||||||
|     assert not result.exception, result.output |     else: | ||||||
|     authority.generate_ovpn_bundle(u"test2") |         os.waitpid(child_pid, 0) | ||||||
|     authority.generate_pkcs12_bundle(u"test3") |     assert os.getuid() == 0 and os.getgid() == 0, "Serve dropped permissions incorrectly!" | ||||||
|     result = runner.invoke(cli, ['list', '-srv']) |  | ||||||
|     assert not result.exception, result.output |  | ||||||
|     result = runner.invoke(cli, ['cron']) |  | ||||||
|     assert not result.exception, result.output |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     # 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()) | ||||||
|  |  | ||||||
|  |     child_pid = os.fork() | ||||||
|  |     if not child_pid: | ||||||
|         result = runner.invoke(cli, ['sign', 'vpn.example.lan']) |         result = runner.invoke(cli, ['sign', 'vpn.example.lan']) | ||||||
|         assert not result.exception, result.output |         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) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user