diff --git a/certidude/api/token.py b/certidude/api/token.py index 12c3c6c..d4e48f3 100644 --- a/certidude/api/token.py +++ b/certidude/api/token.py @@ -24,9 +24,6 @@ class TokenResource(AuthorityHandler): AuthorityHandler.__init__(self, authority) self.manager = manager - def on_get(self, req, resp): - return - def on_put(self, req, resp): try: username, mail, created, expires, profile = self.manager.consume(req.get_param("token", required=True)) @@ -36,7 +33,8 @@ class TokenResource(AuthorityHandler): header, _, der_bytes = pem.unarmor(body) csr = CertificationRequest.load(der_bytes) common_name = csr["certification_request_info"]["subject"].native["common_name"] - assert common_name == username or common_name.startswith(username + "@"), "Invalid common name %s" % common_name + if not common_name.startswith(username + "@"): + raise falcon.HTTPBadRequest("Bad requst", "Invalid common name %s" % common_name) try: _, resp.body = self.authority._sign(csr, body, profile=config.PROFILES.get(profile), overwrite=config.TOKEN_OVERWRITE_PERMITTED) diff --git a/certidude/api/utils/firewall.py b/certidude/api/utils/firewall.py index 9d34dee..b63f47f 100644 --- a/certidude/api/utils/firewall.py +++ b/certidude/api/utils/firewall.py @@ -109,8 +109,9 @@ def authenticate(optional=False): if kerberized: if not req.auth.startswith("Negotiate "): - raise falcon.HTTPBadRequest("Bad request", - "Bad header, expected Negotiate") + raise falcon.HTTPUnauthorized("Unauthorized", + "Bad header, expected Negotiate", + ["Negotiate"]) os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB @@ -158,7 +159,8 @@ def authenticate(optional=False): else: if not req.auth.startswith("Basic "): - raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Basic") + raise falcon.HTTPUnauthorized("Forbidden", "Bad header, expected Basic", ("Basic",)) + basic, token = req.auth.split(" ", 1) user, passwd = b64decode(token).decode("ascii").split(":", 1) diff --git a/certidude/cli.py b/certidude/cli.py index f8df4ab..97295be 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -581,7 +581,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self): nm_config.add_section("vpn") nm_config.set("vpn", "service-type", "org.freedesktop.NetworkManager.openvpn") nm_config.set("vpn", "connection-type", "tls") - nm_config.set("vpn", "comp-lzo", "yes") + nm_config.set("vpn", "comp-lzo", "no") nm_config.set("vpn", "cert-pass-flags", "0") nm_config.set("vpn", "tap-dev", "no") nm_config.set("vpn", "remote-cert-tls", "server") # Assert TLS Server flag of X.509 certificate @@ -717,7 +717,7 @@ def certidude_setup_openvpn_server(authority, common_name, config, subnet, route 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(";comp-lzo\n") config.write("user nobody\n") config.write("group nogroup\n") config.write("persist-tun\n") @@ -822,7 +822,7 @@ def certidude_setup_openvpn_client(authority, remote, common_name, config, proto 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("comp-lzo\n") + config.write(";comp-lzo\n") config.write("user nobody\n") config.write("group nogroup\n") config.write("persist-tun\n") @@ -1081,9 +1081,6 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_confi else: raise ValueError("Fully qualified hostname not specified as common name, make sure hostname -f works") - # Generate secret for tokens - token_url = "https://" + common_name + "/#action=enroll&token=%(token)s&router=%(router)s&protocol=ovpn" - template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "profile") click.echo("Using templates from %s" % template_path) diff --git a/certidude/config.py b/certidude/config.py index f7ee9c3..755dbe1 100644 --- a/certidude/config.py +++ b/certidude/config.py @@ -100,6 +100,7 @@ TOKEN_URL = cp.get("token", "url") TOKEN_BACKEND = cp.get("token", "backend") TOKEN_LIFETIME = timedelta(minutes=cp.getint("token", "lifetime")) # Convert minutes to seconds TOKEN_DATABASE = cp.get("token", "database") +TOKEN_OVERWRITE_PERMITTED = cp.getboolean("token", "overwrite permitted") # TODO: Check if we don't have base or servers # The API call for looking up scripts uses following directory as root @@ -125,7 +126,5 @@ cp2 = configparser.RawConfigParser() cp2.readfp(open(const.BUILDER_CONFIG_PATH, "r")) IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections() if cp2.getboolean(j, "enabled")] -TOKEN_OVERWRITE_PERMITTED=True - SERVICE_PROTOCOLS = set([j.lower() for j in cp.get("service", "protocols").split(" ") if j]) SERVICE_ROUTERS = cp.get("service", "routers") diff --git a/certidude/static/js/certidude.js b/certidude/static/js/certidude.js index 8a9dd33..678357b 100644 --- a/certidude/static/js/certidude.js +++ b/certidude/static/js/certidude.js @@ -6,6 +6,24 @@ const DEVICE_KEYWORDS = ["Android", "iPhone", "iPad", "Windows", "Ubuntu", "Fedo jQuery.timeago.settings.allowFuture = true; +function onRejectRequest(e, common_name, sha256sum) { + $(this).button('loading'); + $.ajax({ + url: "/api/request/" + common_name + "/?sha256sum=" + sha256sum, + type: "delete" + }); +} + +function onSignRequest(e, common_name, sha256sum) { + e.preventDefault(); + $(e.target).button('loading'); + $.ajax({ + url: "/api/request/" + common_name + "/?sha256sum=" + sha256sum, + type: "post" + }); + return false; +} + function normalizeCommonName(j) { return j.replace("@", "--").split(".").join("-"); // dafuq ?! } @@ -27,24 +45,6 @@ function onKeyGen() { window.keys = forge.pki.rsa.generateKeyPair(KEY_SIZE); console.info('Key-pair created.'); - // Device identifier - var dig = forge.md.sha384.create(); - dig.update(window.navigator.userAgent); - - var prefix = "unknown"; - for (i in DEVICE_KEYWORDS) { - var keyword = DEVICE_KEYWORDS[i]; - if (window.navigator.userAgent.indexOf(keyword) >= 0) { - prefix = keyword.toLowerCase(); - break; - } - } - - window.identifier = prefix + "-" + dig.digest().toHex().substring(0, 5); - console.info("Device identifier:", identifier); - - window.common_name = query.subject + "@" + identifier; - window.csr = forge.pki.createCertificationRequest(); csr.publicKey = keys.publicKey; csr.setSubject([{ @@ -82,17 +82,19 @@ function onKeyGen() { $(".option.any").show(); } -function onEnroll(encoding) { - console.info("Service name:", query.title); +function blobToUuid(blob) { var md = forge.md.md5.create(); - md.update(query.title); + md.update(blob); var digest = md.digest().toHex(); - var service_uuid = digest.substring(0, 8) + "-" + + return 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); + digest.substring(20); +} + +function onEnroll(encoding) { + console.info("Service name:", query.title); console.info("User agent:", window.navigator.userAgent); var xhr = new XMLHttpRequest(); @@ -108,8 +110,8 @@ function onEnroll(encoding) { var a = document.createElement("a"); var cert = forge.pki.certificateFromPem(xhr2.responseText); console.info("Got signed certificate:", xhr2.responseText); - var p12 = forge.pkcs12.toPkcs12Asn1( - keys.privateKey, [cert, ca], "", {algorithm: '3des'}); + var p12 = forge.asn1.toDer(forge.pkcs12.toPkcs12Asn1( + keys.privateKey, [cert, ca], "", {algorithm: '3des'})).getBytes(); switch(encoding) { case 'p12': @@ -119,13 +121,13 @@ function onEnroll(encoding) { break case 'sswan': var buf = JSON.stringify({ - uuid: service_uuid, + uuid: blobToUuid(query.title), name: query.title, type: "ikev2-cert", 'ike-proposal': 'aes256-sha384-prfsha384-modp2048', 'esp-proposal': 'aes128gcm16-modp2048', remote: { addr: query.router }, - local: { p12: forge.util.encode64(forge.asn1.toDer(p12).getBytes()) } + local: { p12: forge.util.encode64(p12) } }); console.info("Buf is:", buf); var mimetype = "application/vnd.strongswan.profile" @@ -142,14 +144,18 @@ function onEnroll(encoding) { a.download = query.title + ".ovpn"; break case 'mobileconfig': - var p12 = forge.pkcs12.toPkcs12Asn1( - keys.privateKey, [cert, ca], "1234", {algorithm: '3des'}); + var p12 = forge.asn1.toDer(forge.pkcs12.toPkcs12Asn1( + keys.privateKey, [cert, ca], "1234", {algorithm: '3des'})).getBytes(); var buf = nunjucks.render('snippets/ios.mobileconfig', { session: session, + service_uuid: blobToUuid(query.title), + conf_uuid: blobToUuid(query.title + " conf1"), title: query.title, common_name: common_name, gateway: query.router, - p12: forge.util.encode64(forge.asn1.toDer(p12).getBytes()), + p12_uuid: blobToUuid(p12), + p12: forge.util.encode64(p12), + ca_uuid: blobToUuid(forge.pki.certificateToAsn1(ca)).getBytes()), ca: forge.util.encode64(forge.asn1.toDer(forge.pki.certificateToAsn1(ca)).getBytes()) }); var mimetype = "application/x-apple-aspen-config"; @@ -179,6 +185,7 @@ function onEnroll(encoding) { } function onHashChanged() { + window.query = {}; var a = location.hash.substring(1).split('&'); for (var i = 0; i < a.length; i++) { @@ -200,6 +207,23 @@ function onHashChanged() { $("#view-dashboard").html(env.render('views/error.html', { message: msg })); }, success: function(blob) { + // Device identifier + var dig = forge.md.sha384.create(); + dig.update(window.navigator.userAgent); + + var prefix = "unknown"; + for (i in DEVICE_KEYWORDS) { + var keyword = DEVICE_KEYWORDS[i]; + if (window.navigator.userAgent.indexOf(keyword) >= 0) { + prefix = keyword.toLowerCase(); + break; + } + } + + window.identifier = prefix + "-" + dig.digest().toHex().substring(0, 5); + window.common_name = query.subject + "@" + identifier; + console.info("Device identifier:", identifier); + window.session = { authority: { hostname: window.location.hostname, @@ -221,7 +245,8 @@ function onHashChanged() { } else { if (query.action == "enroll") { $("#view-dashboard").html(env.render('views/enroll.html', { - session:session, + common_name: common_name, + session: session, token: query.token, })); var options = document.querySelectorAll(".option"); diff --git a/certidude/static/snippets/ios.mobileconfig b/certidude/static/snippets/ios.mobileconfig index 36ad9f5..282afa2 100644 --- a/certidude/static/snippets/ios.mobileconfig +++ b/certidude/static/snippets/ios.mobileconfig @@ -6,7 +6,7 @@ {{ gateway }} PayloadIdentifier - org.example.vpn2 + {{ gateway }} PayloadUUID {{ service_uuid }} PayloadType @@ -17,9 +17,9 @@ PayloadIdentifier - org.example.vpn2.conf1 + {{ gateway }}.conf1 PayloadUUID - 29e4456d-3f03-4f15-b46f-4225d89465b7 + {{ conf_uuid }} PayloadType com.apple.vpn.managed PayloadVersion @@ -63,14 +63,14 @@ EnablePFS 1 PayloadCertificateUUID - d60488c6-328e-4944-9c8d-61db8095c865 + {{ p12_uuid }} PayloadIdentifier - ee.k-space.ca2.client + {{ common_name }} PayloadUUID - d60488c6-328e-4944-9c8d-61db8095c865 + {{ p12_uuid }} PayloadType com.apple.security.pkcs12 PayloadVersion @@ -80,14 +80,13 @@ PayloadIdentifier - org.example.ca + {{ session.authority.certificate.common_name }} PayloadUUID - 64988b2c-33e0-4adf-a432-6fbcae543408 + {{ ca_uuid }} PayloadType com.apple.security.root PayloadVersion 1 - PayloadContent {{ ca }} diff --git a/certidude/static/snippets/networkmanager-openvpn.conf b/certidude/static/snippets/networkmanager-openvpn.conf new file mode 100644 index 0000000..db64541 --- /dev/null +++ b/certidude/static/snippets/networkmanager-openvpn.conf @@ -0,0 +1,29 @@ +[connection] +certidude managed = true +id = {{ session.service.title }} +uuid = {{ uuid }} +type = vpn + +[vpn] +service-type = org.freedesktop.NetworkManager.openvpn +connection-type = tls +cert-pass-flags 0 +tap-dev = no +remote-cert-tls = server +remote = {{ routers[0] }} +key = {% if key_path %}{{ key_path }}{% else %}/etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem{% endif %} +cert = {% if certificate_path %}{{ certificate_path }}{% else %}/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem{% endif %} +ca = {% if authority_path %}{{ authority_path }}{% else %}/etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem{% endif %} +tls-cipher = TLS-{% if authority_public_key.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-WITH-AES-256-GCM-SHA384 +cipher = AES-128-GCM +auth = SHA384 +{% if port %};port = {{ port }}{% else %};port = 1194{% endif %} +{% if not proto or not proto.startswith('tcp') %};{% endif %}proto-tcp = yes + +[ipv4] +# Route only pushed subnets to tunnel +never-default = true +method = auto + +[ipv6] +method = auto diff --git a/certidude/static/snippets/networkmanager-strongswan.conf b/certidude/static/snippets/networkmanager-strongswan.conf new file mode 100644 index 0000000..548dccf --- /dev/null +++ b/certidude/static/snippets/networkmanager-strongswan.conf @@ -0,0 +1,23 @@ +[connection] +certidude managed = true +id = {{ session.service.title }} +uuid = {{ uuid }} +type = {{ vpn }} + +[vpn] +service-type = org.freedesktop.NetworkManager.strongswan +encap = no +virtual = yes +method = key +ipcomp = no +address = {{ session.service.routers[0] }} +userkey = {% if key_path %}{{ key_path }}{% else %}/etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem{% endif %} +usercert = {% if certificate_path %}{{ certificate_path }}{% else %}/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem{% endif %} +certificate = {% if authority_path %}{{ authority_path }}{% else %}/etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem{% endif %} +ike = aes256-sha384-prfsha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %} +esp = aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %} +proposal = yes + +[ipv4] +method = auto +;route1 = 0.0.0.0/0 diff --git a/certidude/static/snippets/request-client.ps1 b/certidude/static/snippets/request-client.ps1 new file mode 100644 index 0000000..9bb758b --- /dev/null +++ b/certidude/static/snippets/request-client.ps1 @@ -0,0 +1,47 @@ +# Generate keypair and submit CSR +{% if common_name %}$NAME = "{{ common_name }}" +{% else %}$NAME = $env:computername.toLower() +{% endif %} +@" +[NewRequest] +Subject = "CN=$NAME" +Exportable = FALSE +KeySpec = 1 +KeyUsage = 0xA0 +MachineKeySet = True +ProviderType = 12 +RequestType = PKCS10 +{% if session.authority.certificate.algorithm == "ec" %}ProviderName = "Microsoft Software Key Storage Provider" +KeyAlgorithm = ECDSA_P384 +{% else %}ProviderName = "Microsoft RSA SChannel Cryptographic Provider" +KeyLength = 2048 +{% endif %}"@ | Out-File req.inf +C:\Windows\system32\certreq.exe -new -f -q req.inf host_csr.pem +Invoke-WebRequest `{% if token %} + -Uri 'https://{{ session.authority.hostname }}:8443/api/token/?token={{ token }}' ` + -Method PUT `{% else %} + -Uri 'https://{{ session.authority.hostname }}:8443/api/request/?wait=yes&autosign=yes' ` + -Method POST `{% endif %} + -TimeoutSec 900 ` + -InFile host_csr.pem ` + -ContentType application/pkcs10 ` + -MaximumRedirection 3 -OutFile host_cert.pem + +# Import certificate +Import-Certificate -FilePath host_cert.pem -CertStoreLocation Cert:\LocalMachine\My +{# + +On Windows 7 the Import-Certificate cmdlet is missing, +but certutil.exe can be used instead: + +C:\Windows\system32\certutil.exe -addstore My host_cert.pem + +Everything seems to work except after importing the certificate +it is not properly associated with the private key, +that means "You have private key that corresponds to this certificate" is not +shown under "Valid from ... to ..." in MMC. +This results in error code 13806 during IKEv2 handshake and error message +"IKE failed to find valid machine certificate" + +#} + diff --git a/certidude/static/snippets/strongswan-client.sh b/certidude/static/snippets/strongswan-client.sh index 2c3bd43..2567159 100644 --- a/certidude/static/snippets/strongswan-client.sh +++ b/certidude/static/snippets/strongswan-client.sh @@ -25,4 +25,4 @@ EOF echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ session.authority.hostname }}.pem" > /etc/ipsec.secrets -ipsec restart apparmor +ipsec restart diff --git a/certidude/static/snippets/strongswan-patching.sh b/certidude/static/snippets/strongswan-patching.sh index 6538b75..44257f0 100644 --- a/certidude/static/snippets/strongswan-patching.sh +++ b/certidude/static/snippets/strongswan-patching.sh @@ -14,4 +14,4 @@ chcon --type=home_cert_t /etc/certidude/authority/{{ session.authority.hostname cat << EOF > /etc/apparmor.d/local/usr.lib.ipsec.charon /etc/certidude/authority/** r, EOF -systemctl restart +systemctl restart apparmor diff --git a/certidude/static/snippets/update-trust.ps1 b/certidude/static/snippets/update-trust.ps1 new file mode 100644 index 0000000..6cd6b5c --- /dev/null +++ b/certidude/static/snippets/update-trust.ps1 @@ -0,0 +1,4 @@ +# Install CA certificate +@" +{{ session.authority.certificate.blob }}"@ | Out-File ca_cert.pem +Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root diff --git a/certidude/static/snippets/windows.ps1 b/certidude/static/snippets/windows.ps1 index a286050..d7f8ed6 100644 --- a/certidude/static/snippets/windows.ps1 +++ b/certidude/static/snippets/windows.ps1 @@ -1,37 +1,8 @@ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -# Install CA certificate -@" -{{ session.authority.certificate.blob }}"@ | Out-File ca_cert.pem -{% if session.authority.certificate.algorithm == "ec" %} -Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root -{% else %} -C:\Windows\system32\certutil.exe -addstore Root ca_cert.pem -{% endif %} +{% include "snippets/update-trust.ps1" %} -# Generate keypair and submit CSR -$hostname = $env:computername.ToLower() -@" -[NewRequest] -Subject = "CN=$hostname" -Exportable = FALSE -KeySpec = 1 -KeyUsage = 0xA0 -MachineKeySet = True -ProviderType = 12 -RequestType = PKCS10 -{% if session.authority.certificate.algorithm == "ec" %}ProviderName = "Microsoft Software Key Storage Provider" -KeyAlgorithm = ECDSA_P384 -{% else %}ProviderName = "Microsoft RSA SChannel Cryptographic Provider" -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/{% 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 -{% else %}C:\Windows\system32\certutil.exe -addstore My host_cert.pem -{% endif %} +{% include "snippets/request-client.ps1" %} {% for router in session.service.routers %} # Set up IPSec VPN tunnel to {{ router }} @@ -40,9 +11,12 @@ Add-VpnConnection ` -Name "IPSec to {{ router }}" ` -ServerAddress {{ router }} ` -AuthenticationMethod MachineCertificate ` + -EncryptionLevel Maximum ` -SplitTunneling ` -TunnelType ikev2 ` -PassThru -AllUserConnection + +# Harden VPN configuration Set-VpnConnectionIPsecConfiguration ` -ConnectionName "IPSec to {{ router }}" ` -AuthenticationTransformConstants GCMAES128 ` diff --git a/certidude/static/views/enroll.html b/certidude/static/views/enroll.html index ba91fea..d6fb4ab 100644 --- a/certidude/static/views/enroll.html +++ b/certidude/static/views/enroll.html @@ -77,6 +77,36 @@ sudo systemctl restart network-manager + + + + Ubuntu 18.04+ (advanced) + Copy-paste follownig to terminal as root user: + {% include "snippets/request-client.sh" %} +cat << EOF > '/etc/NetworkManager/system-connections/OpenVPN to {{ session.service.title }}' +{% include "snippets/networkmanager-openvpn.conf" %}EOF + +nmcli con reload + + + + + + + + + Ubuntu 18.04+ (advanced) + Copy-paste follownig to terminal as root user: + {% include "snippets/request-client.sh" %} +cat << EOF > '/etc/NetworkManager/system-connections/IPSec to {{ session.service.title }}' +{% include "snippets/networkmanager-strongswan.conf" %}EOF + +nmcli con reload + + + + + diff --git a/certidude/static/views/request.html b/certidude/static/views/request.html index 4b44222..706a8bc 100644 --- a/certidude/static/views/request.html +++ b/certidude/static/views/request.html @@ -19,11 +19,11 @@ Details + onclick="onRejectRequest(event, '{{ request.common_name }}', '{{ request.sha256sum }}');"> Reject + onclick="onSignRequest(event, '{{ request.common_name }}', '{{ request.sha256sum }}');"> Sign Toggle Dropdown diff --git a/certidude/templates/openvpn-client.conf b/certidude/templates/openvpn-client.conf index 9c63672..f615a7b 100644 --- a/certidude/templates/openvpn-client.conf +++ b/certidude/templates/openvpn-client.conf @@ -8,7 +8,6 @@ client # OpenVPN gateway(s) -comp-lzo nobind ;proto udp ;port 1194 diff --git a/certidude/templates/server/builder.conf b/certidude/templates/server/builder.conf index 79094a7..4c94dab 100644 --- a/certidude/templates/server/builder.conf +++ b/certidude/templates/server/builder.conf @@ -3,7 +3,7 @@ enabled = yes # Path to filesystem overlay used -overlay = {{ doc_path }}/overlay +overlay = {{ builder_path }}/overlay # Hostname or regex to match the IPSec gateway included in the image router = ^(router|vpn|gw|gateway)\d*\. @@ -51,7 +51,7 @@ rename = wdr4300v1_tp_recovery.bin [tpl-archer-c7-factory] enabled = no title = TP-Link Archer C7 (Access Point), TFTP-friendly -command = {{ doc_path }}/builder/ap.sh +command = {{ builder_path }}/ap.sh model = archer-c7-v2 filename = archer-c7-v2-squashfs-factory-eu.bin rename = ArcherC7v2_tp_recovery.bin @@ -59,7 +59,7 @@ rename = ArcherC7v2_tp_recovery.bin [cf-e380ac-factory] enabled = no title = Comfast E380AC (Access Point), TFTP-friendly -command = {{ doc_path }}/builder/ap.sh +command = {{ builder_path }}/ap.sh model = cf-e380ac-v2 filename = cf-e380ac-v2-squashfs-factory.bin rename = firmware_auto.bin @@ -86,7 +86,7 @@ rename = ap-tl-wdr4300-v1-squashfs-sysupgrade.bin [tpl-archer-c7-sysupgrade] ;enabled = yes title = TP-Link Archer C7 (Access Point) -command = {{ doc_path }}/builder/ap.sh +command = {{ builder_path }}/ap.sh model = archer-c7-v2 filename = archer-c7-v2-squashfs-factory-eu.bin rename = ap-archer-c7-v2-squashfs-factory-eu.bin @@ -94,7 +94,7 @@ rename = ap-archer-c7-v2-squashfs-factory-eu.bin [cf-e380ac-sysupgrade] ;enabled = yes title = Comfast E380AC (Access Point) -command = {{ doc_path }}/builder/ap.sh +command = {{ builder_path }}/ap.sh model = cf-e380ac-v2 filename = cf-e380ac-v2-squashfs-factory.bin rename = ap-cf-e380ac-v2-squashfs-factory.bin @@ -102,7 +102,7 @@ rename = ap-cf-e380ac-v2-squashfs-factory.bin [ar150-mfp-sysupgrade] ;enabled = yes title = GL.iNet GL-AR150 (MFP) -command = {{ doc_path }}/builder/mfp.sh +command = {{ builder_path }}/mfp.sh model = gl-ar150 filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin rename = mfp-gl-ar150-squashfs-sysupgrade.bin @@ -110,7 +110,7 @@ rename = mfp-gl-ar150-squashfs-sysupgrade.bin [ar150-cam-sysupgrade] ;enabled = yes title = GL.iNet GL-AR150 (IP Camera) -command = {{ doc_path }}/builder/ipcam.sh +command = {{ builder_path }}/ipcam.sh model = gl-ar150 filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin rename = cam-gl-ar150-squashfs-sysupgrade.bin diff --git a/certidude/templates/server/server.conf b/certidude/templates/server/server.conf index 08cd9fb..543ced9 100644 --- a/certidude/templates/server/server.conf +++ b/certidude/templates/server/server.conf @@ -263,13 +263,17 @@ backend = sql # Database path for SQL backend database = sqlite://{{ directory }}/meta/db.sqlite -# URL format -url = {{ token_url }} +# URL format, router and protocols are substituted from the [service] section below +url = https://{{ common_name }}/#action=enroll&title=certidude.rocks&token=%(token)s&subject=%(subject_username)s&router=%(router)s&protocols=%(protocols)s # Token lifetime in minutes, 48 hours by default. # Note that code tolerates 5 minute clock skew. lifetime = 2880 +# Whether token allows overwriting certificate with same CN +;overwrite permitted = yes +overwrite permitted = no + [script] # Path to the folder with scripts that can be served to the clients, set none to disable scripting diff --git a/certidude/tokens.py b/certidude/tokens.py index e23b901..5db3b60 100644 --- a/certidude/tokens.py +++ b/certidude/tokens.py @@ -11,7 +11,7 @@ class TokenManager(RelationalMixin): def consume(self, uuid): now = datetime.utcnow() retval = self.get( - "select subject, mail, created, expires, profile from token where uuid = ? and created < ? and ? < expires and used is null", + "select subject, mail, created, expires, profile from token where uuid = ? and created <= ? and ? <= expires and used is null", uuid, now + const.CLOCK_SKEW_TOLERANCE, now - const.CLOCK_SKEW_TOLERANCE) @@ -53,10 +53,7 @@ class TokenManager(RelationalMixin): context.update(locals()) mailer.send("token.md", to=subject_mail, **context) - return { - "token": token, - "url": url, - } + return 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" diff --git a/setup.py b/setup.py index 878ee78..2affd86 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,8 @@ setup( url = "http://github.com/laurivosandi/certidude", packages=[ "certidude", - "certidude.api" + "certidude.api", + "certidude.api.utils" ], long_description=open("README.rst").read(), # Include here only stuff required to run certidude client @@ -24,6 +25,7 @@ setup( "configparser", "certbuilder", "csrbuilder", + "crlbuilder", "jinja2", ], scripts=[ @@ -31,7 +33,7 @@ setup( ], include_package_data = True, package_data={ - "certidude": ["certidude/templates/*"], + "certidude": ["certidude/templates/*", "certidude/static/*", "certidude/builder/*"], }, classifiers=[ "Development Status :: 4 - Beta", diff --git a/tests/test_cli.py b/tests/test_cli.py index 3b49c11..c013c85 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,5 +1,11 @@ import coverage +import json +import os +import pytest import pwd +import re +import shutil +import sys from asn1crypto import pem, x509 from oscrypto import asymmetric from csrbuilder import CSRBuilder, pem_armor_csr @@ -9,11 +15,6 @@ from importlib import reload from click.testing import CliRunner from datetime import datetime, timedelta from time import sleep -import json -import pytest -import shutil -import sys -import os coverage.process_startup() @@ -65,6 +66,8 @@ def clean_client(): files = [ "/etc/certidude/client.conf", "/etc/certidude/services.conf", + "/etc/certidude/client.conf.d/ca.conf", + "/etc/certidude/services.conf.d/ca.conf", "/etc/certidude/authority/ca.example.lan/ca_cert.pem", "/etc/certidude/authority/ca.example.lan/client_key.pem", "/etc/certidude/authority/ca.example.lan/server_key.pem", @@ -826,6 +829,35 @@ def test_cli_setup_authority(): # Issue token, needs legit router ^ os.system("certidude token issue userbot") + + clean_client() + + try: + os.makedirs("/etc/certidude/client.conf.d") + except FileExistsError: + pass + try: + os.makedirs("/etc/certidude/services.conf.d") + except FileExistsError: + pass + with open("/etc/certidude/client.conf.d/ca.conf", "w") as fh: + fh.write("[ca.example.lan]\n") + fh.write("trigger = interface up\n") + fh.write("system wide = true\n") + fh.write("common name = roadwarrior5\n") + fh.write("autosign = false\n") + with open("/etc/certidude/services.conf.d/ca.conf", "w") as fh: + fh.write("[OpenVPN to vpn.example.lan]\n") + fh.write("authority = ca.example.lan\n") + fh.write("remote = vpn.example.lan\n") + fh.write("service = network-manager/openvpn\n") + fh.write("[IPSec to ipsec.example.lan]\n") + fh.write("authority = ca.example.lan\n") + fh.write("remote = ipsec.example.lan\n") + fh.write("service = network-manager/strongswan\n") + + assert os.system("certidude enroll --skip-self") == 0 + ######################## # Test image builder ### ######################## @@ -849,7 +881,40 @@ def test_cli_setup_authority(): headers={"content-type": "application/x-www-form-urlencoded", "Authorization":admintoken}) assert r.status_code == 200 - # TODO: check consume + from certidude.tokens import TokenManager + from certidude.user import User + token_manager = TokenManager(config.TOKEN_DATABASE) + token = token_manager.issue(None, User.objects.get("userbot")) + assert re.match("[A-Za-z0-9]{32}$", token), token + + # TODO: submit garbage instead CSR + + # Invalid common name + r = client().simulate_put("/api/token/", + body = generate_csr("random"), + query_string = "token=%s" % token) + assert r.status_code == 400, r.text + + # Unknown token + token = token_manager.issue(None, User.objects.get("userbot")) + r = client().simulate_put("/api/token/", + body = generate_csr("userbot@random"), + query_string = "token=WpPQAgbnak84QgWjbMY4230JHi0hVYJP") + assert r.status_code == 403, r.text + + # Correct token + r = client().simulate_put("/api/token/", + body = generate_csr("userbot@random"), + query_string = "token=%s" % token) + assert r.status_code == 200, r.text + + # Overwrite prohibited + token = token_manager.issue(None, User.objects.get("userbot")) + r = client().simulate_put("/api/token/", + body = generate_csr("userbot@random"), + query_string = "token=%s" % token) + assert r.status_code == 409, r.text + ################################# @@ -1317,7 +1382,7 @@ def test_cli_setup_authority(): ### LDAP auth ### ################# - # Test LDAP bind auth fallback + # TODO: Test LDAP bind auth fallback usertoken = "Basic dXNlcmJvdDpTNGw0azRsNA==" admintoken = "Basic YWRtaW5ib3Q6UzRsNGs0bDQ=" @@ -1327,7 +1392,7 @@ def test_cli_setup_authority(): # 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/", headers={"Authorization":usertoken, "User-Agent": "Android", "Referer":"http://ca.example.lan/"}) - assert r.status_code == 400, r.text + assert r.status_code == 401, r.text assert "expected Negotiate" in r.text, r.text @@ -1413,6 +1478,7 @@ def test_cli_setup_authority(): os.system("certidude token list") os.system("certidude token purge") + os.system("certidude token purge -a") clean_server()
Copy-paste follownig to terminal as root user:
{% include "snippets/request-client.sh" %} +cat << EOF > '/etc/NetworkManager/system-connections/OpenVPN to {{ session.service.title }}' +{% include "snippets/networkmanager-openvpn.conf" %}EOF + +nmcli con reload +
{% include "snippets/request-client.sh" %} +cat << EOF > '/etc/NetworkManager/system-connections/IPSec to {{ session.service.title }}' +{% include "snippets/networkmanager-strongswan.conf" %}EOF + +nmcli con reload +