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

View File

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

View File

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

View File

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

View File

@ -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");

View File

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

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
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
/etc/certidude/authority/** r,
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
# 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 `

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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