From ad1f9c23382a399d68911e6b3c0c4d96dbc7245e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lauri=20V=C3=B5sandi?=
Date: Thu, 17 May 2018 09:00:13 +0000
Subject: [PATCH] Several updates #5
* Better 'systemctl stop certidude' signal handling
* Add 502.json for better bad gateway error handling
* Generate UUID for .sswan and .mobileconfig files from service name
* More detailed token list view in admin interface
* Improved testcases
---
.gitignore | 4 +
README.rst | 16 +--
certidude/api/utils/firewall.py | 2 +-
certidude/cli.py | 60 ++++++-----
certidude/const.py | 2 +-
certidude/static/502.json | 4 +
certidude/static/js/certidude.js | 117 ++++++++++++---------
certidude/static/snippets/ios.mobileconfig | 2 +-
certidude/static/snippets/windows.ps1 | 2 +-
certidude/static/views/authority.html | 9 +-
certidude/static/views/enroll.html | 20 +++-
certidude/static/views/token.html | 10 ++
certidude/templates/server/nginx.conf | 23 ++--
certidude/templates/server/server.conf | 6 +-
certidude/templates/server/systemd.service | 6 +-
certidude/tokens.py | 8 +-
tests/test_cli.py | 93 ++++++++++++----
17 files changed, 248 insertions(+), 136 deletions(-)
create mode 100644 certidude/static/502.json
create mode 100644 certidude/static/views/token.html
diff --git a/.gitignore b/.gitignore
index a493858..287fc8d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -69,3 +69,7 @@ lextab.py
yacctab.py
.pytest_cache
*~
+certidude/static/coverage/
+*.tar
+*.bz2
+*.gz
diff --git a/README.rst b/README.rst
index ae790ea..45703aa 100644
--- a/README.rst
+++ b/README.rst
@@ -301,8 +301,8 @@ Clone the repository:
.. code:: bash
- git clone https://github.com/laurivosandi/certidude
- cd certidude
+ git clone https://github.com/laurivosandi/certidude /srv/certidude
+ cd /srv/certidude
Install dependencies as shown above and additionally:
@@ -316,15 +316,17 @@ To install the package from the source tree:
pip3 install -e .
-To run tests and measure code coverage grab a clean VM or container:
+To run tests and measure code coverage grab a clean VM or container,
+set hostname to ca.example.lan, export environment variable COVERAGE_PROCESS_START globally and run:
.. code:: bash
pip3 install codecov pytest-cov
- rm .coverage*
- COVERAGE_FILE=/tmp/.coverage TRAVIS=1 coverage run --parallel-mode --source certidude -m py.test tests --capture=sys
+ rm /tmp/.coverage*
+ COVERAGE_PROCESS_START=/srv/certidude/.coveragerc py.test tests --capture=sys
coverage combine
coverage report
+ coverage html -i
To uninstall:
@@ -342,7 +344,7 @@ vanilla Ubuntu 16.04 or container:
.. code:: bash
rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl
- apt install --download-only python3-pip
+ apt install python3-pip
pip3 wheel --wheel-dir=/var/cache/certidude/wheels -r requirements.txt
pip3 wheel --wheel-dir=/var/cache/certidude/wheels .
tar -cf certidude-client.tar /var/cache/certidude/wheels
@@ -363,6 +365,8 @@ Transfer certidude-server.tar or certidude-client.tar to the target machine and
Proceed to bootstrap authority without installing packages or assembling assets:
+.. code:: bash
+
certidude setup authority --skip-packages --skip-assets [--elliptic-curve] [--organization "Mycorp LLC"]
Note it's highly recommended to enable nginx PPA in the target machine
diff --git a/certidude/api/utils/firewall.py b/certidude/api/utils/firewall.py
index 7622264..9d34dee 100644
--- a/certidude/api/utils/firewall.py
+++ b/certidude/api/utils/firewall.py
@@ -231,7 +231,7 @@ def authorize_server(func):
def wrapped(resource, req, resp, *args, **kwargs):
buf = req.get_header("X-SSL-CERT")
if not buf:
- logger.info("No TLS certificate presented to access administrative API call")
+ logger.info("No TLS certificate presented to access administrative API call from %s" % req.context.get("remote_addr"))
raise falcon.HTTPForbidden("Forbidden", "Machine not authorized to perform the operation")
header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii"))
diff --git a/certidude/cli.py b/certidude/cli.py
index 3e87a07..1280e92 100755
--- a/certidude/cli.py
+++ b/certidude/cli.py
@@ -24,6 +24,16 @@ from glob import glob
from ipaddress import ip_network
from oscrypto import asymmetric
+try:
+ import coverage
+ cov = coverage.process_startup()
+ if cov:
+ click.echo("Enabling coverage tracking")
+ else:
+ click.echo("Coverage tracking not requested")
+except ImportError:
+ pass
+
logger = logging.getLogger(__name__)
# http://www.mad-hacking.net/documentation/linux/security/ssl-tls/creating-ca.xml
@@ -55,7 +65,7 @@ def setup_client(prefix="client_", dh=False):
if not os.path.exists(path):
rpm("openssl")
apt("openssl")
- cmd = "openssl", "dhparam", "-out", path, ("1024" if os.getenv("TRAVIS") else "2048")
+ cmd = "openssl", "dhparam", "-out", path, str(const.KEY_SIZE)
subprocess.check_call(cmd)
arguments["dhparam_path"] = path
@@ -1008,10 +1018,11 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat
@click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN)
@click.option("--skip-assets", is_flag=True, help="Don't attempt to assemble JS/CSS/font assets")
@click.option("--skip-packages", is_flag=True, help="Don't attempt to install apt/pip/npm packages")
+@click.option("--packages-only", is_flag=True, help="Install only apt/pip/npm packages")
@click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair")
@click.option("--subordinate", is_flag=True, help="Set up subordinate CA instead of root CA")
@fqdn_required
-def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, title, skip_assets, skip_packages, elliptic_curve, subordinate):
+def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, title, skip_assets, skip_packages, elliptic_curve, subordinate, packages_only):
assert subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) in (b"trusty\n", b"xenial\n", b"bionic\n"), "Only Ubuntu 16.04 supported at the moment"
assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root"
@@ -1055,8 +1066,11 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_confi
else:
click.echo("Web server nginx already installed")
- if not os.path.exists("/usr/bin/node"):
- os.symlink("/usr/bin/nodejs", "/usr/bin/node")
+ if not os.path.exists("/usr/bin/node"):
+ os.symlink("/usr/bin/nodejs", "/usr/bin/node")
+
+ if packages_only:
+ return
# Generate secret for tokens
token_url = "https://" + const.FQDN + "/#action=enroll&token=%(token)s&router=%(router)s&protocol=ovpn"
@@ -1150,8 +1164,9 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_confi
with open("/etc/systemd/system/certidude.service", "w") as fh:
fh.write(env.get_template("server/systemd.service").render(vars()))
click.echo("File /etc/systemd/system/certidude.service created")
+ os.system("systemctl daemon-reload")
else:
- click.echo("Not systemd based OS, don't know how to set up initscripts")
+ raise NotImplementedError("Not systemd based OS, don't know how to set up initscripts")
# Set umask to 0022
os.umask(0o022)
@@ -1231,7 +1246,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_confi
os.umask(0o177) # 600
if not os.path.exists(dhparam_path):
- cmd = "openssl", "dhparam", "-out", dhparam_path, ("1024" if os.getenv("TRAVIS") else str(const.KEY_SIZE))
+ cmd = "openssl", "dhparam", "-out", dhparam_path, str(const.KEY_SIZE)
subprocess.check_call(cmd)
if os.path.exists(tls_config.name):
@@ -1362,15 +1377,6 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_confi
assert os.stat("/etc/nginx/sites-available/certidude.conf").st_mode == 0o100600
assert os.stat("/etc/certidude/server.conf").st_mode == 0o100600
- click.echo("Enabling and starting Certidude backend")
- os.system("systemctl enable certidude")
- os.system("systemctl restart certidude")
- click.echo("Enabling and starting nginx")
- os.system("systemctl enable nginx")
- os.system("systemctl start nginx")
- os.system("systemctl reload nginx")
- click.echo()
-
click.echo("To enable e-mail notifications install Postfix as sattelite system and set mailer address in %s" % const.SERVER_CONFIG_PATH)
click.echo()
click.echo("Use following commands to inspect the newly created files:")
@@ -1383,6 +1389,15 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_confi
click.echo()
click.echo(" echo 'select * from log;' | sqlite3 /var/lib/certidude/meta/db.sqlite")
click.echo(" echo 'select * from token;' | sqlite3 /var/lib/certidude/meta/db.sqlite")
+ click.echo()
+ click.echo("Enabling Certidude backend and nginx...")
+ os.system("systemctl enable certidude")
+ os.system("systemctl enable nginx")
+ click.echo("To (re)start services:")
+ click.echo()
+ click.echo(" systemctl restart certidude")
+ click.echo(" systemctl restart nginx")
+ click.echo()
return 0
@@ -1598,14 +1613,6 @@ def certidude_serve(port, listen, fork):
with open(const.SERVER_PID_PATH, "w") as pidfile:
pidfile.write("%d\n" % pid)
- def cleanup_handler(*args):
- push.publish("server-stopped")
- logger.debug("Shutting down Certidude")
- sys.exit(0) # TODO: use another code, needs test refactor
-
- import signal
- signal.signal(signal.SIGTERM, cleanup_handler) # Handle SIGTERM from systemd
-
push.publish("server-started")
logger.debug("Started Certidude at %s", const.FQDN)
@@ -1613,7 +1620,10 @@ def certidude_serve(port, listen, fork):
try:
httpd.serve_forever()
except KeyboardInterrupt:
- cleanup_handler() # FIXME
+ click.echo("Caught Ctrl-C, exiting...")
+ push.publish("server-stopped")
+ logger.debug("Shutting down Certidude")
+ return
@click.command("yubikey", help="Set up Yubikey as client authentication token")
@@ -1676,7 +1686,7 @@ def certidude_token_list():
token_manager = TokenManager(config.TOKEN_DATABASE)
cols = "uuid", "expires", "subject", "state"
now = datetime.utcnow()
- for token in token_manager.list(expired=True, used=True, token=True):
+ for token in token_manager.list(expired=True, used=True):
token["state"] = "used" if token.get("used") else ("valid" if token.get("expires") > now else "expired")
print(";".join([str(token.get(col)) for col in cols]))
diff --git a/certidude/const.py b/certidude/const.py
index e43a6fe..2782067 100644
--- a/certidude/const.py
+++ b/certidude/const.py
@@ -5,7 +5,7 @@ import socket
import sys
from datetime import timedelta
-KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096
+KEY_SIZE = 1024 if os.getenv("COVERAGE_PROCESS_START") else 4096
CURVE_NAME = "secp384r1"
RE_FQDN = "^(([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])?$"
RE_HOSTNAME = "^[a-z0-9]([a-z0-9\-_]{0,61}[a-z0-9])?$"
diff --git a/certidude/static/502.json b/certidude/static/502.json
new file mode 100644
index 0000000..4f302ba
--- /dev/null
+++ b/certidude/static/502.json
@@ -0,0 +1,4 @@
+{
+ "title": "502 Bad Gateway",
+ "description": "It seems the server had bit of a hiccup, perhaps this helps: systemctl restart certidude && journalctl -f"
+}
diff --git a/certidude/static/js/certidude.js b/certidude/static/js/certidude.js
index 8a60fe7..8a9dd33 100644
--- a/certidude/static/js/certidude.js
+++ b/certidude/static/js/certidude.js
@@ -40,7 +40,7 @@ function onKeyGen() {
}
}
- window.identifier = prefix + "-" + dig.digest().toHex().substring(0, 8);
+ window.identifier = prefix + "-" + dig.digest().toHex().substring(0, 5);
console.info("Device identifier:", identifier);
window.common_name = query.subject + "@" + identifier;
@@ -79,9 +79,21 @@ function onKeyGen() {
options[i].style.display = "block";
}
}
+ $(".option.any").show();
}
function onEnroll(encoding) {
+ console.info("Service name:", query.title);
+ var md = forge.md.md5.create();
+ md.update(query.title);
+ var digest = md.digest().toHex();
+ var service_uuid = digest.substring(0, 8) + "-" +
+ digest.substring(8, 12) + "-" +
+ digest.substring(12, 16) + "-" +
+ digest.substring(16,20) + "-" +
+ digest.substring(20)
+ console.info("Service UUID:", service_uuid);
+
console.info("User agent:", window.navigator.userAgent);
var xhr = new XMLHttpRequest();
xhr.open('GET', "/api/certificate");
@@ -103,62 +115,45 @@ function onEnroll(encoding) {
case 'p12':
var buf = forge.asn1.toDer(p12).getBytes();
var mimetype = "application/x-pkcs12"
- a.download = query.router + ".p12";
+ a.download = query.title + ".p12";
break
case 'sswan':
var buf = JSON.stringify({
- uuid: "a061d140-d3f9-4db7-b2f8-32d6703f4618",
- name: identifier,
+ uuid: service_uuid,
+ name: query.title,
type: "ikev2-cert",
'ike-proposal': 'aes256-sha384-prfsha384-modp2048',
- 'esp-proposal': 'aes128gcm16-aes128gmac-modp2048',
+ 'esp-proposal': 'aes128gcm16-modp2048',
remote: { addr: query.router },
local: { p12: forge.util.encode64(forge.asn1.toDer(p12).getBytes()) }
});
console.info("Buf is:", buf);
var mimetype = "application/vnd.strongswan.profile"
- a.download = query.router + ".sswan";
+ a.download = query.title + ".sswan";
break
case 'ovpn':
var buf = nunjucks.render('snippets/openvpn-client.conf', {
- session: {
- authority: {
- certificate: {
- common_name: "Certidude at " + window.location.hostname,
- algorithm: "rsa"
- }
- },
- service: {
- protocols: query.protocols.split(","),
- routers: [query.router],
- }
- },
+ session: session,
key: forge.pki.privateKeyToPem(keys.privateKey),
cert: xhr2.responseText,
ca: xhr.responseText
});
var mimetype = "application/x-openvpn-profile";
- a.download = query.router + ".ovpn";
+ a.download = query.title + ".ovpn";
break
case 'mobileconfig':
var p12 = forge.pkcs12.toPkcs12Asn1(
keys.privateKey, [cert, ca], "1234", {algorithm: '3des'});
var buf = nunjucks.render('snippets/ios.mobileconfig', {
- session: {
- authority: {
- certificate: {
- common_name: "Certidude at " + window.location.hostname,
- algorithm: "rsa"
- }
- }
- },
+ session: session,
+ title: query.title,
common_name: common_name,
gateway: query.router,
p12: forge.util.encode64(forge.asn1.toDer(p12).getBytes()),
ca: forge.util.encode64(forge.asn1.toDer(forge.pki.certificateToAsn1(ca)).getBytes())
});
var mimetype = "application/x-apple-aspen-config";
- a.download = query.router + ".mobileconfig";
+ a.download = query.title + ".mobileconfig";
break
}
a.href = "data:" + mimetype + ";base64," + forge.util.encode64(buf);
@@ -193,27 +188,55 @@ function onHashChanged() {
console.info("Hash is now:", query);
- if (window.location.protocol != "https:") {
- $.get("/api/certificate/", function(blob) {
- $("#view-dashboard").html(env.render('views/insecure.html', { window: window,
- session: { authority: {
- hostname: window.location.hostname,
- certificate: { blob: blob }}}
- }));
- });
- } else {
- if (query.action == "enroll") {
- $("#view-dashboard").html(env.render('views/enroll.html'));
- var options = document.querySelectorAll(".option");
- for (i = 0; i < options.length; i++) {
- options[i].style.display = "none";
+ $.get({
+ method: "GET",
+ url: "/api/certificate",
+ error: function(response) {
+ if (response.responseJSON) {
+ var msg = response.responseJSON
+ } else {
+ var msg = { title: "Error " + response.status, description: response.statusText }
}
- setTimeout(onKeyGen, 100);
- console.info("Generating key pair...");
- } else {
- loadAuthority(query);
+ $("#view-dashboard").html(env.render('views/error.html', { message: msg }));
+ },
+ success: function(blob) {
+ window.session = {
+ authority: {
+ hostname: window.location.hostname,
+ certificate: {
+ common_name: "Certidude at " + window.location.hostname,
+ algorithm: "rsa",
+ blob: blob
+ }
+ },
+ service: {
+ title: query.title ? query.title : query.router,
+ protocols: query.protocols ? query.protocols.split(",") : null,
+ routers: query.router ? [query.router] : null,
+ }
+ }
+
+ if (window.location.protocol != "https:") {
+ $("#view-dashboard").html(env.render('views/insecure.html', {session:session}));
+ } else {
+ if (query.action == "enroll") {
+ $("#view-dashboard").html(env.render('views/enroll.html', {
+ session:session,
+ token: query.token,
+ }));
+ var options = document.querySelectorAll(".option");
+ for (i = 0; i < options.length; i++) {
+ options[i].style.display = "none";
+ }
+ setTimeout(onKeyGen, 100);
+ console.info("Generating key pair...");
+ } else {
+ loadAuthority(query);
+ }
+ }
}
- }
+ });
+
}
function onTagClicked(e) {
diff --git a/certidude/static/snippets/ios.mobileconfig b/certidude/static/snippets/ios.mobileconfig
index 77d3f61..36ad9f5 100644
--- a/certidude/static/snippets/ios.mobileconfig
+++ b/certidude/static/snippets/ios.mobileconfig
@@ -8,7 +8,7 @@
PayloadIdentifier
org.example.vpn2
PayloadUUID
- 9f93912b-5fd2-4455-99fd-13b9a47b4581
+ {{ service_uuid }}
PayloadType
Configuration
PayloadVersion
diff --git a/certidude/static/snippets/windows.ps1 b/certidude/static/snippets/windows.ps1
index 578d1ef..a286050 100644
--- a/certidude/static/snippets/windows.ps1
+++ b/certidude/static/snippets/windows.ps1
@@ -26,7 +26,7 @@ KeyAlgorithm = ECDSA_P384
KeyLength = 2048
{% endif %}"@ | Out-File req.inf
C:\Windows\system32\certreq.exe -new -f -q req.inf host_csr.pem
-Invoke-WebRequest -TimeoutSec 900 -Uri 'https://{{ session.authority.hostname }}:8443/api/request/?wait=yes&autosign=yes' -InFile host_csr.pem -ContentType application/pkcs10 -Method POST -MaximumRedirection 3 -OutFile host_cert.pem
+Invoke-WebRequest -TimeoutSec 900 -Uri 'https://{{ session.authority.hostname }}:8443/api/{% if token %}token/?uuid={{ token }}{% else %}request/?wait=yes&autosign=yes{% endif %}' -InFile host_csr.pem -ContentType application/pkcs10 -Method POST -MaximumRedirection 3 -OutFile host_cert.pem
# Import certificate
{% if session.authority.certificate.algorithm == "ec" %}Import-Certificate -FilePath host_cert.pem -CertStoreLocation Cert:\LocalMachine\My
diff --git a/certidude/static/views/authority.html b/certidude/static/views/authority.html
index 3c3544a..584033a 100644
--- a/certidude/static/views/authority.html
+++ b/certidude/static/views/authority.html
@@ -231,14 +231,9 @@ curl http://{{ session.authority.hostname }}/api/revoked/?wait=yes -L -H "Accept
Issued tokens:
-
+
{% for token in session.authority.tokens %}
-
- {{ token.subject }}
- {% if token.issuer %}{% if token.issuer != token.subject %}by {{ token.issuer }}{% else %}by himself{% endif %}{% else %}via shell{% endif %},
- expires
- {{ token.expires }}
-
+ {% include "views/token.html" %}
{% endfor %}
diff --git a/certidude/static/views/enroll.html b/certidude/static/views/enroll.html
index e715d80..ba91fea 100644
--- a/certidude/static/views/enroll.html
+++ b/certidude/static/views/enroll.html
@@ -103,7 +103,17 @@ systemctl restart NetworkManager
or
C:\ProgramData\Microsoft\Network\Connections\Pbk
Fetch PKCS#12 container
- Fetch VPN profile
+ Fetch IPSec IKEv2 VPN profile
+
+
+
+
+
+
+
+
Windows
+
To configure IPSec IKEv2 tunnel on Windows, open PowerShell as administrator and copy-paste following:
+
{% include "snippets/windows.ps1" %}
@@ -179,7 +189,7 @@ systemctl restart NetworkManager
Where password for the certificate is prompted, enter 1234.
Hit Done . Go to Settings , open VPN submenu and tap on the VPN profile to connect.
- Fetch VPN profile
+ Fetch IPSec IKEv2 VPN profile
@@ -231,8 +241,8 @@ systemctl restart NetworkManager
-
+
diff --git a/certidude/static/views/token.html b/certidude/static/views/token.html
new file mode 100644
index 0000000..c007076
--- /dev/null
+++ b/certidude/static/views/token.html
@@ -0,0 +1,10 @@
+
+
+
+ {{ token.uuid }}...
+ {{ token.subject }}
+ {% if token.issuer %}{% if token.issuer != token.subject %}by {{ token.issuer }}{% else %}by himself{% endif %}{% else %}via shell{% endif %},
+ expires
+ {{ token.expires }}
+
+
diff --git a/certidude/templates/server/nginx.conf b/certidude/templates/server/nginx.conf
index 7b3b245..b5e4613 100644
--- a/certidude/templates/server/nginx.conf
+++ b/certidude/templates/server/nginx.conf
@@ -21,6 +21,9 @@ ssl_certificate {{ directory }}/signed/{{ common_name }}.pem;
ssl_certificate_key {{ directory }}/self_key.pem;
server {
+ # Uncomment following to automatically redirect to HTTPS
+ #rewrite ^/$ https://$server_name$request_uri? permanent;
+
# Section for serving insecure HTTP, note that this is suitable for
# OCSP, SCEP, CRL-s etc which is already covered by PKI protection mechanisms.
# This also solves the chicken-and-egg problem of deploying the certificates
@@ -33,9 +36,6 @@ server {
proxy_pass http://127.0.1.1:8080/api/;
}
- # Path to static files
- root {{static_path}};
-
# Path to compiled assets
location /assets/ {
alias {{ assets_dir }}/;
@@ -64,8 +64,9 @@ server {
}
{% endif %}
- # Uncomment following to enable HTTPS
- #rewrite ^/$ https://$server_name$request_uri? permanent;
+ # Path to static files
+ root {{static_path}};
+ error_page 502 /502.json;
access_log /var/log/nginx/certidude-plaintext-access.log;
error_log /var/log/nginx/certidude-plaintext-error.log;
@@ -94,9 +95,6 @@ server {
proxy_pass http://127.0.1.1:8080/api/;
}
- # Path to static files
- root {{static_path}};
-
# Path to compiled assets
location /assets/ {
alias {{ assets_dir }}/;
@@ -121,6 +119,10 @@ server {
}
{% endif %}
+ # Path to static files
+ root {{static_path}};
+ error_page 502 /502.json;
+
access_log /var/log/nginx/certidude-frontend-access.log;
error_log /var/log/nginx/certidude-frontend-error.log;
}
@@ -151,6 +153,10 @@ server {
nchan_subscriber longpoll;
}
+ # Path to static files
+ root {{static_path}};
+ error_page 502 /502.json;
+
access_log /var/log/nginx/certidude-mutual-auth-access.log;
error_log /var/log/nginx/certidude-mutual-auth-error.log;
}
@@ -173,7 +179,6 @@ server {
access_log /var/log/nginx/certidude-push-access.log;
error_log /var/log/nginx/certidude-push-error.log;
-
}
{% endif %}
diff --git a/certidude/templates/server/server.conf b/certidude/templates/server/server.conf
index 228030f..08cd9fb 100644
--- a/certidude/templates/server/server.conf
+++ b/certidude/templates/server/server.conf
@@ -160,10 +160,10 @@ admin subnets = 0.0.0.0/0
[logging]
# Disable logging
-backend =
+;backend =
# Use SQLite backend
-;backend = sql
+backend = sql
database = sqlite://{{ directory }}/meta/db.sqlite
[signature]
@@ -239,7 +239,7 @@ name = Certidude at {{ common_name }}
{% if domain %}
address = certificates@{{ domain }}
{% else %}
-address = certificates@exaple.com
+address = certificates@example.com
{% endif %}
[tagging]
diff --git a/certidude/templates/server/systemd.service b/certidude/templates/server/systemd.service
index 2a20b95..5b224db 100644
--- a/certidude/templates/server/systemd.service
+++ b/certidude/templates/server/systemd.service
@@ -3,10 +3,14 @@ Description=Certidude server
After=network.target
[Service]
+Type=simple
+EnvironmentFile=/etc/environment
+Environment=LANG=C.UTF-8
Environment=PYTHON_EGG_CACHE=/tmp/.cache
PIDFile=/run/certidude/server.pid
-ExecStop=/bin/kill -s TERM $MAINPID
+KillSignal=SIGINT
ExecStart={{ certidude_path }} serve
+TimeoutSec=15
[Install]
WantedBy=multi-user.target
diff --git a/certidude/tokens.py b/certidude/tokens.py
index f63ec18..e23b901 100644
--- a/certidude/tokens.py
+++ b/certidude/tokens.py
@@ -58,11 +58,8 @@ class TokenManager(RelationalMixin):
"url": url,
}
- def list(self, expired=False, used=False, token=False):
- stmt = "select created as 'created[timestamp]', expires as 'expires[timestamp]', used as 'used[timestamp]', issuer, mail, subject"
- if token:
- stmt += ", uuid"
- stmt += " from token"
+ def list(self, expired=False, used=False):
+ stmt = "select created as 'created[timestamp]', expires as 'expires[timestamp]', used as 'used[timestamp]', issuer, mail, subject, substr(uuid, 0, 8) as uuid from token"
where = []
args = []
if not expired:
@@ -73,7 +70,6 @@ class TokenManager(RelationalMixin):
if where:
stmt = stmt + " where " + (" and ".join(where))
stmt += " order by expires"
-
return self.iterfetch(stmt, *args)
def purge(self, all=False):
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 0738df5..3b49c11 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,3 +1,4 @@
+import coverage
import pwd
from asn1crypto import pem, x509
from oscrypto import asymmetric
@@ -14,6 +15,8 @@ import shutil
import sys
import os
+coverage.process_startup()
+
UA_FEDORA_FIREFOX = "Mozilla/5.0 (X11; Fedora; Linux x86_64) " \
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36"
@@ -69,6 +72,8 @@ def clean_client():
"/etc/certidude/authority/ca.example.lan/server_req.pem",
"/etc/certidude/authority/ca.example.lan/client_cert.pem",
"/etc/certidude/authority/ca.example.lan/server_cert.pem",
+ "/etc/NetworkManager/system-connections/IPSec to ipsec.example.lan",
+ "/etc/NetworkManager/system-connections/OpenVPN to vpn.example.lan",
]
for path in files:
if os.path.exists(path):
@@ -93,14 +98,6 @@ def clean_server():
os.umask(0o22)
- if os.path.exists("/run/certidude/server.pid"):
- with open("/run/certidude/server.pid") as fh:
- try:
- os.kill(int(fh.read()), 15)
- except OSError:
- pass
-
-
if os.path.exists("/var/lib/certidude"):
shutil.rmtree("/var/lib/certidude")
if os.path.exists("/run/certidude"):
@@ -125,11 +122,14 @@ def clean_server():
"/tmp/key.pem",
"/tmp/req.pem",
"/tmp/cert.pem",
+ "/usr/bin/node",
]
for filename in files:
- if os.path.exists(filename):
+ try:
os.unlink(filename)
+ except:
+ pass
# Remove OpenVPN stuff
if os.path.exists("/etc/openvpn"):
@@ -142,6 +142,7 @@ def clean_server():
# Remove Samba stuff
os.system("rm -Rfv /var/lib/samba/*")
assert not os.path.exists("/var/lib/samba/private/secrets.keytab")
+ assert not os.path.exists("/etc/krb5.keytab")
# Restore initial resolv.conf
shutil.copyfile("/etc/resolv.conf.orig", "/etc/resolv.conf")
@@ -227,6 +228,8 @@ def test_cli_setup_authority():
# Make sure nginx is running
assert os.system("nginx -t") == 0, "invalid nginx configuration"
+ os.system("systemctl restart certidude")
+ os.system("systemctl restart nginx")
assert os.path.exists("/run/nginx.pid"), "nginx wasn't started up properly"
# Make sure we generated legit CA certificate
@@ -623,7 +626,7 @@ def test_cli_setup_authority():
assert r.text == "[]", r.text
# Test script without tags
- r = client().simulate_get("/api/signed/test/script/")
+ r = requests.get("http://ca.example.lan/api/signed/test/script/")
assert r.status_code == 200, r.text # script render ok
assert "# No tags" in r.text, r.text
@@ -648,19 +651,17 @@ def test_cli_setup_authority():
headers={"Authorization":admintoken})
assert r.status_code == 200, r.text
assert "Revoked " in inbox.pop(), inbox
- """
# Log can be read only by admin
- r = client().simulate_get("/api/log/")
+ r = requests.get("http://ca.example.lan/api/log/?limit=100")
assert r.status_code == 401, r.text
- r = client().simulate_get("/api/log/",
+ r = requests.get("http://ca.example.lan/api/log/?limit=100",
headers={"Authorization":usertoken})
assert r.status_code == 403, r.text
- r = client().simulate_get("/api/log/",
+ r = requests.get("http://ca.example.lan/api/log/?limit=100",
headers={"Authorization":admintoken})
assert r.status_code == 200, r.text
assert r.headers.get('content-type') == "application/json; charset=UTF-8"
- """
# Test session API call
r = client().simulate_get("/api/")
@@ -698,7 +699,7 @@ def test_cli_setup_authority():
clean_client()
result = runner.invoke(cli, ["setup", "nginx", "-cn", "www", "ca.example.lan"])
- assert result.exception # FQDN required
+ assert result.exception
result = runner.invoke(cli, ["setup", "nginx", "-cn", "www.example.lan", "ca.example.lan"])
assert not result.exception, result.output
@@ -819,8 +820,25 @@ def test_cli_setup_authority():
assert not result.exception, result.output
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
assert "Writing certificate to:" in result.output, result.output
+ assert os.path.exists("/etc/NetworkManager/system-connections/OpenVPN to vpn.example.lan")
+ # Issue token, needs legit router ^
+ os.system("certidude token issue userbot")
+
+ ########################
+ # Test image builder ###
+ ########################
+
+ r = client().simulate_get("/api/build/ar150-mfp-sysupgrade/mfp-gl-ar150-squashfs-sysupgrade.bin")
+ assert r.status_code == 401, r.text
+ r = client().simulate_get("/api/build/ar150-mfp-sysupgrade/mfp-gl-ar150-squashfs-sysupgrade.bin",
+ headers={"Authorization":usertoken})
+ assert r.status_code == 403, r.text
+ r = client().simulate_get("/api/build/ar150-mfp-sysupgrade/mfp-gl-ar150-squashfs-sysupgrade.bin",
+ headers={"Authorization":admintoken})
+ assert r.status_code == 200, r.text
+
#######################
### Token mechanism ###
@@ -995,6 +1013,7 @@ def test_cli_setup_authority():
with open("/etc/certidude/client.conf", "a") as fh:
fh.write("autosign = false\n")
+ fh.write("system wide = yes\n")
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
assert not result.exception, result.output
@@ -1048,7 +1067,7 @@ def test_cli_setup_authority():
assert not result.exception, result.output
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
assert "Writing certificate to:" in result.output, result.output
-
+ assert os.path.exists("/etc/NetworkManager/system-connections/IPSec to ipsec.example.lan")
######################################
### Test revocation on client side ###
@@ -1115,20 +1134,30 @@ def test_cli_setup_authority():
### Switch to Kerberos/LDAP auth ###
####################################
+ assert os.path.exists("/run/certidude/server.pid")
+ pid_certidude = int(open("/run/certidude/server.pid").read())
os.system("systemctl stop certidude")
+ assert not os.path.exists("/run/certidude/server.pid")
# Install packages
clean_server()
# Bootstrap domain controller here,
# Samba startup takes some time
+ assert not os.path.exists("/var/lib/samba/private/secrets.keytab")
+ assert not os.path.exists("/etc/krb5.keytab")
+
os.system("samba-tool domain provision --server-role=dc --domain=EXAMPLE --realm=EXAMPLE.LAN --host-name=ca")
+ assert not os.path.exists("/run/samba/samba.pid")
os.system("systemctl restart samba-ad-dc")
os.system("samba-tool user add userbot S4l4k4l4 --given-name='User' --surname='Bot'")
os.system("samba-tool user add adminbot S4l4k4l4 --given-name='Admin' --surname='Bot'")
os.system("samba-tool group addmembers 'Domain Admins' adminbot")
os.system("samba-tool user setpassword administrator --newpassword=S4l4k4l4")
- os.symlink("/var/lib/samba/private/secrets.keytab", "/etc/krb5.keytab")
+ try:
+ os.symlink("/var/lib/samba/private/secrets.keytab", "/etc/krb5.keytab")
+ except:
+ pass
os.chmod("/var/lib/samba/private/secrets.keytab", 0o644) # To allow access to certidude server
if os.path.exists("/etc/krb5.conf"): # Remove the one from krb5-user package
os.unlink("/etc/krb5.conf")
@@ -1140,14 +1169,15 @@ def test_cli_setup_authority():
# Samba bind 636 late (probably generating keypair)
# so LDAPS connections below will fail
- timeout = 0
- while timeout < 30:
+ timeout = 30
+ while timeout > 0:
if os.path.exists("/var/lib/samba/private/tls/cert.pem"):
break
sleep(1)
- timeout += 1
+ timeout -= 1
else:
assert False, "Samba startup timed out"
+ assert os.path.exists("/run/samba/samba.pid")
# (re)auth against DC
assert os.system("kdestroy") == 0
@@ -1175,7 +1205,7 @@ def test_cli_setup_authority():
# - CRL disabled
assert not os.path.exists("/var/lib/certidude/ca_key.pem")
- assert os.system("certidude setup authority --skip-packages") == 0
+ assert os.system("certidude setup authority --skip-packages -o 'Demola LLC'") == 0
assert os.path.exists("/var/lib/certidude/ca_key.pem")
assert os.path.exists("/etc/cron.hourly/certidude")
@@ -1203,6 +1233,13 @@ def test_cli_setup_authority():
# Start certidude backend
assert os.system("systemctl restart certidude") == 0
+
+ cov_finished = False
+ for path in os.listdir("/tmp/"):
+ if path.startswith(".coverage.ca.%d." % pid_certidude):
+ cov_finished = True
+ assert cov_finished, "Didn't find %d in %s" % (pid_certidude, os.listdir("/tmp"))
+
assert_cleanliness()
# Apply /etc/certidude/server.conf changes
@@ -1285,7 +1322,7 @@ def test_cli_setup_authority():
admintoken = "Basic YWRtaW5ib3Q6UzRsNGs0bDQ="
with open("/etc/ldap/ldap.conf", "w") as fh:
- fh.write("TLS_REQCERT never\n") # TODO: Correct way
+ fh.write("TLS_CACERT /var/lib/samba/private/tls/ca.pem")
# curl http://ca.example.lan/api/ -u adminbot:S4l4k4l4 -H "User-agent: Android" -H "Referer: http://ca.example.lan"
r = requests.get("http://ca.example.lan/api/",
@@ -1355,8 +1392,15 @@ def test_cli_setup_authority():
result = runner.invoke(cli, ['expire'])
assert not result.exception, result.output
+ pid_certidude = int(open("/run/certidude/server.pid").read())
assert os.system("systemctl stop certidude") == 0
+ cov_finished = False
+ for path in os.listdir("/tmp/"):
+ if path.startswith(".coverage.ca.%d." % pid_certidude):
+ cov_finished = True
+ assert cov_finished
+
assert open("/etc/apparmor.d/local/usr.lib.ipsec.charon").read() == \
"/etc/certidude/authority/ca.example.lan/client_key.pem r,\n" + \
"/etc/certidude/authority/ca.example.lan/ca_cert.pem r,\n" + \
@@ -1367,6 +1411,9 @@ def test_cli_setup_authority():
os.system("service openvpn stop")
os.system("ipsec stop")
+ os.system("certidude token list")
+ os.system("certidude token purge")
+
clean_server()
if __name__ == "__main__":