diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index 5a8c212..81d04ed 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -44,6 +44,7 @@ class SessionResource(object): except IOError: submission_hostname = None yield dict( + server = authority.server_flags(common_name), submitted = submitted, common_name = common_name, address = submission_address, @@ -103,6 +104,7 @@ class SessionResource(object): yield dict( serial = "%x" % cert.serial_number, + organizational_unit = cert.subject.native.get("organizational_unit_name"), common_name = common_name, # TODO: key type, key length, key exponent, key modulo signed = signed, @@ -158,10 +160,8 @@ class SessionResource(object): request_subnets = config.REQUEST_SUBNETS or None, admin_subnets=config.ADMIN_SUBNETS or None, signature = dict( - server_certificate_lifetime=config.SERVER_CERTIFICATE_LIFETIME, - client_certificate_lifetime=config.CLIENT_CERTIFICATE_LIFETIME, revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME, - profiles = [dict(organizational_unit=ou, flags=f, lifetime=lt) for f, lt, ou in config.PROFILES.values()] + profiles = [dict(name=k, server=v[0]=="server", lifetime=v[1], organizational_unit=v[2], title=v[3]) for k,v in config.PROFILES.items()] ) ) if req.context.get("user").is_admin() else None, features=dict( diff --git a/certidude/api/builder.py b/certidude/api/builder.py index 61538a4..76d58a6 100644 --- a/certidude/api/builder.py +++ b/certidude/api/builder.py @@ -21,6 +21,7 @@ class ImageBuilderResource(object): suffix = config.cp2.get(profile, "filename") build = "/var/lib/certidude/builder/" + profile + log_path = build + "/build.log" if not os.path.exists(build + "/overlay/etc/uci-defaults"): os.makedirs(build + "/overlay/etc/uci-defaults") os.system("rsync -av " + overlay_path + "/ " + build + "/overlay/") @@ -31,12 +32,16 @@ class ImageBuilderResource(object): fh.write(template.render(authority_name=const.FQDN)) proc = subprocess.Popen(("/bin/bash", build_script_path), - stdout=open(build + "/build.log", "w"), stderr=subprocess.STDOUT, + stdout=open(log_path, "w"), stderr=subprocess.STDOUT, close_fds=True, shell=False, - cwd=build, - env={"PROFILE":model, "PATH":"/usr/sbin:/usr/bin:/sbin:/bin"}, + cwd=os.path.dirname(os.path.realpath(build_script_path)), + env={"PROFILE":model, "PATH":"/usr/sbin:/usr/bin:/sbin:/bin", + "BUILD":build, "OVERLAY":build + "/overlay/"}, startupinfo=None, creationflags=0) proc.communicate() + if proc.returncode: + logger.info("Build script finished with non-zero exitcode, see %s for more information" % log_path) + raise falcon.HTTPInternalServerError("Build script finished with non-zero exitcode") for dname in os.listdir(build): if dname.startswith("lede-imagebuilder-"): diff --git a/certidude/api/lease.py b/certidude/api/lease.py index a20ea08..7ba1bf9 100644 --- a/certidude/api/lease.py +++ b/certidude/api/lease.py @@ -33,6 +33,11 @@ class LeaseResource(object): @authorize_server def on_post(self, req, resp): client_common_name = req.get_param("client", required=True) + if "=" in client_common_name: # It's actually DN, resolve it to CN + _, client_common_name = client_common_name.split(" CN=", 1) + if "," in client_common_name: + client_common_name, _ = client_common_name.split(",", 1) + path, buf, cert, signed, expires = authority.get_signed(client_common_name) # TODO: catch exceptions if req.get_param("serial") and cert.serial_number != req.get_param_as_int("serial"): # OCSP-ish solution for OpenVPN, not exposed for StrongSwan raise falcon.HTTPForbidden("Forbidden", "Invalid serial number supplied") diff --git a/certidude/api/request.py b/certidude/api/request.py index 42a8a55..7424eb9 100644 --- a/certidude/api/request.py +++ b/certidude/api/request.py @@ -225,7 +225,10 @@ class RequestDetailResource(object): Sign a certificate signing request """ try: - cert, buf = authority.sign(cn, ou=req.get_param("ou"), overwrite=True, signer=req.context.get("user").name) + cert, buf = authority.sign(cn, + profile=req.get_param("profile", default="default"), + overwrite=True, + signer=req.context.get("user").name) # Mailing and long poll publishing implemented in the function above except EnvironmentError: # no such CSR raise falcon.HTTPNotFound() diff --git a/certidude/api/signed.py b/certidude/api/signed.py index 29fc8bb..07ea373 100644 --- a/certidude/api/signed.py +++ b/certidude/api/signed.py @@ -38,6 +38,7 @@ class SignedCertificateDetailResource(object): common_name = cn, signer = signer_username, serial_number = "%x" % cert.serial_number, + organizational_unit = cert.subject.native.get("organizational_unit_name"), signed = cert["tbs_certificate"]["validity"]["not_before"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", expires = cert["tbs_certificate"]["validity"]["not_after"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", sha256sum = hashlib.sha256(buf).hexdigest())) diff --git a/certidude/authority.py b/certidude/authority.py index 680343a..1769540 100644 --- a/certidude/authority.py +++ b/certidude/authority.py @@ -55,7 +55,7 @@ def self_enroll(): fh.write(asymmetric.dump_private_key(private_key, None)) else: now = datetime.utcnow() - if now - timedelta(days=1) < expires: + if now + timedelta(days=1) < expires: click.echo("Certificate %s still valid, delete to self-enroll again" % path) return @@ -307,7 +307,7 @@ def delete_request(common_name): config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(), headers={"User-Agent": "Certidude API"}) -def sign(common_name, skip_notify=False, skip_push=False, overwrite=False, ou=None, signer=None): +def sign(common_name, skip_notify=False, skip_push=False, overwrite=False, profile="default", signer=None): """ Sign certificate signing request by it's common name """ @@ -320,13 +320,15 @@ def sign(common_name, skip_notify=False, skip_push=False, overwrite=False, ou=No # Sign with function below - cert, buf = _sign(csr, csr_buf, skip_notify, skip_push, overwrite, ou, signer) + cert, buf = _sign(csr, csr_buf, skip_notify, skip_push, overwrite, profile, signer) os.unlink(req_path) return cert, buf -def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, ou=None, signer=None): +def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, profile="default", signer=None): # TODO: CRLDistributionPoints, OCSP URL, Certificate URL + if profile not in config.PROFILES: + raise ValueError("Invalid profile supplied '%s'" % profile) assert buf.startswith(b"-----BEGIN CERTIFICATE REQUEST-----") assert isinstance(csr, CertificationRequest) @@ -367,8 +369,9 @@ def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, ou=None # Sign via signer process dn = {u'common_name': common_name } - if ou: - dn["organizational_unit"] = ou + profile_server_flags, lifetime, dn["organizational_unit_name"], _ = config.PROFILES[profile] + lifetime = int(lifetime) + builder = CertificateBuilder(dn, csr_pubkey) builder.serial_number = random.randint( 0x1000000000000000000000000000000000000000, @@ -376,16 +379,14 @@ def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, ou=None now = datetime.utcnow() builder.begin_date = now - timedelta(minutes=5) - builder.end_date = now + timedelta(days=config.SERVER_CERTIFICATE_LIFETIME - if server_flags(common_name) - else config.CLIENT_CERTIFICATE_LIFETIME) + builder.end_date = now + timedelta(days=lifetime) builder.issuer = certificate builder.ca = False builder.key_usage = set(["digital_signature", "key_encipherment"]) - # OpenVPN uses CN while StrongSwan uses SAN - if server_flags(common_name): - builder.subject_alt_domains = [common_name] + # If we have FQDN and profile suggests server flags, enable them + if server_flags(common_name) and profile_server_flags: + builder.subject_alt_domains = [common_name] # OpenVPN uses CN while StrongSwan uses SAN to match hostname of the server builder.extended_key_usage = set(["server_auth", "1.3.6.1.5.5.8.2.2", "client_auth"]) else: builder.extended_key_usage = set(["client_auth"]) diff --git a/certidude/cli.py b/certidude/cli.py index 2afc0d0..8f84219 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -1312,7 +1312,7 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign def certidude_sign(common_name, overwrite): from certidude import authority drop_privileges() - cert = authority.sign(common_name, overwrite) + cert = authority.sign(common_name, overwrite=overwrite) @click.command("revoke", help="Revoke certificate") diff --git a/certidude/config.py b/certidude/config.py index 52a349d..2ef6785 100644 --- a/certidude/config.py +++ b/certidude/config.py @@ -60,8 +60,6 @@ USER_MULTIPLE_CERTIFICATES = { cp.get("authority", "user enrollment")] REQUEST_SUBMISSION_ALLOWED = cp.getboolean("authority", "request submission allowed") -CLIENT_CERTIFICATE_LIFETIME = cp.getint("signature", "client certificate lifetime") -SERVER_CERTIFICATE_LIFETIME = cp.getint("signature", "server certificate lifetime") AUTHORITY_CERTIFICATE_URL = cp.get("signature", "authority certificate url") AUTHORITY_CRL_URL = cp.get("signature", "revoked url") AUTHORITY_OCSP_URL = cp.get("signature", "responder url") diff --git a/certidude/static/js/certidude.js b/certidude/static/js/certidude.js index ae5069b..fb07465 100644 --- a/certidude/static/js/certidude.js +++ b/certidude/static/js/certidude.js @@ -118,7 +118,7 @@ function onRequestSubmitted(e) { console.info("Going to prepend:", request); onRequestDeleted(e); // Delete any existing ones just in case $("#pending_requests").prepend( - env.render('views/request.html', { request: request })); + env.render('views/request.html', { request: request, session: session })); $("#pending_requests time").timeago(); }, error: function(response) { diff --git a/certidude/static/views/authority.html b/certidude/static/views/authority.html index 8e9a223..129bdd8 100644 --- a/certidude/static/views/authority.html +++ b/certidude/static/views/authority.html @@ -28,23 +28,105 @@ curl -f -L -H "Content-type: application/pkcs10" --data-binary @client_req.pem \ http://{{ window.location.hostname }}/api/request/?wait=yes > client_cert.pem -
Vanilla OpenWrt/LEDE
+
OpenVPN gateway on OpenWrt/LEDE router
-

On OpenWrt/LEDE router to convert it into VPN gateway:

+

First enroll certificates:

-
mkdir -p /var/lib/certidude/{{ window.location.hostname }}; \
-grep -c certidude /etc/sysupgrade.conf || echo /var/lib/certidude >> /etc/sysupgrade.conf; \
-curl -f http://{{ window.location.hostname }}/api/certificate/ -o /var/lib/certidude/{{ window.location.hostname }}/ca_cert.pem; \
-test -e /var/lib/certidude/{{ window.location.hostname }}/client_key.pem || openssl genrsa -out /var/lib/certidude/{{ window.location.hostname }}/client_key.pem 2048; \
-test -e /var/lib/certidude/{{ window.location.hostname }}/client_req.pem || read -p "Enter FQDN: " NAME; openssl req -new -sha256 \
-  -key /var/lib/certidude/{{ window.location.hostname }}/client_key.pem \
-  -out /var/lib/certidude/{{ window.location.hostname }}/client_req.pem -subj "/CN=$NAME"; \
+              
# Derive FQDN from WAN interface's reverse DNS record
+FQDN=$(nslookup $(uci get network.wan.ipaddr) |  grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
+
+mkdir -p /etc/certidude/authority/{{ window.location.hostname }}; \
+grep -c certidude /etc/sysupgrade.conf || echo /etc/certidude >> /etc/sysupgrade.conf; \
+curl -f http://{{ window.location.hostname }}/api/certificate/ -o /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem; \
+test -e /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem \
+ || openssl genrsa -out /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem 2048; \
+test -e /etc/certidude/authority/{{ window.location.hostname }}/server_req.pem \
+ || openssl req -new -sha256 \
+      -key /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem \
+      -out /etc/certidude/authority/{{ window.location.hostname }}/server_req.pem -subj "/CN=$FQDN"; \
 curl -f -L -H "Content-type: application/pkcs10" \
-  --data-binary @/var/lib/certidude/{{ window.location.hostname }}/client_req.pem \
-  -o /var/lib/certidude/{{ window.location.hostname }}/client_cert.pem \
+  --data-binary @/etc/certidude/authority/{{ window.location.hostname }}/server_req.pem \
+  -o /etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem \
   http://{{ window.location.hostname }}/api/request/?wait=yes
+

Then set up service:

+
+
# Create VPN gateway up/down script for reporting client IP addresses to CA
+cat <<\EOF > /etc/certidude/updown
+#!/bin/sh
+CURL="curl -f --key /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem --cert /etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem --cacert /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem https://{{ window.location.hostname }}:8443/api/lease/"
+
+case $PLUTO_VERB in
+  up-client|down-client) $CURL --data "outer_address=$PLUTO_PEER&inner_address=$PLUTO_PEER_SOURCEIP&client=$PLUTO_PEER_ID" ;;
+  *) $CURL --data "client=$X509_0_CN&outer_address=$untrusted_ip&inner_address=$ifconfig_pool_remote_ip&serial=$tls_serial_0" ;;
+esac
+EOF
+
+chmod +x /etc/certidude/updown
+
+
+# Generate Diffie-Hellman parameters file for OpenVPN
+test -e /etc/certidude/dh.pem \
+ || openssl dhparam 2048 -out /etc/certidude/dh.pem
+
+# Create interface definition for tunnel
+uci set network.vpn=interface
+uci set network.vpn.name='vpn'
+uci set network.vpn.ifname=tun_s2c
+uci set network.vpn.proto='none'
+
+# Create zone definition for VPN interface
+uci set firewall.vpn=zone
+uci set firewall.vpn.name='vpn'
+uci set firewall.vpn.input='ACCEPT'
+uci set firewall.vpn.forward='ACCEPT'
+uci set firewall.vpn.output='ACCEPT'
+uci set firewall.vpn.network='vpn'
+
+# Allow UDP 1194 on WAN interface
+uci set firewall.openvpn=rule
+uci set firewall.openvpn.name='Allow OpenVPN'
+uci set firewall.openvpn.src='wan'
+uci set firewall.openvpn.dest_port=1194
+uci set firewall.openvpn.proto='udp'
+uci set firewall.openvpn.target='ACCEPT'
+
+# Forward traffic from VPN to LAN
+uci set firewall.c2s=forwarding
+uci set firewall.c2s.src='vpn'
+uci set firewall.c2s.dest='lan'
+
+# Permit DNS queries from VPN
+uci set dhcp.@dnsmasq[0].localservice='0'
+
+touch /etc/config/openvpn
+uci set openvpn.s2c=openvpn
+uci set openvpn.s2c.local=$(uci get network.wan.ipaddr)
+uci set openvpn.s2c.script_security=2
+uci set openvpn.s2c.client_connect='/etc/certidude/updown'
+uci set openvpn.s2c.tls_version_min='1.2'
+uci set openvpn.s2c.tls_cipher='TLS-DHE-RSA-WITH-AES-256-GCM-SHA384'
+uci set openvpn.s2c.cipher='AES-256-CBC'
+uci set openvpn.s2c.auth='SHA384'
+uci set openvpn.s2c.dev=tun_s2c
+uci set openvpn.s2c.server='10.179.43.0 255.255.255.0'
+uci set openvpn.s2c.key='/etc/certidude/authority/{{ window.location.hostname }}/server_key.pem'
+uci set openvpn.s2c.cert='/etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem'
+uci set openvpn.s2c.ca='/etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem'
+uci set openvpn.s2c.dh='/etc/certidude/dh.pem'
+uci set openvpn.s2c.enabled=1
+uci set openvpn.s2c.comp_lzo=yes
+uci add_list openvpn.s2c.push="route-metric 1000"
+uci add_list openvpn.s2c.push="route $(uci get network.lan.ipaddr) $(uci get network.lan.netmask)"
+uci add_list openvpn.s2c.push="dhcp-option DNS $(uci get network.lan.ipaddr)"
+uci add_list openvpn.s2c.push="dhcp-option DOMAIN $(uci get dhcp.@dnsmasq[0].domain)"
+
+/etc/init.d/openvpn restart
+/etc/init.d/firewall restart
+
+ + {% if session.authority.builder %}
OpenWrt/LEDE image builder

Hit a link to generate machine specific image. Note that this might take couple minutes to finish.

diff --git a/certidude/static/views/request.html b/certidude/static/views/request.html index 7e5b642..83ec143 100644 --- a/certidude/static/views/request.html +++ b/certidude/static/views/request.html @@ -30,10 +30,11 @@ diff --git a/certidude/static/views/signed.html b/certidude/static/views/signed.html index 29ef52b..7ecbad8 100644 --- a/certidude/static/views/signed.html +++ b/certidude/static/views/signed.html @@ -1,6 +1,10 @@

+ {% if certificate.organizational_unit %} + + {{ certificate.organizational_unit }} / + {% endif %} {% if certificate.server %} {% else %} @@ -17,12 +21,9 @@ Signed - , + {% if certificate.signer %} by {{ certificate.signer }}{% endif %}, expires . - {% if certificate.organizational_unit %} - Part of {{ certificate.organizational_unit }} organizational unit. - {% endif %}

{% if session.authority.tagging %} @@ -91,10 +92,10 @@ curl --cert client_cert.pem https://{{ window.location.hostname }}:8443/api/sign

- - + + - + {% if certificate.lease %}
Common name{{ certificate.common_name }}
Organizational unit{% if certificate.organizational_unit %}{{ certificate.organizational_unit }}{% else %}-{% endif %}
Common name{{ certificate.common_name }}
Organizational unit{% if certificate.organizational_unit %}{{ certificate.organizational_unit }}{% else %}-{% endif %}
Serial number{{ certificate.serial | serial }}
Signed{{ certificate.signed | datetime }}{% if certificate.signer %}, by {{ certificate.signer }}{% endif %}
Signed{{ certificate.signed | datetime }}{% if certificate.signer %} by {{ certificate.signer }}{% endif %}
Expires{{ certificate.expires | datetime }}
Lease{{ certificate.lease.inner_address }} at {{ certificate.lease.last_seen | datetime }} diff --git a/certidude/templates/server/server.conf b/certidude/templates/server/server.conf index f11efad..5d490e4 100644 --- a/certidude/templates/server/server.conf +++ b/certidude/templates/server/server.conf @@ -191,13 +191,13 @@ lifetime = 30 # Secret for generating and validating tokens, regenerate occasionally secret = {{ token_secret }} - [profile] -# title, flags, lifetime, organizational unit -default = client, 120, -srv = server, 365, Server -gw = server, 3, Gateway -ap = client, 1825, Access Point +# name, flags, lifetime, organizational unit, title +default = client, 120, Roadwarrior, Roadwarrior +gw = server, 30, Gateway, Gateway +srv = server, 365, Server, +ap = client, 1825, Access Point, Access Point +mfp = client, 30, MFP, Printers [script] # Path to the folder with scripts that can be served to the clients, set none to disable scripting