1
0
mirror of https://github.com/laurivosandi/certidude synced 2024-12-22 08:15:18 +00:00

Several updates #6

* Preliminary advanced snippets for claiming token
* Better frontend mouse click event handling
* Token overwrites now toggleable via config
* Disable compression for OpenVPN snippets
* Make sure image builder scripts are included in .whl package
* Token mechanism tests
* Various bugfixes
This commit is contained in:
Lauri Võsandi 2018-05-20 13:46:27 +00:00
parent 6299d468c0
commit ef16bac80f
21 changed files with 311 additions and 116 deletions

View File

@ -24,9 +24,6 @@ class TokenResource(AuthorityHandler):
AuthorityHandler.__init__(self, authority) AuthorityHandler.__init__(self, authority)
self.manager = manager self.manager = manager
def on_get(self, req, resp):
return
def on_put(self, req, resp): def on_put(self, req, resp):
try: try:
username, mail, created, expires, profile = self.manager.consume(req.get_param("token", required=True)) 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) header, _, der_bytes = pem.unarmor(body)
csr = CertificationRequest.load(der_bytes) csr = CertificationRequest.load(der_bytes)
common_name = csr["certification_request_info"]["subject"].native["common_name"] 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: try:
_, resp.body = self.authority._sign(csr, body, profile=config.PROFILES.get(profile), _, resp.body = self.authority._sign(csr, body, profile=config.PROFILES.get(profile),
overwrite=config.TOKEN_OVERWRITE_PERMITTED) overwrite=config.TOKEN_OVERWRITE_PERMITTED)

View File

@ -109,8 +109,9 @@ def authenticate(optional=False):
if kerberized: if kerberized:
if not req.auth.startswith("Negotiate "): if not req.auth.startswith("Negotiate "):
raise falcon.HTTPBadRequest("Bad request", raise falcon.HTTPUnauthorized("Unauthorized",
"Bad header, expected Negotiate") "Bad header, expected Negotiate",
["Negotiate"])
os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB
@ -158,7 +159,8 @@ def authenticate(optional=False):
else: else:
if not req.auth.startswith("Basic "): 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) basic, token = req.auth.split(" ", 1)
user, passwd = b64decode(token).decode("ascii").split(":", 1) user, passwd = b64decode(token).decode("ascii").split(":", 1)

View File

@ -581,7 +581,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
nm_config.add_section("vpn") nm_config.add_section("vpn")
nm_config.set("vpn", "service-type", "org.freedesktop.NetworkManager.openvpn") nm_config.set("vpn", "service-type", "org.freedesktop.NetworkManager.openvpn")
nm_config.set("vpn", "connection-type", "tls") 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", "cert-pass-flags", "0")
nm_config.set("vpn", "tap-dev", "no") nm_config.set("vpn", "tap-dev", "no")
nm_config.set("vpn", "remote-cert-tls", "server") # Assert TLS Server flag of X.509 certificate 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("ca %s\n" % paths.get("authority_path"))
config.write("crl-verify %s\n" % paths.get("revocations_path")) config.write("crl-verify %s\n" % paths.get("revocations_path"))
config.write("dh %s\n" % paths.get("dhparam_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("user nobody\n")
config.write("group nogroup\n") config.write("group nogroup\n")
config.write("persist-tun\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("cert %s\n" % paths.get("certificate_path"))
config.write("ca %s\n" % paths.get("authority_path")) config.write("ca %s\n" % paths.get("authority_path"))
config.write("crl-verify %s\n" % paths.get("revocations_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("user nobody\n")
config.write("group nogroup\n") config.write("group nogroup\n")
config.write("persist-tun\n") config.write("persist-tun\n")
@ -1081,9 +1081,6 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_confi
else: else:
raise ValueError("Fully qualified hostname not specified as common name, make sure hostname -f works") 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") template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "profile")
click.echo("Using templates from %s" % template_path) click.echo("Using templates from %s" % template_path)

View File

@ -100,6 +100,7 @@ TOKEN_URL = cp.get("token", "url")
TOKEN_BACKEND = cp.get("token", "backend") TOKEN_BACKEND = cp.get("token", "backend")
TOKEN_LIFETIME = timedelta(minutes=cp.getint("token", "lifetime")) # Convert minutes to seconds TOKEN_LIFETIME = timedelta(minutes=cp.getint("token", "lifetime")) # Convert minutes to seconds
TOKEN_DATABASE = cp.get("token", "database") TOKEN_DATABASE = cp.get("token", "database")
TOKEN_OVERWRITE_PERMITTED = cp.getboolean("token", "overwrite permitted")
# TODO: Check if we don't have base or servers # TODO: Check if we don't have base or servers
# The API call for looking up scripts uses following directory as root # 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")) 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")] 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_PROTOCOLS = set([j.lower() for j in cp.get("service", "protocols").split(" ") if j])
SERVICE_ROUTERS = cp.get("service", "routers") SERVICE_ROUTERS = cp.get("service", "routers")

View File

@ -6,6 +6,24 @@ const DEVICE_KEYWORDS = ["Android", "iPhone", "iPad", "Windows", "Ubuntu", "Fedo
jQuery.timeago.settings.allowFuture = true; 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) { function normalizeCommonName(j) {
return j.replace("@", "--").split(".").join("-"); // dafuq ?! return j.replace("@", "--").split(".").join("-"); // dafuq ?!
} }
@ -27,24 +45,6 @@ function onKeyGen() {
window.keys = forge.pki.rsa.generateKeyPair(KEY_SIZE); window.keys = forge.pki.rsa.generateKeyPair(KEY_SIZE);
console.info('Key-pair created.'); 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(); window.csr = forge.pki.createCertificationRequest();
csr.publicKey = keys.publicKey; csr.publicKey = keys.publicKey;
csr.setSubject([{ csr.setSubject([{
@ -82,17 +82,19 @@ function onKeyGen() {
$(".option.any").show(); $(".option.any").show();
} }
function onEnroll(encoding) { function blobToUuid(blob) {
console.info("Service name:", query.title);
var md = forge.md.md5.create(); var md = forge.md.md5.create();
md.update(query.title); md.update(blob);
var digest = md.digest().toHex(); var digest = md.digest().toHex();
var service_uuid = digest.substring(0, 8) + "-" + return digest.substring(0, 8) + "-" +
digest.substring(8, 12) + "-" + digest.substring(8, 12) + "-" +
digest.substring(12, 16) + "-" + digest.substring(12, 16) + "-" +
digest.substring(16,20) + "-" + digest.substring(16,20) + "-" +
digest.substring(20) digest.substring(20);
console.info("Service UUID:", service_uuid); }
function onEnroll(encoding) {
console.info("Service name:", query.title);
console.info("User agent:", window.navigator.userAgent); console.info("User agent:", window.navigator.userAgent);
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
@ -108,8 +110,8 @@ function onEnroll(encoding) {
var a = document.createElement("a"); var a = document.createElement("a");
var cert = forge.pki.certificateFromPem(xhr2.responseText); var cert = forge.pki.certificateFromPem(xhr2.responseText);
console.info("Got signed certificate:", xhr2.responseText); console.info("Got signed certificate:", xhr2.responseText);
var p12 = forge.pkcs12.toPkcs12Asn1( var p12 = forge.asn1.toDer(forge.pkcs12.toPkcs12Asn1(
keys.privateKey, [cert, ca], "", {algorithm: '3des'}); keys.privateKey, [cert, ca], "", {algorithm: '3des'})).getBytes();
switch(encoding) { switch(encoding) {
case 'p12': case 'p12':
@ -119,13 +121,13 @@ function onEnroll(encoding) {
break break
case 'sswan': case 'sswan':
var buf = JSON.stringify({ var buf = JSON.stringify({
uuid: service_uuid, uuid: blobToUuid(query.title),
name: query.title, name: query.title,
type: "ikev2-cert", type: "ikev2-cert",
'ike-proposal': 'aes256-sha384-prfsha384-modp2048', 'ike-proposal': 'aes256-sha384-prfsha384-modp2048',
'esp-proposal': 'aes128gcm16-modp2048', 'esp-proposal': 'aes128gcm16-modp2048',
remote: { addr: query.router }, 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); console.info("Buf is:", buf);
var mimetype = "application/vnd.strongswan.profile" var mimetype = "application/vnd.strongswan.profile"
@ -142,14 +144,18 @@ function onEnroll(encoding) {
a.download = query.title + ".ovpn"; a.download = query.title + ".ovpn";
break break
case 'mobileconfig': case 'mobileconfig':
var p12 = forge.pkcs12.toPkcs12Asn1( var p12 = forge.asn1.toDer(forge.pkcs12.toPkcs12Asn1(
keys.privateKey, [cert, ca], "1234", {algorithm: '3des'}); keys.privateKey, [cert, ca], "1234", {algorithm: '3des'})).getBytes();
var buf = nunjucks.render('snippets/ios.mobileconfig', { var buf = nunjucks.render('snippets/ios.mobileconfig', {
session: session, session: session,
service_uuid: blobToUuid(query.title),
conf_uuid: blobToUuid(query.title + " conf1"),
title: query.title, title: query.title,
common_name: common_name, common_name: common_name,
gateway: query.router, 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()) ca: forge.util.encode64(forge.asn1.toDer(forge.pki.certificateToAsn1(ca)).getBytes())
}); });
var mimetype = "application/x-apple-aspen-config"; var mimetype = "application/x-apple-aspen-config";
@ -179,6 +185,7 @@ function onEnroll(encoding) {
} }
function onHashChanged() { function onHashChanged() {
window.query = {}; window.query = {};
var a = location.hash.substring(1).split('&'); var a = location.hash.substring(1).split('&');
for (var i = 0; i < a.length; i++) { for (var i = 0; i < a.length; i++) {
@ -200,6 +207,23 @@ function onHashChanged() {
$("#view-dashboard").html(env.render('views/error.html', { message: msg })); $("#view-dashboard").html(env.render('views/error.html', { message: msg }));
}, },
success: function(blob) { 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 = { window.session = {
authority: { authority: {
hostname: window.location.hostname, hostname: window.location.hostname,
@ -221,7 +245,8 @@ function onHashChanged() {
} else { } else {
if (query.action == "enroll") { if (query.action == "enroll") {
$("#view-dashboard").html(env.render('views/enroll.html', { $("#view-dashboard").html(env.render('views/enroll.html', {
session:session, common_name: common_name,
session: session,
token: query.token, token: query.token,
})); }));
var options = document.querySelectorAll(".option"); var options = document.querySelectorAll(".option");

View File

@ -6,7 +6,7 @@
<string>{{ gateway }}</string> <string>{{ gateway }}</string>
<!-- This is a reverse-DNS style unique identifier used to detect duplicate profiles --> <!-- This is a reverse-DNS style unique identifier used to detect duplicate profiles -->
<key>PayloadIdentifier</key> <key>PayloadIdentifier</key>
<string>org.example.vpn2</string> <string>{{ gateway }}</string>
<key>PayloadUUID</key> <key>PayloadUUID</key>
<string>{{ service_uuid }}</string> <string>{{ service_uuid }}</string>
<key>PayloadType</key> <key>PayloadType</key>
@ -17,9 +17,9 @@
<array> <array>
<dict> <dict>
<key>PayloadIdentifier</key> <key>PayloadIdentifier</key>
<string>org.example.vpn2.conf1</string> <string>{{ gateway }}.conf1</string>
<key>PayloadUUID</key> <key>PayloadUUID</key>
<string>29e4456d-3f03-4f15-b46f-4225d89465b7</string> <string>{{ conf_uuid }}</string>
<key>PayloadType</key> <key>PayloadType</key>
<string>com.apple.vpn.managed</string> <string>com.apple.vpn.managed</string>
<key>PayloadVersion</key> <key>PayloadVersion</key>
@ -63,14 +63,14 @@
<key>EnablePFS</key> <key>EnablePFS</key>
<integer>1</integer> <integer>1</integer>
<key>PayloadCertificateUUID</key> <key>PayloadCertificateUUID</key>
<string>d60488c6-328e-4944-9c8d-61db8095c865</string> <string>{{ p12_uuid }}</string>
</dict> </dict>
</dict> </dict>
<dict> <dict>
<key>PayloadIdentifier</key> <key>PayloadIdentifier</key>
<string>ee.k-space.ca2.client</string> <string>{{ common_name }}</string>
<key>PayloadUUID</key> <key>PayloadUUID</key>
<string>d60488c6-328e-4944-9c8d-61db8095c865</string> <string>{{ p12_uuid }}</string>
<key>PayloadType</key> <key>PayloadType</key>
<string>com.apple.security.pkcs12</string> <string>com.apple.security.pkcs12</string>
<key>PayloadVersion</key> <key>PayloadVersion</key>
@ -80,14 +80,13 @@
</dict> </dict>
<dict> <dict>
<key>PayloadIdentifier</key> <key>PayloadIdentifier</key>
<string>org.example.ca</string> <string>{{ session.authority.certificate.common_name }}</string>
<key>PayloadUUID</key> <key>PayloadUUID</key>
<string>64988b2c-33e0-4adf-a432-6fbcae543408</string> <string>{{ ca_uuid }}</string>
<key>PayloadType</key> <key>PayloadType</key>
<string>com.apple.security.root</string> <string>com.apple.security.root</string>
<key>PayloadVersion</key> <key>PayloadVersion</key>
<integer>1</integer> <integer>1</integer>
<!-- This is the Base64 (PEM) encoded CA certificate -->
<key>PayloadContent</key> <key>PayloadContent</key>
<data>{{ ca }}</data> <data>{{ ca }}</data>
</dict> </dict>

View File

@ -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

View File

@ -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

View File

@ -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"
#}

View File

@ -25,4 +25,4 @@ EOF
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ session.authority.hostname }}.pem" > /etc/ipsec.secrets echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ session.authority.hostname }}.pem" > /etc/ipsec.secrets
ipsec restart apparmor ipsec restart

View File

@ -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 cat << EOF > /etc/apparmor.d/local/usr.lib.ipsec.charon
/etc/certidude/authority/** r, /etc/certidude/authority/** r,
EOF EOF
systemctl restart systemctl restart apparmor

View File

@ -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

View File

@ -1,37 +1,8 @@
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Install CA certificate {% include "snippets/update-trust.ps1" %}
@"
{{ 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 %}
# Generate keypair and submit CSR {% include "snippets/request-client.ps1" %}
$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 %}
{% for router in session.service.routers %} {% for router in session.service.routers %}
# Set up IPSec VPN tunnel to {{ router }} # Set up IPSec VPN tunnel to {{ router }}
@ -40,9 +11,12 @@ Add-VpnConnection `
-Name "IPSec to {{ router }}" ` -Name "IPSec to {{ router }}" `
-ServerAddress {{ router }} ` -ServerAddress {{ router }} `
-AuthenticationMethod MachineCertificate ` -AuthenticationMethod MachineCertificate `
-EncryptionLevel Maximum `
-SplitTunneling ` -SplitTunneling `
-TunnelType ikev2 ` -TunnelType ikev2 `
-PassThru -AllUserConnection -PassThru -AllUserConnection
# Harden VPN configuration
Set-VpnConnectionIPsecConfiguration ` Set-VpnConnectionIPsecConfiguration `
-ConnectionName "IPSec to {{ router }}" ` -ConnectionName "IPSec to {{ router }}" `
-AuthenticationTransformConstants GCMAES128 ` -AuthenticationTransformConstants GCMAES128 `

View File

@ -77,6 +77,36 @@ sudo systemctl restart network-manager
</div> </div>
</div> </div>
<div class="col-sm-12 mt-3 option ubuntu linux openvpn advanced">
<div class="card">
<div class="card-block">
<h3 class="card-title">Ubuntu 18.04+ (advanced)</h3>
<p class="card-text">Copy-paste follownig to terminal as root user:</p>
<pre><code>{% 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
</code></pre>
</div>
</div>
</div>
<div class="col-sm-12 mt-3 option ubuntu linux ikev2 advanced">
<div class="card">
<div class="card-block">
<h3 class="card-title">Ubuntu 18.04+ (advanced)</h3>
<p class="card-text">Copy-paste follownig to terminal as root user:</p>
<pre><code>{% 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
</code></pre>
</div>
</div>
</div>
<div class="col-sm-12 mt-3 option fedora linux openvpn"> <div class="col-sm-12 mt-3 option fedora linux openvpn">
<div class="card"> <div class="card">

View File

@ -19,11 +19,11 @@
<button type="button" class="btn btn-secondary" data-toggle="collapse" data-target="#details-{{ request.sha256sum }}"><i class="fa fa-list"></i> Details</button> <button type="button" class="btn btn-secondary" data-toggle="collapse" data-target="#details-{{ request.sha256sum }}"><i class="fa fa-list"></i> Details</button>
<button type="button" class="btn btn-danger" <button type="button" class="btn btn-danger"
data-loading-text="<i class='fa fa-circle-o-notch fa-spin'></i> Rejecting..." data-loading-text="<i class='fa fa-circle-o-notch fa-spin'></i> Rejecting..."
onclick="javascript:$(this).button('loading');$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}',type:'delete'});"> onclick="onRejectRequest(event, '{{ request.common_name }}', '{{ request.sha256sum }}');">
<i class="fa fa-trash"></i> Reject</button> <i class="fa fa-trash"></i> Reject</button>
<button type="button" class="btn btn-success" <button type="button" class="btn btn-success"
data-loading-text="<i class='fa fa-circle-o-notch fa-spin'></i> Processing Order" data-loading-text="<i class='fa fa-circle-o-notch fa-spin'></i> Processing Order"
onclick="javascript:$(this).button('loading');$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}',type:'post'});"> onclick="onSignRequest(event, '{{ request.common_name }}', '{{ request.sha256sum }}');">
<i class="fa fa-thumbs-up"></i> Sign</button> <i class="fa fa-thumbs-up"></i> Sign</button>
<button type="button" class="btn btn-success dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn btn-success dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span> <span class="sr-only">Toggle Dropdown</span>

View File

@ -8,7 +8,6 @@
client client
# OpenVPN gateway(s) # OpenVPN gateway(s)
comp-lzo
nobind nobind
;proto udp ;proto udp
;port 1194 ;port 1194

View File

@ -3,7 +3,7 @@
enabled = yes enabled = yes
# Path to filesystem overlay used # 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 # Hostname or regex to match the IPSec gateway included in the image
router = ^(router|vpn|gw|gateway)\d*\. router = ^(router|vpn|gw|gateway)\d*\.
@ -51,7 +51,7 @@ rename = wdr4300v1_tp_recovery.bin
[tpl-archer-c7-factory] [tpl-archer-c7-factory]
enabled = no enabled = no
title = TP-Link Archer C7 (Access Point), TFTP-friendly title = TP-Link Archer C7 (Access Point), TFTP-friendly
command = {{ doc_path }}/builder/ap.sh command = {{ builder_path }}/ap.sh
model = archer-c7-v2 model = archer-c7-v2
filename = archer-c7-v2-squashfs-factory-eu.bin filename = archer-c7-v2-squashfs-factory-eu.bin
rename = ArcherC7v2_tp_recovery.bin rename = ArcherC7v2_tp_recovery.bin
@ -59,7 +59,7 @@ rename = ArcherC7v2_tp_recovery.bin
[cf-e380ac-factory] [cf-e380ac-factory]
enabled = no enabled = no
title = Comfast E380AC (Access Point), TFTP-friendly title = Comfast E380AC (Access Point), TFTP-friendly
command = {{ doc_path }}/builder/ap.sh command = {{ builder_path }}/ap.sh
model = cf-e380ac-v2 model = cf-e380ac-v2
filename = cf-e380ac-v2-squashfs-factory.bin filename = cf-e380ac-v2-squashfs-factory.bin
rename = firmware_auto.bin rename = firmware_auto.bin
@ -86,7 +86,7 @@ rename = ap-tl-wdr4300-v1-squashfs-sysupgrade.bin
[tpl-archer-c7-sysupgrade] [tpl-archer-c7-sysupgrade]
;enabled = yes ;enabled = yes
title = TP-Link Archer C7 (Access Point) title = TP-Link Archer C7 (Access Point)
command = {{ doc_path }}/builder/ap.sh command = {{ builder_path }}/ap.sh
model = archer-c7-v2 model = archer-c7-v2
filename = archer-c7-v2-squashfs-factory-eu.bin filename = archer-c7-v2-squashfs-factory-eu.bin
rename = ap-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] [cf-e380ac-sysupgrade]
;enabled = yes ;enabled = yes
title = Comfast E380AC (Access Point) title = Comfast E380AC (Access Point)
command = {{ doc_path }}/builder/ap.sh command = {{ builder_path }}/ap.sh
model = cf-e380ac-v2 model = cf-e380ac-v2
filename = cf-e380ac-v2-squashfs-factory.bin filename = cf-e380ac-v2-squashfs-factory.bin
rename = ap-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] [ar150-mfp-sysupgrade]
;enabled = yes ;enabled = yes
title = GL.iNet GL-AR150 (MFP) title = GL.iNet GL-AR150 (MFP)
command = {{ doc_path }}/builder/mfp.sh command = {{ builder_path }}/mfp.sh
model = gl-ar150 model = gl-ar150
filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin
rename = mfp-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] [ar150-cam-sysupgrade]
;enabled = yes ;enabled = yes
title = GL.iNet GL-AR150 (IP Camera) title = GL.iNet GL-AR150 (IP Camera)
command = {{ doc_path }}/builder/ipcam.sh command = {{ builder_path }}/ipcam.sh
model = gl-ar150 model = gl-ar150
filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin
rename = cam-gl-ar150-squashfs-sysupgrade.bin rename = cam-gl-ar150-squashfs-sysupgrade.bin

View File

@ -263,13 +263,17 @@ backend = sql
# Database path for SQL backend # Database path for SQL backend
database = sqlite://{{ directory }}/meta/db.sqlite database = sqlite://{{ directory }}/meta/db.sqlite
# URL format # URL format, router and protocols are substituted from the [service] section below
url = {{ token_url }} 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. # Token lifetime in minutes, 48 hours by default.
# Note that code tolerates 5 minute clock skew. # Note that code tolerates 5 minute clock skew.
lifetime = 2880 lifetime = 2880
# Whether token allows overwriting certificate with same CN
;overwrite permitted = yes
overwrite permitted = no
[script] [script]
# Path to the folder with scripts that can be served to the clients, set none to disable scripting # Path to the folder with scripts that can be served to the clients, set none to disable scripting

View File

@ -11,7 +11,7 @@ class TokenManager(RelationalMixin):
def consume(self, uuid): def consume(self, uuid):
now = datetime.utcnow() now = datetime.utcnow()
retval = self.get( 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, uuid,
now + const.CLOCK_SKEW_TOLERANCE, now + const.CLOCK_SKEW_TOLERANCE,
now - const.CLOCK_SKEW_TOLERANCE) now - const.CLOCK_SKEW_TOLERANCE)
@ -53,10 +53,7 @@ class TokenManager(RelationalMixin):
context.update(locals()) context.update(locals())
mailer.send("token.md", to=subject_mail, **context) mailer.send("token.md", to=subject_mail, **context)
return { return token
"token": token,
"url": url,
}
def list(self, expired=False, used=False): 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" 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"

View File

@ -14,7 +14,8 @@ setup(
url = "http://github.com/laurivosandi/certidude", url = "http://github.com/laurivosandi/certidude",
packages=[ packages=[
"certidude", "certidude",
"certidude.api" "certidude.api",
"certidude.api.utils"
], ],
long_description=open("README.rst").read(), long_description=open("README.rst").read(),
# Include here only stuff required to run certidude client # Include here only stuff required to run certidude client
@ -24,6 +25,7 @@ setup(
"configparser", "configparser",
"certbuilder", "certbuilder",
"csrbuilder", "csrbuilder",
"crlbuilder",
"jinja2", "jinja2",
], ],
scripts=[ scripts=[
@ -31,7 +33,7 @@ setup(
], ],
include_package_data = True, include_package_data = True,
package_data={ package_data={
"certidude": ["certidude/templates/*"], "certidude": ["certidude/templates/*", "certidude/static/*", "certidude/builder/*"],
}, },
classifiers=[ classifiers=[
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",

View File

@ -1,5 +1,11 @@
import coverage import coverage
import json
import os
import pytest
import pwd import pwd
import re
import shutil
import sys
from asn1crypto import pem, x509 from asn1crypto import pem, x509
from oscrypto import asymmetric from oscrypto import asymmetric
from csrbuilder import CSRBuilder, pem_armor_csr from csrbuilder import CSRBuilder, pem_armor_csr
@ -9,11 +15,6 @@ from importlib import reload
from click.testing import CliRunner from click.testing import CliRunner
from datetime import datetime, timedelta from datetime import datetime, timedelta
from time import sleep from time import sleep
import json
import pytest
import shutil
import sys
import os
coverage.process_startup() coverage.process_startup()
@ -65,6 +66,8 @@ def clean_client():
files = [ files = [
"/etc/certidude/client.conf", "/etc/certidude/client.conf",
"/etc/certidude/services.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/ca_cert.pem",
"/etc/certidude/authority/ca.example.lan/client_key.pem", "/etc/certidude/authority/ca.example.lan/client_key.pem",
"/etc/certidude/authority/ca.example.lan/server_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 ^ # Issue token, needs legit router ^
os.system("certidude token issue userbot") 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 ### # Test image builder ###
######################## ########################
@ -849,7 +881,40 @@ def test_cli_setup_authority():
headers={"content-type": "application/x-www-form-urlencoded", "Authorization":admintoken}) headers={"content-type": "application/x-www-form-urlencoded", "Authorization":admintoken})
assert r.status_code == 200 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 ### ### LDAP auth ###
################# #################
# Test LDAP bind auth fallback # TODO: Test LDAP bind auth fallback
usertoken = "Basic dXNlcmJvdDpTNGw0azRsNA==" usertoken = "Basic dXNlcmJvdDpTNGw0azRsNA=="
admintoken = "Basic YWRtaW5ib3Q6UzRsNGs0bDQ=" 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" # 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/", r = requests.get("http://ca.example.lan/api/",
headers={"Authorization":usertoken, "User-Agent": "Android", "Referer":"http://ca.example.lan/"}) 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 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 list")
os.system("certidude token purge") os.system("certidude token purge")
os.system("certidude token purge -a")
clean_server() clean_server()