tests: Better code coverage

This commit is contained in:
Lauri Võsandi 2017-05-03 21:03:51 +00:00
parent a5565439ab
commit 189c604832
12 changed files with 298 additions and 240 deletions

2
.gitignore vendored
View File

@ -37,7 +37,7 @@ pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage*
.cache
nosetests.xml
coverage.xml

View File

@ -10,8 +10,9 @@ virtualenv:
install:
- echo "127.0.0.1 localhost" | sudo tee /etc/hosts
- echo "127.0.1.1 ca.example.lan ca" | sudo tee -a /etc/hosts
- echo "127.0.0.1 vpn.koodur.lan" | sudo tee -a /etc/hosts
- sudo mkdir -p /etc/systemd/system /etc/NetworkManager/system-connections
- echo "127.0.0.1 vpn.example.lan vpn" | sudo tee -a /etc/hosts
- echo "127.0.0.1 www.example.lan www" | sudo tee -a /etc/hosts
- sudo mkdir -p /etc/systemd/system
- sudo pip install -r requirements.txt
- sudo pip install codecov pytest-cov
- sudo pip install -e .

View File

@ -154,7 +154,7 @@ class StaticResource(object):
else:
resp.status = falcon.HTTP_404
resp.body = "File '%s' not found" % req.path
logger.info("Faile '%s' not found, path resolved to '%s'", req.path, path)
logger.info("Fail '%s' not found, path resolved to '%s'", req.path, path)
import ipaddress
class NormalizeMiddleware(object):

View File

@ -29,6 +29,7 @@ class RequestListResource(object):
"""
Validate and parse certificate signing request
"""
reason = "No reason"
body = req.stream.read(req.content_length)
csr = x509.load_pem_x509_csr(body, default_backend())
try:
@ -87,14 +88,15 @@ class RequestListResource(object):
verifier.verify()
except InvalidSignature:
logger.error("Renewal failed, invalid signature supplied for %s", common_name.value)
reason = "Renewal failed, invalid signature supplied"
else:
# At this point renewal signature was valid but we need to perform some extra checks
if datetime.utcnow() > cert.not_valid_after:
logger.error("Renewal failed, current certificate for %s has expired", common_name.value)
# Put on hold
reason = "Renewal failed, current certificate expired"
elif not config.CERTIFICATE_RENEWAL_ALLOWED:
logger.error("Renewal requested for %s, but not allowed by authority settings", common_name.value)
# Put on hold
reason = "Renewal requested, but not allowed by authority settings"
else:
resp.set_header("Content-Type", "application/x-x509-user-cert")
_, resp.body = authority._sign(csr, body, overwrite=True)
@ -106,25 +108,30 @@ class RequestListResource(object):
Process automatic signing if the IP address is whitelisted,
autosigning was requested and certificate can be automatically signed
"""
if req.get_param_as_bool("autosign") and "." not in common_name.value:
for subnet in config.AUTOSIGN_SUBNETS:
if req.context.get("remote_addr") in subnet:
try:
resp.set_header("Content-Type", "application/x-pem-file")
_, resp.body = authority._sign(csr, body)
logger.info("Autosigned %s as %s is whitelisted", common_name.value, req.context.get("remote_addr"))
return
except EnvironmentError:
logger.info("Autosign for %s failed, signed certificate already exists",
common_name.value, req.context.get("remote_addr"))
break
if req.get_param_as_bool("autosign"):
if "." not in common_name.value:
reason = "Autosign failed, IP address not whitelisted"
for subnet in config.AUTOSIGN_SUBNETS:
if req.context.get("remote_addr") in subnet:
try:
resp.set_header("Content-Type", "application/x-pem-file")
_, resp.body = authority._sign(csr, body)
logger.info("Autosigned %s as %s is whitelisted", common_name.value, req.context.get("remote_addr"))
return
except EnvironmentError:
logger.info("Autosign for %s failed, signed certificate already exists",
common_name.value, req.context.get("remote_addr"))
reason = "Autosign failed, signed certificate already exists"
break
else:
reason = "Autosign failed, only client certificates allowed to be signed automatically"
# Attempt to save the request otherwise
try:
csr = authority.store_request(body)
except errors.RequestExists:
reason = "Same request already uploaded exists"
# We should still redirect client to long poll URL below
pass
except errors.DuplicateCommonNameError:
# TODO: Certificate renewal
logger.warning(u"Rejected signing request with overlapping common name from %s",
@ -147,6 +154,7 @@ class RequestListResource(object):
else:
# Request was accepted, but not processed
resp.status = falcon.HTTP_202
resp.body = reason
class RequestDetailResource(object):

View File

@ -15,7 +15,7 @@ import subprocess
import sys
from configparser import ConfigParser, NoOptionError, NoSectionError
from certidude.helpers import certidude_request_certificate
from certidude.common import expand_paths, ip_address, ip_network, apt, rpm, pip, drop_privileges
from certidude.common import ip_address, ip_network, apt, rpm, pip, drop_privileges
from datetime import datetime, timedelta
from time import sleep
import const
@ -31,6 +31,56 @@ logger = logging.getLogger(__name__)
NOW = datetime.utcnow().replace(tzinfo=None)
def setup_client(prefix="client_"):
# Create section in /etc/certidude/client.conf
def wrapper(func):
def wrapped(**arguments):
from certidude import const
common_name = arguments.get("common_name")
authority = arguments.get("authority")
b = os.path.join(const.STORAGE_PATH, authority)
# Create corresponding section in Certidude client configuration file
client_config = ConfigParser()
if os.path.exists(const.CLIENT_CONFIG_PATH):
client_config.readfp(open(const.CLIENT_CONFIG_PATH))
if client_config.has_section(authority):
click.echo("Section '%s' already exists in %s, remove to regenerate" % (authority, const.CLIENT_CONFIG_PATH))
else:
client_config.add_section(authority)
client_config.set(authority, "trigger", "interface up")
client_config.set(authority, "common name", common_name)
client_config.set(authority, "request path", os.path.join(b, prefix + "req.pem"))
client_config.set(authority, "key path", os.path.join(b, prefix + "key.pem"))
client_config.set(authority, "certificate path", os.path.join(b, prefix + "cert.pem"))
client_config.set(authority, "authority path", os.path.join(b, "ca_cert.pem"))
client_config.set(authority, "revocations path", os.path.join(b, "ca_crl.pem"))
with open(const.CLIENT_CONFIG_PATH + ".part", 'wb') as fh:
client_config.write(fh)
os.rename(const.CLIENT_CONFIG_PATH + ".part", const.CLIENT_CONFIG_PATH)
click.echo("Section '%s' added to %s" % (authority, const.CLIENT_CONFIG_PATH))
for j in ("key", "request", "certificate", "authority", "revocations"):
arguments["%s_path" % j] = client_config.get(authority, "%s path" % j)
return func(**arguments)
return wrapped
return wrapper
def generate_dhparam(path):
# Prevent logjam etc for OpenVPN and nginx server
def wrapper(func):
def wrapped(**arguments):
if not os.path.exists(path):
rpm("openssl")
apt("openssl")
cmd = "openssl", "dhparam", "-out", path, ("1024" if os.getenv("TRAVIS") else "2048")
subprocess.check_call(cmd)
arguments["dhparam_path"] = path
return func(**arguments)
return wrapped
return wrapper
@click.command("request", help="Run processes for requesting certificates and configuring services")
@click.option("-r", "--renew", default=False, is_flag=True, help="Renew now")
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background")
@ -46,15 +96,12 @@ def certidude_request(fork, renew, no_wait):
click.echo("No %s!" % const.CLIENT_CONFIG_PATH)
return 1
if not os.path.exists(const.SERVICES_CONFIG_PATH):
click.echo("No %s!" % const.SERVICES_CONFIG_PATH)
return 1
clients = ConfigParser()
clients.readfp(open(const.CLIENT_CONFIG_PATH))
service_config = ConfigParser()
service_config.readfp(open(const.SERVICES_CONFIG_PATH))
if os.path.exists(const.SERVICES_CONFIG_PATH):
service_config.readfp(open(const.SERVICES_CONFIG_PATH))
# Process directories
if not os.path.exists(const.RUN_DIR):
@ -76,13 +123,9 @@ def certidude_request(fork, renew, no_wait):
for authority in clients.sections():
try:
endpoint_dhparam = clients.get(authority, "dhparam path")
if not os.path.exists(endpoint_dhparam):
cmd = "openssl", "dhparam", "-out", endpoint_dhparam, ("512" if os.getenv("TRAVIS") else "2048")
subprocess.check_call(cmd)
endpoint_renewal_overlap = clients.getint(authority, "renewal overlap")
except NoOptionError:
pass
endpoint_renewal_overlap = None
try:
endpoint_insecure = clients.getboolean(authority, "insecure")
except NoOptionError:
@ -157,6 +200,7 @@ def certidude_request(fork, renew, no_wait):
endpoint_authority_path,
endpoint_revocations_path,
endpoint_common_name,
endpoint_renewal_overlap,
insecure=endpoint_insecure,
autosign=True,
wait=not no_wait,
@ -196,7 +240,6 @@ def certidude_request(fork, renew, no_wait):
# IPSec set up with initscripts
if service_config.get(endpoint, "service") == "init/strongswan":
remote = service_config.get(endpoint, "remote")
from ipsecparse import loads
config = loads(open("%s/ipsec.conf" % const.STRONGSWAN_PREFIX).read())
for section_type, section_name in config:
@ -286,7 +329,8 @@ def certidude_request(fork, renew, no_wait):
with open(nm_config_path, "w") as fh:
nm_config.write(fh)
click.echo("Created %s" % nm_config_path)
os.system("nmcli con reload")
if os.path.exists("/run/NetworkManager"):
os.system("nmcli con reload")
continue
@ -323,7 +367,8 @@ def certidude_request(fork, renew, no_wait):
with open(os.path.join("/etc/NetworkManager/system-connections", endpoint), "w") as fh:
nm_config.write(fh)
click.echo("Created %s" % fh.name)
os.system("nmcli con reload")
if os.path.exists("/run/NetworkManager"):
os.system("nmcli con reload")
continue
# TODO: Puppet, OpenLDAP, <insert awesomeness here>
@ -343,34 +388,13 @@ def certidude_request(fork, renew, no_wait):
default="/etc/openvpn/site-to-client.conf",
type=click.File(mode="w", atomic=True, lazy=True),
help="OpenVPN configuration file")
def certidude_setup_openvpn_server(authority, common_name, config, subnet, route, local, proto, port):
@generate_dhparam("/etc/openvpn/dh.pem")
@setup_client(prefix="server_")
def certidude_setup_openvpn_server(authority, common_name, config, subnet, route, local, proto, port, **paths):
# Install dependencies
apt("openvpn")
rpm("openvpn")
# Create corresponding section in Certidude client configuration file
client_config = ConfigParser()
if os.path.exists(const.CLIENT_CONFIG_PATH):
client_config.readfp(open(const.CLIENT_CONFIG_PATH))
if client_config.has_section(authority):
click.echo("Section '%s' already exists in %s, remove to regenerate" % (authority, const.CLIENT_CONFIG_PATH))
else:
client_config.add_section(authority)
client_config.set(authority, "trigger", "interface up")
client_config.set(authority, "common name", common_name)
slug = common_name.replace(".", "-")
client_config.set(authority, "request path", "/etc/openvpn/keys/%s.csr" % slug)
client_config.set(authority, "key path", "/etc/openvpn/keys/%s.key" % slug)
client_config.set(authority, "certificate path", "/etc/openvpn/keys/%s.crt" % slug)
client_config.set(authority, "authority path", "/etc/openvpn/keys/ca.crt")
client_config.set(authority, "revocations path", "/etc/openvpn/keys/ca.crl")
client_config.set(authority, "dhparam path", "/etc/openvpn/keys/dhparam.pem")
with open(const.CLIENT_CONFIG_PATH + ".part", 'wb') as fh:
client_config.write(fh)
os.rename(const.CLIENT_CONFIG_PATH + ".part", const.CLIENT_CONFIG_PATH)
click.echo("Section '%s' added to %s" % (authority, const.CLIENT_CONFIG_PATH))
# Create corresponding section in /etc/certidude/services.conf
endpoint = "OpenVPN server %s of %s" % (common_name, authority)
service_config = ConfigParser()
@ -394,17 +418,18 @@ def certidude_setup_openvpn_server(authority, common_name, config, subnet, route
config.write("proto %s\n" % proto)
config.write("port %d\n" % port)
config.write("local %s\n" % local)
config.write("key %s\n" % client_config.get(authority, "key path"))
config.write("cert %s\n" % client_config.get(authority, "certificate path"))
config.write("ca %s\n" % client_config.get(authority, "authority path"))
config.write("dh %s\n" % client_config.get(authority, "dhparam path"))
config.write("key %s\n" % paths.get("key_path"))
config.write("cert %s\n" % paths.get("certificate_path"))
config.write("ca %s\n" % paths.get("authority_path"))
config.write("crl-verify %s\n" % paths.get("revocations_path"))
config.write("dh %s\n" % paths.get("dhparam_path"))
config.write("comp-lzo\n")
config.write("user nobody\n")
config.write("group nogroup\n")
config.write("persist-tun\n")
config.write("persist-key\n")
config.write("#ifconfig-pool-persist /tmp/openvpn-leases.txt\n")
config.write("#crl-verify %s\n" % client_config.get(authority, "revocations path"))
click.echo("Generated %s" % config.name)
click.echo("Inspect generated files and issue following to request certificate:")
@ -423,18 +448,14 @@ def certidude_setup_openvpn_server(authority, common_name, config, subnet, route
default="/etc/nginx/sites-available/%s.conf" % const.HOSTNAME,
type=click.File(mode="w", atomic=True, lazy=True),
help="Site configuration file of nginx, /etc/nginx/sites-available/%s.conf by default" % const.HOSTNAME)
@click.option("--directory", "-d", default="/etc/nginx/ssl", help="Directory for keys, /etc/nginx/ssl by default")
@click.option("--key-path", "-key", default=const.HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % const.HOSTNAME)
@click.option("--request-path", "-csr", default=const.HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % const.HOSTNAME)
@click.option("--certificate-path", "-crt", default=const.HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % const.HOSTNAME)
@click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to -d by default")
@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to -d by default")
@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl relative to -d by default")
@click.option("--verify-client", "-vc", default="optional", type=click.Choice(['optional', 'on', 'off']))
@expand_paths()
def certidude_setup_nginx(authority, site_config, tls_config, common_name, directory, key_path, request_path, certificate_path, authority_path, revocations_path, dhparam_path, verify_client):
if not os.path.exists("/etc/nginx"):
raise ValueError("nginx not installed")
@generate_dhparam("/etc/nginx/ssl/dh.pem")
@setup_client(prefix="server_")
def certidude_setup_nginx(authority, common_name, site_config, tls_config, verify_client, **paths):
apt("nginx")
rpm("nginx")
from jinja2 import Environment, PackageLoader
env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True)
if "." not in common_name:
raise ValueError("Fully qualified hostname not specified as common name, make sure hostname -f works")
client_config = ConfigParser()
@ -450,7 +471,6 @@ def certidude_setup_nginx(authority, site_config, tls_config, common_name, direc
client_config.set(authority, "key path", key_path)
client_config.set(authority, "certificate path", certificate_path)
client_config.set(authority, "authority path", authority_path)
client_config.set(authority, "dhparam path", dhparam_path)
client_config.set(authority, "revocations path", revocations_path)
with open(const.CLIENT_CONFIG_PATH + ".part", 'wb') as fh:
client_config.write(fh)
@ -459,6 +479,7 @@ def certidude_setup_nginx(authority, site_config, tls_config, common_name, direc
context = globals() # Grab const.BLAH
context.update(locals())
context.update(paths)
if os.path.exists(site_config.name):
click.echo("Configuration file %s already exists, not overwriting" % site_config.name)
@ -492,30 +513,12 @@ def certidude_setup_nginx(authority, site_config, tls_config, common_name, direc
default="/etc/openvpn/client-to-site.conf",
type=click.File(mode="w", atomic=True, lazy=True),
help="OpenVPN configuration file")
def certidude_setup_openvpn_client(authority, remote, common_name, config, proto):
@setup_client()
def certidude_setup_openvpn_client(authority, remote, common_name, config, proto, **ctx):
# Install dependencies
apt("openvpn")
rpm("openvpn")
# Create corresponding section in Certidude client configuration file
client_config = ConfigParser()
if os.path.exists(const.CLIENT_CONFIG_PATH):
client_config.readfp(open(const.CLIENT_CONFIG_PATH))
if client_config.has_section(authority):
click.echo("Section '%s' already exists in %s, remove to regenerate" % (authority, const.CLIENT_CONFIG_PATH))
else:
client_config.add_section(authority)
client_config.set(authority, "trigger", "interface up")
client_config.set(authority, "common name", common_name)
client_config.set(authority, "request path", "/etc/openvpn/keys/%s.csr" % const.common_name)
client_config.set(authority, "key path", "/etc/openvpn/keys/%s.key" % common_name)
client_config.set(authority, "certificate path", "/etc/openvpn/keys/%s.crt" % common_name)
client_config.set(authority, "authority path", "/etc/openvpn/keys/ca.crt")
client_config.set(authority, "revocations path", "/etc/openvpn/keys/ca.crl")
with open(const.CLIENT_CONFIG_PATH + ".part", 'wb') as fh:
client_config.write(fh)
os.rename(const.CLIENT_CONFIG_PATH + ".part", const.CLIENT_CONFIG_PATH)
click.echo("Section '%s' added to %s" % (authority, const.CLIENT_CONFIG_PATH))
# Create corresponding section in /etc/certidude/services.conf
endpoint = "OpenVPN to %s" % remote
@ -538,7 +541,7 @@ def certidude_setup_openvpn_client(authority, remote, common_name, config, proto
config.write("remote %s\n" % remote)
config.write("remote-cert-tls server\n")
config.write("proto %s\n" % proto)
config.write("dev tun\n")
config.write("dev tun-%s\n" % remote.split(".")[0])
config.write("nobind\n")
config.write("key %s\n" % client_config.get(authority, "key path"))
config.write("cert %s\n" % client_config.get(authority, "certificate path"))
@ -563,7 +566,8 @@ def certidude_setup_openvpn_client(authority, remote, common_name, config, proto
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name, %s by default" % const.FQDN)
@click.option("--subnet", "-sn", default=u"192.168.33.0/24", type=ip_network, help="IPsec virtual subnet, 192.168.33.0/24 by default")
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
def certidude_setup_strongswan_server(authority, common_name, subnet, route):
@setup_client(prefix="server_")
def certidude_setup_strongswan_server(authority, common_name, subnet, route, **paths):
if "." not in common_name:
raise ValueError("Hostname has to be fully qualified!")
@ -572,31 +576,27 @@ def certidude_setup_strongswan_server(authority, common_name, subnet, route):
rpm("strongswan")
pip("ipsecparse")
# Create corresponding section in Certidude client configuration file
client_config = ConfigParser()
if os.path.exists(const.CLIENT_CONFIG_PATH):
client_config.readfp(open(const.CLIENT_CONFIG_PATH))
if client_config.has_section(authority):
click.echo("Section '%s' already exists in %s, remove to regenerate" % (authority, const.CLIENT_CONFIG_PATH))
# Create corresponding section in /etc/certidude/services.conf
endpoint = "IPsec gateway for %s" % authority
service_config = ConfigParser()
if os.path.exists(const.SERVICES_CONFIG_PATH):
service_config.readfp(open(const.SERVICES_CONFIG_PATH))
if service_config.has_section(endpoint):
click.echo("Section '%s' already exists in %s, not reconfiguring" % (endpoint, const.SERVICES_CONFIG_PATH))
else:
client_config.add_section(authority)
client_config.set(authority, "trigger", "interface up")
client_config.set(authority, "common name", const.FQDN)
client_config.set(authority, "request path", "%s/ipsec.d/reqs/%s.pem" % (const.STRONGSWAN_PREFIX, const.HOSTNAME))
client_config.set(authority, "key path", "%s/ipsec.d/private/%s.pem" % (const.STRONGSWAN_PREFIX, const.HOSTNAME))
client_config.set(authority, "certificate path", "%s/ipsec.d/certs/%s.pem" % (const.STRONGSWAN_PREFIX, const.HOSTNAME))
client_config.set(authority, "authority path", "%s/ipsec.d/cacerts/ca.pem" % const.STRONGSWAN_PREFIX)
client_config.set(authority, "revocations path", "%s/ipsec.d/crls/ca.pem" % const.STRONGSWAN_PREFIX)
with open(const.CLIENT_CONFIG_PATH + ".part", 'wb') as fh:
client_config.write(fh)
os.rename(const.CLIENT_CONFIG_PATH + ".part", const.CLIENT_CONFIG_PATH)
click.echo("Section '%s' added to %s" % (authority, const.CLIENT_CONFIG_PATH))
service_config.add_section(endpoint)
service_config.set(endpoint, "authority", authority)
service_config.set(endpoint, "service", "init/strongswan")
with open(const.SERVICES_CONFIG_PATH + ".part", 'wb') as fh:
service_config.write(fh)
os.rename(const.SERVICES_CONFIG_PATH + ".part", const.SERVICES_CONFIG_PATH)
click.echo("Section '%s' added to %s" % (endpoint, const.SERVICES_CONFIG_PATH))
# Create corresponding section to /etc/ipsec.conf
from ipsecparse import loads
config = loads(open("%s/ipsec.conf" % const.STRONGSWAN_PREFIX).read())
config["conn", authority] = dict(
leftcert=client_config.get(authority, "certificate path"),
leftcert=paths.get("certificate_path"),
leftsubnet=",".join(route),
right="%any",
rightsourceip=str(subnet),
@ -617,32 +617,13 @@ def certidude_setup_strongswan_server(authority, common_name, subnet, route):
@click.argument("authority")
@click.argument("remote")
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME)
def certidude_setup_strongswan_client(authority, remote, common_name):
@setup_client()
def certidude_setup_strongswan_client(authority, remote, common_name, **paths):
# Install dependencies
apt("strongswan")
rpm("strongswan")
pip("ipsecparse")
# Create corresponding section in /etc/certidude/client.conf
client_config = ConfigParser()
if os.path.exists(const.CLIENT_CONFIG_PATH):
client_config.readfp(open(const.CLIENT_CONFIG_PATH))
if client_config.has_section(authority):
click.echo("Section '%s' already exists in %s, remove to regenerate" % (authority, const.CLIENT_CONFIG_PATH))
else:
client_config.add_section(authority)
client_config.set(authority, "trigger", "interface up")
client_config.set(authority, "common name", common_name)
client_config.set(authority, "request path", "%s/ipsec.d/reqs/%s.pem" % (const.STRONGSWAN_PREFIX, common_name))
client_config.set(authority, "key path", "%s/ipsec.d/private/%s.pem" % (const.STRONGSWAN_PREFIX, common_name))
client_config.set(authority, "certificate path", "%s/ipsec.d/certs/%s.pem" % (const.STRONGSWAN_PREFIX, common_name))
client_config.set(authority, "authority path", "%s/ipsec.d/cacerts/ca.pem" % const.STRONGSWAN_PREFIX)
client_config.set(authority, "revocations path", "%s/ipsec.d/crls/ca.pem" % const.STRONGSWAN_PREFIX)
with open(const.CLIENT_CONFIG_PATH + ".part", 'wb') as fh:
client_config.write(fh)
os.rename(const.CLIENT_CONFIG_PATH + ".part", const.CLIENT_CONFIG_PATH)
click.echo("Section '%s' added to %s" % (authority, const.CLIENT_CONFIG_PATH))
# Create corresponding section in /etc/certidude/services.conf
endpoint = "IPsec connection to %s" % remote
service_config = ConfigParser()
@ -666,7 +647,7 @@ def certidude_setup_strongswan_client(authority, remote, common_name):
config["conn", remote] = dict(
leftsourceip="%config",
left="%defaultroute",
leftcert=client_config.get(authority, "certificate path"),
leftcert=paths.get("certificate_path"),
rightid="%any",
right=remote,
#rightsubnet=route,
@ -689,33 +670,14 @@ def certidude_setup_strongswan_client(authority, remote, common_name):
@click.argument("authority") # Certidude server
@click.argument("remote") # StrongSwan gateway
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME)
def certidude_setup_strongswan_networkmanager(authority, remote, common_name):
@setup_client()
def certidude_setup_strongswan_networkmanager(authority, remote, common_name, **paths):
# Install dependencies
apt("strongswan-nm")
rpm("NetworkManager-strongswan-gnome")
endpoint = "IPSec to %s" % remote
# Create corresponding section in /etc/certidude/client.conf
client_config = ConfigParser()
if os.path.exists(const.CLIENT_CONFIG_PATH):
client_config.readfp(open(const.CLIENT_CONFIG_PATH))
if client_config.has_section(authority):
click.echo("Section '%s' already exists in %s, remove to regenerate" % (authority, const.CLIENT_CONFIG_PATH))
else:
client_config.add_section(authority)
client_config.set(authority, "trigger", "interface up")
client_config.set(authority, "common name", common_name)
client_config.set(authority, "request path", "/etc/ipsec.d/reqs/%s.pem" % common_name)
client_config.set(authority, "key path", "/etc/ipsec.d/private/%s.pem" % common_name)
client_config.set(authority, "certificate path", "/etc/ipsec.d/certs/%s.pem" % common_name)
client_config.set(authority, "authority path", "/etc/ipsec.d/cacerts/ca.pem")
client_config.set(authority, "revocations path", "/etc/ipsec.d/crls/ca.pem")
with open(const.CLIENT_CONFIG_PATH + ".part", 'wb') as fh:
client_config.write(fh)
os.rename(const.CLIENT_CONFIG_PATH + ".part", const.CLIENT_CONFIG_PATH)
click.echo("Section '%s' added to %s" % (authority, const.CLIENT_CONFIG_PATH))
# Create corresponding section in /etc/certidude/services.conf
service_config = ConfigParser()
if os.path.exists(const.SERVICES_CONFIG_PATH):
@ -737,27 +699,9 @@ def certidude_setup_strongswan_networkmanager(authority, remote, common_name):
@click.argument("authority")
@click.argument("remote") # OpenVPN gateway
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME)
def certidude_setup_openvpn_networkmanager(authority, remote, common_name):
# Create corresponding section in /etc/certidude/client.conf
client_config = ConfigParser()
if os.path.exists(const.CLIENT_CONFIG_PATH):
client_config.readfp(open(const.CLIENT_CONFIG_PATH))
if client_config.has_section(authority):
click.echo("Section '%s' already exists in %s, remove to regenerate" % (authority, const.CLIENT_CONFIG_PATH))
else:
client_config.add_section(authority)
client_config.set(authority, "trigger", "interface up")
client_config.set(authority, "common name", common_name)
client_config.set(authority, "request path", "/etc/ipsec.d/reqs/%s.pem" % common_name)
client_config.set(authority, "key path", "/etc/ipsec.d/private/%s.pem" % common_name)
client_config.set(authority, "certificate path", "/etc/ipsec.d/certs/%s.pem" % common_name)
client_config.set(authority, "authority path", "/etc/ipsec.d/cacerts/ca.pem")
client_config.set(authority, "revocations path", "/etc/ipsec.d/crls/ca.pem")
with open(const.CLIENT_CONFIG_PATH + ".part", 'wb') as fh:
client_config.write(fh)
os.rename(const.CLIENT_CONFIG_PATH + ".part", const.CLIENT_CONFIG_PATH)
click.echo("Section '%s' added to %s" % (authority, const.CLIENT_CONFIG_PATH))
@setup_client()
def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **paths):
# Create corresponding section in /etc/certidude/services.conf
endpoint = "OpenVPN to %s" % remote
service_config = ConfigParser()
@ -767,7 +711,7 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name):
click.echo("Section '%s' already exists in %s, remove to regenerate" % (endpoint, const.SERVICES_CONFIG_PATH))
else:
service_config.add_section(endpoint)
service_config.set(authority, "authority", server)
service_config.set(endpoint, "authority", authority)
service_config.set(endpoint, "remote", remote)
service_config.set(endpoint, "service", "network-manager/openvpn")
service_config.write(open("/etc/certidude/services.conf", "w"))

View File

@ -32,33 +32,6 @@ def ip_address(j):
import ipaddress
return ipaddress.ip_address(unicode(j))
def expand_paths():
"""
Prefix '..._path' keyword arguments of target function with 'directory' keyword argument
and create the directory if necessary
TODO: Move to separate file
"""
def wrapper(func):
def wrapped(**arguments):
d = arguments.get("directory")
for key, value in arguments.items():
if key.endswith("_path"):
if d:
value = os.path.join(d, value)
value = os.path.realpath(value)
parent = os.path.dirname(value)
if not os.path.exists(parent):
click.echo("Making directory %s for %s" % (repr(parent), repr(key)))
os.makedirs(parent)
elif not os.path.isdir(parent):
raise Exception("Path %s is not directory!" % parent)
arguments[key] = value
return func(**arguments)
return wrapped
return wrapper
def apt(packages):
"""
Install packages for Debian and Ubuntu

View File

@ -15,6 +15,7 @@ SERVER_LOG_PATH = "/var/log/certidude-server.log"
SIGNER_SOCKET_PATH = "/run/certidude/signer.sock"
SIGNER_PID_PATH = os.path.join(RUN_DIR, "signer.pid")
SIGNER_LOG_PATH = "/var/log/certidude-signer.log"
STORAGE_PATH = "/var/lib/certidude"
try:
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]

View File

@ -16,12 +16,12 @@ def selinux_fixup(path):
cmd = "chcon", "--type=home_cert_t", path
subprocess.call(cmd)
def certidude_request_certificate(server, system_keytab_required, key_path, request_path, certificate_path, authority_path, revocations_path, common_name, autosign=False, wait=False, bundle=False, renew=False, insecure=False):
def certidude_request_certificate(server, system_keytab_required, key_path, request_path, certificate_path, authority_path, revocations_path, common_name, renewal_overlap, autosign=False, wait=False, bundle=False, renew=False, insecure=False):
"""
Exchange CSR for certificate using Certidude HTTP API server
"""
import requests
from certidude import errors, const
from certidude import errors, const, config
from cryptography import x509
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
@ -178,14 +178,15 @@ def certidude_request_certificate(server, system_keytab_required, key_path, requ
cert_buf = open(certificate_path).read()
cert = x509.load_pem_x509_certificate(cert_buf, default_backend())
lifetime = (cert.not_valid_after - cert.not_valid_before)
overlap = lifetime / 4 # TODO: Make overlap configurable
if datetime.now() > cert.not_valid_after - overlap:
click.echo("Certificate expired %s" % cert.not_valid_after)
if renewal_overlap and datetime.now() > cert.not_valid_after - timedelta(days=renewal_overlap):
click.echo("Certificate will expire %s, will attempt to renew" % cert.not_valid_after)
renew = True
else:
click.echo("Found valid certificate: %s" % certificate_path)
if not renew: # Don't do anything if renewal wasn't requested explicitly
return
else:
cert = None
# If machine is joined to domain attempt to present machine credentials for authentication
if system_keytab_required:
@ -211,7 +212,7 @@ def certidude_request_certificate(server, system_keytab_required, key_path, requ
"Accept": "application/x-x509-user-cert,application/x-pem-file"
}
if renew:
if renew and cert:
signer = key.signer(
padding.PSS(
mgf=padding.MGF1(hashes.SHA512()),
@ -233,7 +234,7 @@ def certidude_request_certificate(server, system_keytab_required, key_path, requ
if submission.status_code == requests.codes.ok:
pass
if submission.status_code == requests.codes.accepted:
# Server stored the request for processing (202 Accepted), but waiting was not requested, hence quitting for now
click.echo("Server accepted the request, but refused to sign immideately (%s). Waiting was not requested, hence quitting for now" % submission.text)
return
if submission.status_code == requests.codes.conflict:
raise errors.DuplicateCommonNameError("Different signing request with same CN is already present on server, server refuses to overwrite")

View File

@ -1,7 +1,11 @@
Token for {{ user.name }}
{% if issuer == user %}
Token has been issued for {{ user }} for retrieving profile from link below.
{% else %}
{{ issuer }} has provided {{ user }} a token for retrieving
profile from the link below.
{% endif %}
{% if config.BUNDLE_FORMAT == "ovpn" %}
To set up OpenVPN for your device:

View File

@ -1,8 +1,8 @@
server {
listen 80;
server_name {{const.FQDN}};
rewrite ^ https://{{const.FQDN}}$request_uri?;
server_name {{common_name}};
rewrite ^ https://{{common_name}}$request_uri?;
}
server {
@ -10,7 +10,7 @@ server {
add_header X-Frame-Options "DENY";
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
listen 443 ssl;
server_name {{const.FQDN}};
server_name {{common_name}};
client_max_body_size 10G;
ssl_certificate {{certificate_path}};
ssl_certificate_key {{key_path}};

View File

@ -88,6 +88,7 @@ revoked url = {{ revoked_url }}
renewal allowed = false
;renewal allowed = true
[push]
# This should occasionally be regenerated
event source token = {{ push_token }}

View File

@ -55,6 +55,21 @@ def generate_csr(cn=None):
).public_bytes(serialization.Encoding.PEM)
return buf
def clean_client():
assert os.getuid() == 0 and os.getgid() == 0
if os.path.exists("/etc/certidude/client.conf"):
os.unlink("/etc/certidude/client.conf")
if os.path.exists("/etc/certidude/services.conf"):
os.unlink("/etc/certidude/services.conf")
# Remove client storage area
if os.path.exists("/tmp/ca.example.lan"):
for filename in os.listdir("/tmp/ca.example.lan"):
if filename.endswith(".pem"):
os.unlink(os.path.join("/tmp/ca.example.lan", filename))
def test_cli_setup_authority():
import os
import sys
@ -78,13 +93,19 @@ def test_cli_setup_authority():
shutil.rmtree("/var/lib/certidude/ca.example.lan")
if os.path.exists("/etc/certidude/server.conf"):
os.unlink("/etc/certidude/server.conf")
if os.path.exists("/etc/certidude/client.conf"):
os.unlink("/etc/certidude/client.conf")
if os.path.exists("/run/certidude"):
shutil.rmtree("/run/certidude")
if os.path.exists("/var/log/certidude.log"):
os.unlink("/var/log/certidude.log")
# Remove nginx stuff
if os.path.exists("/etc/nginx/sites-available/ca.conf"):
os.unlink("/etc/nginx/sites-available/ca.conf")
if os.path.exists("/etc/nginx/sites-enabled/ca.conf"):
os.unlink("/etc/nginx/sites-enabled/ca.conf")
if os.path.exists("/etc/nginx/conf.d/tls.conf"):
os.unlink("/etc/nginx/conf.d/tls.conf")
with open("/etc/ipsec.conf", "w") as fh: # TODO: make compatible with Fedora
pass
@ -96,6 +117,8 @@ def test_cli_setup_authority():
if os.path.exists("/etc/openvpn/keys"):
shutil.rmtree("/etc/openvpn/keys")
clean_client()
from certidude.cli import entry_point as cli
from certidude import const
@ -397,34 +420,62 @@ def test_cli_setup_authority():
assert r2.headers.get('content-type') == "application/x-pkcs12"
assert "Signed " in inbox.pop(), inbox
result = runner.invoke(cli, ['setup', 'openvpn', 'server', "-cn", "vpn.example.lan", "ca.example.lan"])
assert not result.exception, result.output
# Beyond this point don't use client()
const.STORAGE_PATH = "/tmp/"
result = runner.invoke(cli, ['setup', 'openvpn', 'client', "-cn", "roadwarrior1", "ca.example.lan", "vpn.example.lan"])
assert not result.exception, result.output
#############
### nginx ###
#############
clean_client()
result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec.example.lan", "ca.example.lan"])
assert not result.exception, result.output
result = runner.invoke(cli, ['setup', 'strongswan', 'client', "-cn", "roadwarrior2", "ca.example.lan", "ipsec.example.lan"])
assert not result.exception, result.output
result = runner.invoke(cli, ['setup', 'openvpn', 'networkmanager', "-cn", "roadwarrior3", "ca.example.lan", "vpn.example.lan"])
assert not result.exception, result.output
result = runner.invoke(cli, ['setup', 'strongswan', 'networkmanager', "-cn", "roadwarrior4", "ca.example.lan", "ipsec.example.lan"])
result = runner.invoke(cli, ["setup", "nginx", "-cn", "www.example.lan", "ca.example.lan"])
assert not result.exception, result.output
import os
if not os.path.exists("/etc/openvpn/keys"):
os.makedirs("/etc/openvpn/keys")
with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n")
# pregen dhparam
result = runner.invoke(cli, ["request", "--no-wait"])
assert not result.exception, "server responded %s, server logs say %s" % (result.output, open("/var/log/certidude.log").read())
assert not result.exception, result.output
child_pid = os.fork()
if not child_pid:
result = runner.invoke(cli, ['sign', 'www.example.lan'])
assert not result.exception, result.output
return
else:
os.waitpid(child_pid, 0)
result = runner.invoke(cli, ["request", "--no-wait"])
assert not result.exception, result.output
assert "Writing certificate to:" in result.output, result.output
result = runner.invoke(cli, ["request", "--renew", "--no-wait"])
assert not result.exception, result.output
assert "Writing certificate to:" in result.output, result.output
# Test nginx setup
assert os.system("nginx -t") == 0, "Generated nginx config was invalid"
###############
### OpenVPN ###
###############
clean_client()
if not os.path.exists("/etc/openvpn/keys"):
os.makedirs("/etc/openvpn/keys")
result = runner.invoke(cli, ['setup', 'openvpn', 'server', "-cn", "vpn.example.lan", "ca.example.lan"])
assert not result.exception, result.output
with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n")
result = runner.invoke(cli, ["request", "--no-wait"])
assert not result.exception, result.output
child_pid = os.fork()
if not child_pid:
@ -436,13 +487,87 @@ def test_cli_setup_authority():
result = runner.invoke(cli, ["request", "--no-wait"])
assert not result.exception, result.output
result = runner.invoke(cli, ["request", "--renew"])
assert "Writing certificate to:" in result.output, result.output
assert os.path.exists("/tmp/ca.example.lan/server_cert.pem")
# Reset config
os.unlink("/etc/certidude/client.conf")
os.unlink("/etc/certidude/services.conf")
result = runner.invoke(cli, ['setup', 'openvpn', 'client', "-cn", "roadwarrior1", "ca.example.lan", "vpn.example.lan"])
assert not result.exception, result.output
with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n")
result = runner.invoke(cli, ["request", "--no-wait"])
assert not result.exception, result.output
assert "Writing certificate to:" in result.output, result.output
# TODO: test client verification with curl
###############
### IPSec ###
###############
clean_client()
result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec.example.lan", "ca.example.lan"])
assert not result.exception, result.output
with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n")
result = runner.invoke(cli, ["request", "--no-wait"])
assert not result.exception, result.output
child_pid = os.fork()
if not child_pid:
result = runner.invoke(cli, ['sign', 'ipsec.example.lan'])
assert not result.exception, result.output
return
else:
os.waitpid(child_pid, 0)
result = runner.invoke(cli, ["request", "--no-wait"])
assert not result.exception, result.output
assert "Writing certificate to:" in result.output, result.output
assert os.path.exists("/tmp/ca.example.lan/server_cert.pem")
# Reset config
os.unlink("/etc/certidude/client.conf")
os.unlink("/etc/certidude/services.conf")
result = runner.invoke(cli, ['setup', 'strongswan', 'client', "-cn", "roadwarrior2", "ca.example.lan", "ipsec.example.lan"])
assert not result.exception, result.output
with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n")
result = runner.invoke(cli, ["request", "--no-wait"])
assert not result.exception, result.output
assert "Writing certificate to:" in result.output, result.output
######################
### NetworkManager ###
######################
result = runner.invoke(cli, ['setup', 'openvpn', 'networkmanager', "-cn", "roadwarrior3", "ca.example.lan", "vpn.example.lan"])
assert not result.exception, result.output
result = runner.invoke(cli, ['setup', 'strongswan', 'networkmanager', "-cn", "roadwarrior4", "ca.example.lan", "ipsec.example.lan"])
assert not result.exception, result.output
###################
### Final tests ###
###################
# Test revocation on command-line
child_pid = os.fork()
if not child_pid:
result = runner.invoke(cli, ['revoke', 'vpn.example.lan'])
result = runner.invoke(cli, ['revoke', 'www.example.lan'])
assert not result.exception, result.output
return
else: