mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 00:05:19 +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:
parent
6299d468c0
commit
ef16bac80f
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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");
|
||||
|
@ -6,7 +6,7 @@
|
||||
<string>{{ gateway }}</string>
|
||||
<!-- This is a reverse-DNS style unique identifier used to detect duplicate profiles -->
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>org.example.vpn2</string>
|
||||
<string>{{ gateway }}</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>{{ service_uuid }}</string>
|
||||
<key>PayloadType</key>
|
||||
@ -17,9 +17,9 @@
|
||||
<array>
|
||||
<dict>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>org.example.vpn2.conf1</string>
|
||||
<string>{{ gateway }}.conf1</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>29e4456d-3f03-4f15-b46f-4225d89465b7</string>
|
||||
<string>{{ conf_uuid }}</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.vpn.managed</string>
|
||||
<key>PayloadVersion</key>
|
||||
@ -63,14 +63,14 @@
|
||||
<key>EnablePFS</key>
|
||||
<integer>1</integer>
|
||||
<key>PayloadCertificateUUID</key>
|
||||
<string>d60488c6-328e-4944-9c8d-61db8095c865</string>
|
||||
<string>{{ p12_uuid }}</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>ee.k-space.ca2.client</string>
|
||||
<string>{{ common_name }}</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>d60488c6-328e-4944-9c8d-61db8095c865</string>
|
||||
<string>{{ p12_uuid }}</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.security.pkcs12</string>
|
||||
<key>PayloadVersion</key>
|
||||
@ -80,14 +80,13 @@
|
||||
</dict>
|
||||
<dict>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>org.example.ca</string>
|
||||
<string>{{ session.authority.certificate.common_name }}</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>64988b2c-33e0-4adf-a432-6fbcae543408</string>
|
||||
<string>{{ ca_uuid }}</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.security.root</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<!-- This is the Base64 (PEM) encoded CA certificate -->
|
||||
<key>PayloadContent</key>
|
||||
<data>{{ ca }}</data>
|
||||
</dict>
|
||||
|
29
certidude/static/snippets/networkmanager-openvpn.conf
Normal file
29
certidude/static/snippets/networkmanager-openvpn.conf
Normal 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
|
23
certidude/static/snippets/networkmanager-strongswan.conf
Normal file
23
certidude/static/snippets/networkmanager-strongswan.conf
Normal 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
|
47
certidude/static/snippets/request-client.ps1
Normal file
47
certidude/static/snippets/request-client.ps1
Normal 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"
|
||||
|
||||
#}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
4
certidude/static/snippets/update-trust.ps1
Normal file
4
certidude/static/snippets/update-trust.ps1
Normal 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
|
@ -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 `
|
||||
|
@ -77,6 +77,36 @@ sudo systemctl restart network-manager
|
||||
</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="card">
|
||||
|
@ -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-danger"
|
||||
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>
|
||||
<button type="button" class="btn btn-success"
|
||||
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>
|
||||
<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>
|
||||
|
@ -8,7 +8,6 @@
|
||||
client
|
||||
|
||||
# OpenVPN gateway(s)
|
||||
comp-lzo
|
||||
nobind
|
||||
;proto udp
|
||||
;port 1194
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
6
setup.py
6
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",
|
||||
|
@ -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()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user