Initial commit

ec-key-support
Lauri Võsandi 2021-05-27 13:15:46 +03:00
commit d121e8417c
66 changed files with 2782 additions and 0 deletions

22
.htmlhintrc 100644
View File

@ -0,0 +1,22 @@
{
"attr-lowercase": true,
"attr-no-duplication": true,
"attr-unsafe-chars": true,
"attr-value-double-quotes": true,
"attr-value-not-empty": true,
"doctype-first": false,
"head-script-disabled": true,
"href-abs-or-rel": true,
"id-class-ad-disabled": true,
"id-class-value": true,
"id-unique": true,
"img-alt-require": true,
"space-tab-mixed-disabled": true,
"spec-char-escape": true,
"src-not-empty": true,
"style-disabled": true,
"tag-pair": true,
"tag-self-close": true,
"tagname-lowercase": true,
"title-require": true
}

73
.htmllintrc 100644
View File

@ -0,0 +1,73 @@
{
"plugins": [], // npm modules to load
"maxerr": false,
"raw-ignore-regex": false,
"attr-bans": [
"align",
"background",
"bgcolor",
"border",
"frameborder",
"longdesc",
"marginwidth",
"marginheight",
"scrolling",
"style",
"width"
],
"indent-delta": false,
"indent-style": "nonmixed",
"indent-width": 2,
"indent-width-cont": false,
"spec-char-escape": true,
"text-ignore-regex": false,
"tag-bans": [
"style",
"b"
],
"tag-close": true,
"tag-name-lowercase": true,
"tag-name-match": true,
"tag-self-close": false,
"doctype-first": false,
"doctype-html5": false,
"attr-name-style": "dash",
"attr-name-ignore-regex": false,
"attr-no-dup": true,
"attr-no-unsafe-char": true,
"attr-order": false,
"attr-quote-style": "double",
"attr-req-value": true,
"attr-new-line": false,
"attr-validate": true,
"id-no-dup": true,
"id-class-style": false,
"id-class-no-ad": true,
"class-style": false,
"class-no-dup": false,
"id-class-ignore-regex": false,
"img-req-alt": true,
"img-req-src": true,
"html-valid-content-model": true,
"head-valid-content-model": true,
"href-style": false,
"link-req-noopener": true,
"label-req-for": true,
"line-end-style": "lf",
"line-no-trailing-whitespace": true,
"line-max-len": false,
"line-max-len-ignore-regex": false,
"head-req-title": true,
"title-no-dup": true,
"title-max-len": 60,
"html-req-lang": false,
"lang-style": "case",
"fig-req-figcaption": false,
"focusable-tabindex-style": false,
"input-radio-req-name": true,
"input-req-label": false,
"table-req-caption": false,
"table-req-header": false,
"tag-req-attr": false
}

View File

@ -0,0 +1,13 @@
repos:
- repo: https://github.com/jorisroovers/gitlint
rev: v0.15.1
hooks:
- id: gitlint
- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
rev: v1.1.1
hooks:
- id: htmlhint
args: [--config, .htmlhintrc]
- id: htmllint
- id: dockerfile_lint

15
Dockerfile 100644
View File

@ -0,0 +1,15 @@
FROM alpine as build
MAINTAINER lauri <lauri@pinecrypt.com>
RUN apk add --update npm nginx rsync bash
RUN npm install --silent --no-optional -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg xterm
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80 443 8443
WORKDIR /var/lib/nginx/html/
RUN rsync -avq /usr/lib/node_modules/font-awesome/fonts/ fonts/
COPY static ./
COPY templates templates
RUN nunjucks-precompile --include snippets --include views templates >> js/bundle.js
RUN bash -c 'cat /usr/lib/node_modules/{jquery/dist/jquery.min.js,tether/dist/js/tether.min.js,bootstrap/dist/js/bootstrap.min.js,node-forge/dist/forge.all.min.js,qrcode-svg/dist/qrcode.min.js,timeago/jquery.timeago.js,nunjucks/browser/nunjucks-slim.min.js,xterm/lib/xterm.js} >> js/bundle.js'
RUN bash -c 'cat /usr/lib/node_modules/{tether/dist/css/tether.min.css,bootstrap/dist/css/bootstrap.min.css,font-awesome/css/font-awesome.min.css,xterm/css/xterm.css} >> css/bundle.css'
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT /entrypoint.sh

5
entrypoint.sh 100755
View File

@ -0,0 +1,5 @@
#!/bin/sh
while [ ! -f /var/lib/certidude/server-secrets/self_cert.pem ]; do
sleep 1
done
exec nginx -g "daemon off; error_log /dev/stdout info;"

156
nginx.conf 100644
View File

@ -0,0 +1,156 @@
user nginx;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
}
http {
upstream read-write {
server 127.0.0.1:4001;
}
upstream ocsp-responder {
server 127.0.0.1:5001;
}
upstream builder {
server 127.0.0.1:7001;
}
upstream event {
server 127.0.0.1:8001;
}
resolver 127.0.0.11;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
##
# Gzip Settings
##
gzip on;
# Basic DoS prevention measures
limit_conn addr 100;
client_body_timeout 5s;
client_header_timeout 5s;
limit_conn_zone $binary_remote_addr zone=addr:10m;
# Backend configuration
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-SSL-CERT $ssl_client_cert;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
send_timeout 600;
# To use CA-s own certificate for frontend and mutually authenticated connections
ssl_certificate /var/lib/certidude/server-secrets/self_cert.pem;
ssl_certificate_key /var/lib/certidude/server-secrets/self_key.pem;
server {
# Section for serving insecure HTTP, note that this is suitable for
# OCSP, CRL-s etc which is already covered by PKI protection mechanisms.
listen 80 default_server;
# Proxy pass OCSP responder
location /api/ocsp/ {
proxy_pass http://ocsp-responder;
}
# Event server
location /api/event/ {
proxy_buffering off;
proxy_cache off;
proxy_pass http://event;
}
# Proxy pass to backend
location /api/ {
proxy_pass http://read-write;
}
}
server {
# Section for accessing web interface over HTTPS
listen 127.0.0.1:1443 ssl http2 default_server;
# HSTS header below should make sure web interface will be accessed over HTTPS only
# once it has been configured
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;";
#proxy pass event
location /api/event/ {
proxy_buffering off;
proxy_cache off;
proxy_pass http://event;
}
#Proxy pass longpoll
location /api/longpoll/ {
proxy_buffering off;
proxy_cache off;
proxy_pass http://event;
}
# OpenWrt image builder
location /api/build/ {
proxy_pass http://builder;
}
# Proxy pass to backend
location /api/ {
proxy_pass http://read-write;
}
# This is for Let's Encrypt enroll/renewal
location /.well-known/ {
alias /var/www/html/.well-known/;
}
}
server {
# Section for certificate authenticated HTTPS clients,
# for submitting information to CA eg. leases,
# for delivering scripts to clients,
# for exchanging messages over WebSockets
server_name $hostname;
listen 8443 ssl http2;
# Enforce OCSP stapling for the server certificate
# Note that even nginx 1.14.0 doesn't immideately populate the OCSP cache
# You need to run separate cronjob to populate the OCSP response cache
ssl_stapling on;
ssl_stapling_verify on;
# Allow client authentication with certificate,
# backend must still check if certificate was used for TLS handshake
ssl_verify_client optional;
ssl_client_certificate /var/lib/certidude/server-secrets/ca_cert.pem;
# Proxy pass to backend
location /api/ {
proxy_pass http://read-write;
}
}
}

4
static/502.json 100644
View File

@ -0,0 +1,4 @@
{
"title": "502 Bad Gateway",
"description": "It seems the server had bit of a hiccup, perhaps this helps: systemctl restart certidude-backend && journalctl -f"
}

View File

@ -0,0 +1,104 @@
@keyframes fresh {
from { background-color: #ffc107; }
to { background-color: white; }
}
.fresh {
animation-name: fresh;
animation-duration: 30s;
}
.loader-container {
margin: 20% auto 0 auto;
text-align: center;
}
.loader {
border: 16px solid #f3f3f3; /* Light grey */
border-top: 16px solid #3498db; /* Blue */
border-radius: 50%;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
display: inline-block;
}
@font-face {
font-family: 'PT Sans Narrow';
font-style: normal;
font-weight: 400;
src: local('PT Sans Narrow'), local('PTSans-Narrow'), url('../fonts/pt-sans.woff2') format('woff2');
}
@font-face {
font-family: 'Ubuntu Mono';
font-style: normal;
font-weight: 400;
src: local('Ubuntu Mono'), local('UbuntuMono-Regular'), url('../fonts/ubuntu-mono.woff2') format('woff2');
}
@font-face {
font-family: 'Gentium Basic';
font-style: normal;
font-weight: 400;
src: local('Gentium Basic'), local('GentiumBasic'), url('../fonts/gentium-basic.woff2') format('woff2');
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
body, input {
}
body, input {
}
pre {
font-family: 'Ubuntu Mono';
background: #333;
overflow: auto;
border: 1px solid #292929;
border-radius: 4px;
color: #ddd;
padding: 6px 10px;
}
pre code a {
color: #eef;
}
h1, h2 {
}
#view {
margin: 5em auto 5em auto;
}
footer div {
text-align: center;
}
svg {
position: relative;
}
.badge {
cursor: pointer;
}
.disabled {
pointer-events: none;
opacity: 0.4;
cursor: not-allowed;
}
#signed_certificates .filterable .fa-circle { color: #888888; }
#signed_certificates .filterable[data-state='online'] .fa-circle { color: #5cb85c; }
#signed_certificates .filterable[data-state='offline'] .fa-circle { color: #0275d8; }
#signed_certificates .filterable[data-state='dead'] .fa-circle { color: #d9534f; }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 KiB

48
static/index.html 100644
View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Pinecrypt Gateway</title>
<link href="/css/bundle.css" rel="stylesheet" type="text/css"/>
<link href="/css/style.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript" src="/js/bundle.js"></script>
<script type="text/javascript" src="/js/certidude.js"></script>
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
</head>
<body onhashchange="onHashChanged();">
<nav class="navbar navbar-toggleable-md navbar-inverse bg-inverse fixed-top">
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand" href="#columns=2">Pinecrypt Gateway</a>
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link disabled dashboard" href="#">Dashboard</a>
</li>
<li class="nav-item hidden-xl-up">
<a class="nav-link" href="#">Log</a>
</li>
</ul>
<div class="form-inline my-2 my-lg-0">
<input id="search" class="form-control mr-sm-2" style="display:none;" type="search" placeholder="🔍">
</div>
</div>
</nav>
<div id="view-dashboard" class="container-fluid" style="margin: 5em 0 0 0;">
<div class="loader-container">
<div class="loader"></div>
<p>Loading certificate authority...</p>
</div>
</div>
<footer class="footer">
<div class="container">
Pinecrypt Gateway by
<a href="https://pinecrypt.com">Pinecrypt Labs</a>
</div>
</footer>
</body>
</html>

View File

@ -0,0 +1,795 @@
'use strict';
const KEY_SIZE = 2048;
const DEVICE_KEYWORDS = ["Android", "iPhone", "iPad", "Windows", "Ubuntu", "Fedora", "Mac", "Linux"];
jQuery.timeago.settings.allowFuture = true;
function onLaunchShell(common_name) {
$.post({
url: "/api/signed/" + common_name + "/shell/",
error: function() {
alert("Failed to launch shell for: " + common_name);
},
success: function(blob) {
console.info(blob);
if (blob.action == "start-session") {
console.info("Returned shell blob:", blob);
var win = window.open("/shell.html#" + blob.source + "/" + blob.target, '_blank');
if (win) {
win.focus();
} else {
alert("Please disable popup blocker for this site!");
}
} else {
alert("Failed to start session");
}
}
});
return false;
}
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 ?!
}
function onShowAll() {
var options = document.querySelectorAll(".option");
for (i = 0; i < options.length; i++) {
options[i].style.display = "block";
}
}
function onKeyGen() {
if (window.navigator.userAgent.indexOf(" Edge/") >= 0) {
$("#enroll .loader-container").hide();
$("#enroll .edge-broken").show();
return;
}
window.keys = forge.pki.rsa.generateKeyPair(KEY_SIZE);
console.info('Key-pair created.');
window.csr = forge.pki.createCertificationRequest();
csr.publicKey = keys.publicKey;
csr.setSubject([{
name: 'commonName', value: common_name
}]);
csr.sign(keys.privateKey, forge.md.sha384.create());
console.info('Certification request created');
$("#enroll .loader-container").hide();
var prefix = null;
for (i in DEVICE_KEYWORDS) {
var keyword = DEVICE_KEYWORDS[i];
if (window.navigator.userAgent.indexOf(keyword) >= 0) {
prefix = keyword.toLowerCase();
break;
}
}
if (prefix == null) {
$(".option").show();
return;
}
var protocols = query.protocols.split(",");
console.info("Showing snippets for:", protocols);
for (var j = 0; j < protocols.length; j++) {
var options = document.querySelectorAll(".option." + protocols[j] + "." + prefix);
for (i = 0; i < options.length; i++) {
options[i].style.display = "block";
}
}
$(".option.any").show();
}
function blobToUuid(blob) {
var md = forge.md.md5.create();
md.update(blob);
var digest = md.digest().toHex();
return digest.substring(0, 8) + "-" +
digest.substring(8, 12) + "-" +
digest.substring(12, 16) + "-" +
digest.substring(16,20) + "-" +
digest.substring(20);
}
function onEnroll(encoding) {
console.info("Service name:", query.title);
console.info("User agent:", window.navigator.userAgent);
var xhr = new XMLHttpRequest();
xhr.open('GET', "/api/certificate/");
xhr.onload = function() {
if (xhr.status === 200) {
var ca = forge.pki.certificateFromPem(xhr.responseText);
console.info("Got CA certificate:");
var xhr2 = new XMLHttpRequest();
xhr2.open("PUT", "/api/token/?token=" + query.token );
xhr2.onload = function() {
if (xhr2.status === 200) {
var a = document.createElement("a");
var cert = forge.pki.certificateFromPem(xhr2.responseText);
console.info("Got signed certificate:", xhr2.responseText);
var p12 = forge.asn1.toDer(forge.pkcs12.toPkcs12Asn1(
keys.privateKey, [cert, ca], "", {algorithm: '3des'})).getBytes();
switch(encoding) {
case 'p12':
var buf = forge.asn1.toDer(p12).getBytes();
var mimetype = "application/x-pkcs12"
a.download = query.title + ".p12";
break
case 'sswan':
var buf = JSON.stringify({
uuid: blobToUuid(authority.namespace),
name: authority.namespace,
type: "ikev2-cert",
'ike-proposal': 'aes256-sha384-prfsha384-modp2048',
'esp-proposal': 'aes128gcm16-modp2048',
remote: {
addr: authority.namespace,
revocation: {
crl: false,
strict: true
}
},
local: {
p12: forge.util.encode64(p12)
}
});
console.info("Buf is:", buf);
var mimetype = "application/vnd.strongswan.profile"
a.download = query.title + ".sswan";
break
case 'ovpn':
var buf = nunjucks.render('snippets/openvpn-client.conf', {
authority: authority,
key: forge.pki.privateKeyToPem(keys.privateKey),
cert: xhr2.responseText,
ca: xhr.responseText
});
var mimetype = "application/x-openvpn-profile";
a.download = query.title + ".ovpn";
break
case 'mobileconfig':
var p12 = forge.asn1.toDer(forge.pkcs12.toPkcs12Asn1(
keys.privateKey, [cert, ca], "1234", {algorithm: '3des'})).getBytes();
var buf = nunjucks.render('snippets/ios.mobileconfig', {
authority: authority,
service_uuid: blobToUuid(query.title),
conf_uuid: blobToUuid(query.title + " conf1"),
title: query.title,
common_name: common_name,
gateway: authority.namespace,
p12_uuid: blobToUuid(p12),
p12: forge.util.encode64(p12),
ca_uuid: blobToUuid(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";
a.download = query.title + ".mobileconfig";
break
}
a.href = "data:" + mimetype + ";base64," + forge.util.encode64(buf);
console.info("Offering bundle for download");
document.body.appendChild(a); // Firefox needs this!
a.click();
} else {
if (xhr2.status == 403) { alert("Token used or expired"); }
console.info('Request failed. Returned status of ' + xhr2.status);
try {
var r = JSON.parse(xhr2.responseText);
console.info("Server said: " + r.title);
console.info(r.description);
} catch(e) {
console.info("Server said: " + xhr2.statusText);
}
}
};
xhr2.send(forge.pki.certificationRequestToPem(csr));
}
}
xhr.send();
}
function onHashChanged() {
window.query = {};
var a = location.hash.substring(1).split('&');
for (var i = 0; i < a.length; i++) {
var b = a[i].split('=');
query[decodeURIComponent(b[0])] = decodeURIComponent(b[1] || '');
}
console.info("Hash is now:", query);
$.get({
method: "GET",
url: "/api/bootstrap/",
error: function(response) {
if (response.responseJSON) {
var msg = response.responseJSON
} else {
var msg = { title: "Error " + response.status, description: response.statusText }
}
$("#view-dashboard").html(env.render('views/error.html', { message: msg }));
},
success: function(authority) {
window.authority = authority
// 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.common_name = prefix + "-" + dig.digest().toHex().substring(0, 5);
console.info("Device identifier:", common_name);
if (window.location.protocol != "https:") {
$("#view-dashboard").html(env.render('views/insecure.html', {authority:authority}));
} else {
if (query.action == "enroll") {
$("#view-dashboard").html(env.render('views/enroll.html', {
common_name: common_name,
authority: authority,
token: query.token,
}));
var options = document.querySelectorAll(".option");
for (i = 0; i < options.length; i++) {
options[i].style.display = "none";
}
setTimeout(onKeyGen, 100);
console.info("Generating key pair...");
} else {
loadAuthority(query);
}
}
}
});
}
function onTagClicked(e) {
e.preventDefault();
var cn = $(e.target).attr("data-cn");
var id = $(e.target).attr("title");
var value = $(e.target).html();
var updated = prompt("Enter new tag or clear to remove the tag", value);
if (updated == "") {
$(event.target).addClass("disabled");
$.ajax({
method: "DELETE",
url: "/api/signed/" + cn + "/tag/" + id + "/"
});
} else if (updated && updated != value) {
$(e.target).addClass("disabled");
$.ajax({
method: "PUT",
url: "/api/signed/" + cn + "/tag/" + id + "/",
data: { value: updated },
dataType: "text",
complete: function(xhr, status) {
console.info("Tag added successfully", xhr.status, status);
},
success: function() {
},
error: function(xhr, status, e) {
console.info("Submitting request failed with:", status, e);
alert(e);
}
});
}
return false;
}
function onNewTagClicked(e) {
e.preventDefault();
var cn = $(e.target).attr("data-cn");
var key = $(e.target).attr("data-key");
var value = prompt("Enter new " + key + " tag for " + cn);
if (!value) return;
if (value.length == 0) return;
var $container = $(".tags[data-cn='" + cn + "']");
$container.addClass("disabled");
$.ajax({
method: "POST",
url: "/api/signed/" + cn + "/tag/",
data: { value: value, key: key },
dataType: "text",
complete: function(xhr, status) {
console.info("Tag added successfully", xhr.status, status);
},
success: function() {
$container.removeClass("disabled");
},
error: function(xhr, status, e) {
console.info("Submitting request failed with:", status, e);
alert(e);
}
});
return false;
}
function onTagFilterChanged() {
var key = $(event.target).val();
console.info("New key is:", key);
}
function onLogEntry (e) {
if (e.data) {
e = JSON.parse(e.data);
e.fresh = true;
}
if ($("#log-level-" + e.severity).prop("checked")) {
$("#log-entries").prepend(env.render("views/logentry.html", {
entry: {
created: new Date(e.created).toLocaleString(),
message: e.message,
severity: e.severity,
fresh: e.fresh,
keywords: e.message.toLowerCase().split(/,?[ <>/]+/).join("|")
}
}));
}
};
function onRequestSubmitted(e) {
console.log("Request submitted:", e.data);
$.ajax({
method: "GET",
url: "/api/request/id/" + e.data + "/",
dataType: "json",
success: function(request, status, xhr) {
console.info("Going to prepend:", request);
onRequestDeleted(request.id); // Delete any existing ones just in case
$("#pending_requests").prepend(
env.render('views/request.html', { request: request, authority: authority }));
$("#pending_requests time").timeago();
},
error: function(response) {
console.info("Failed to retrieve certificate:", response);
}
});
}
function onRequestDeleted(e) {
console.log("Removing deleted request", e.data);
$("#request-" + e.data).remove();
}
function onLeaseUpdate(e) {
var slug = normalizeCommonName(e.data);
console.log("Lease updated:", e.data);
$.ajax({
method: "GET",
url: "/api/signed/" + e.data + "/lease/",
dataType: "json",
success: function(lease, status, xhr) {
console.info("Retrieved lease update details:", lease);
lease.age = (new Date() - new Date(lease.last_seen)) / 1000.0
var $lease = $("#certificate-" + slug + " .lease");
$lease.html(env.render('views/lease.html', {
certificate: {
lease: lease }}));
$("time", $lease).timeago();
filterSigned();
},
error: function(response) {
console.info("Failed to retrieve certificate:", response);
}
});
}
function onRequestSigned(e) {
console.log("Request signed:", e.data);
var id = e.data
//var slug = normalizeCommonName(e.data);
//console.log("Removing:", slug);
console.log("Removing:", e.data);
$("#request-" + id).slideUp("normal", function() { $(this).remove(); });
$("#certificate-" + id).slideUp("normal", function() { $(this).remove(); });
$.ajax({
method: "GET",
url: "/api/signed/id/" + e.data + "/",
dataType: "json",
success: function(certificate, status, xhr) {
console.info("Retrieved certificate:", certificate);
$("#signed_certificates").prepend(
env.render('views/signed.html', { certificate: certificate, session: session }));
$("#signed_certificates time").timeago(); // TODO: optimize?
filterSigned();
},
error: function(response) {
console.info("Failed to retrieve certificate:", response);
}
});
}
function onCertificateRevoked(e) {
console.log("Removing revoked certificate", e.data);
$("#certificate-" + e.data).slideUp("normal", function() { $(this).remove(); });
}
function onTagUpdated(e) {
var cn = e.data;
console.log("Tag updated event recevied", cn);
$.ajax({
method: "GET",
url: "/api/signed/" + cn + "/tag/",
dataType: "json",
success:function(tags, status, xhr) {
console.info("Updated", cn, "tags", tags);
$(".tags[data-cn='" + cn+"']").html(
env.render('views/tags.html', {
certificate: {
common_name: cn,
tags:tags }}));
}
})
}
function onAttributeUpdated(e) {
var cn = e.data;
console.log("Attributes updated", cn);
$.ajax({
method: "GET",
url: "/api/signed/" + cn + "/attr/",
dataType: "json",
success:function(attributes, status, xhr) {
console.info("Updated", cn, "attributes", attributes);
$(".attributes[data-cn='" + cn + "']").html(
env.render('views/attributes.html', {
certificate: {
common_name: cn,
attributes:attributes }}));
}
})
}
function onSubmitRequest() {
$.ajax({
method: "POST",
url: "/api/request/",
headers: {
"Accept": "application/json; charset=utf-8",
"Content-Type": "application/pkcs10"
},
data: $("#request_body").val(),
success:function(attributes, status, xhr) {
// Close the modal
$("[data-dismiss=modal]").trigger({ type: "click" });
},
error: function(xhr, status, e) {
console.info("Submitting request failed with:", status, e);
alert(e);
}
})
}
function onServerStarted() {
console.info("Server started");
location.reload();
}
function onServerStopped() {
$("#view-dashboard").html('<div class="loader"></div><p>Server under maintenance</p>');
console.info("Server stopped");
}
function onIssueToken() {
$.ajax({
method: "POST",
url: "/api/token/",
data: { username: $("#token_username").val(), mail: $("#token_mail").val() },
dataType: "text",
complete: function(xhr, status) {
console.info("Token sent successfully", xhr.status, status);
},
success: function(data) {
var url = JSON.parse(data).url;
console.info("DATA:", url);
var code = new QRCode({
content: url,
width: 512,
height: 512,
});
document.getElementById("token_qrcode").innerHTML = code.svg();
},
error: function(xhr, status, e) {
console.info("Submitting request failed with:", status, e);
alert(e);
}
});
}
function filterSigned() {
if ($("#search").val() != "") {
console.info("Not filtering by state since keyword filter is active");
return;
}
$("#signed_certificates .filterable").each(function(i,j) {
var last_seen = j.getAttribute("data-last-seen");
var state = "new";
if (last_seen) {
var age = (new Date() - new Date(last_seen)) / 1000;
if (age > 172800) {
state = "dead";
} else if (age > 10800) {
state = "offline";
} else {
state = "online";
}
}
j.setAttribute("data-state", state);
j.style.display = $("#signed-filter-" + state).prop("checked") ? "block" : "none";
});
$("#signed-total").html($("#signed_certificates .filterable").length);
$("#signed-filter-counter").html($("#signed_certificates .filterable:visible").length);
}
function loadAuthority(query) {
console.info("Loading CA, to debug: curl " + window.location.href + " --negotiate -u : -H 'Accept: application/json'");
$.ajax({
method: "GET",
url: "/api/session/",
dataType: "json",
error: function(response) {
if (response.responseJSON) {
var msg = response.responseJSON
} else {
var msg = { title: "Error " + response.status, description: response.statusText }
}
$("#view-dashboard").html(env.render('views/error.html', { message: msg }));
},
success: function(session, status, xhr) {
window.session = session;
console.info("Loaded:", session);
$("#login").hide();
$("#search").show();
/**
* Render authority views
**/
$("#view-dashboard").html(env.render('views/authority.html', {
session: session,
authority: authority,
window: window,
// Parameters for unified snippets
dhparam_path: "/etc/ssl/dhparam.pem",
key_path: "/etc/certidude/authority/" + window.authority.namespace + "/host_key.pem",
certificate_path: "/etc/certidude/authority/" + window.authority.namespace + "/host_cert.pem",
authority_path: "/etc/certidude/authority/" + window.authority.namespace + "/ca_cert.pem",
revocations_path: "/etc/certidude/authority/" + window.authority.namespace + "/crl.pem",
common_name: "$NAME"
}));
// Initial filtering
$("#signed-filter .btn").on('change', filterSigned);
filterSigned();
// Attach timeago events
$("time").timeago();
if (window.authority) {
$("#log input").each(function(i, e) {
console.info("e.checked:", e.checked , "and", e.id, "@localstorage is", localStorage[e.id], "setting to:", localStorage[e.id] || e.checked, "bool:", localStorage[e.id] || e.checked == "true");
e.checked = localStorage[e.id] ? localStorage[e.id] == "true" : e.checked;
});
$("#log input").change(function() {
localStorage[this.id] = this.checked;
});
console.info("Opening EventSource from:", window.session.events);
window.source = new EventSource(window.session.events);
source.onmessage = function(event) {
console.log("Received server-sent event:", event);
}
source.addEventListener("lease-update", onLeaseUpdate);
source.addEventListener("request-deleted", onRequestDeleted);
source.addEventListener("request-submitted", onRequestSubmitted);
source.addEventListener("request-signed", onRequestSigned);
source.addEventListener("certificate-revoked", onCertificateRevoked);
source.addEventListener("tag-update", onTagUpdated);
source.addEventListener("attribute-update", onAttributeUpdated);
source.addEventListener("server-started", onServerStarted);
source.addEventListener("server-stopped", onServerStopped);
}
$("nav#menu li").click(function(e) {
$("section").hide();
$("section#" + $(e.target).attr("data-section")).show();
});
$("#enroll").click(function() {
var keys = forge.pki.rsa.generateKeyPair(1024);
$.ajax({
method: "POST",
url: "/api/token/",
data: "username=" + session.user.name,
complete: function(xhr, status) {
console.info("Token generated successfully:", xhr, status);
},
error: function(xhr, status, e) {
console.info("Token generation failed:", status, e);
alert(e);
}
});
var privateKeyBuffer = forge.pki.privateKeyToPem(keys.privateKey);
});
/**
* Set up search bar
*/
$(window).on("search", function() {
var q = $("#search").val();
$(".filterable").each(function(i, e) {
if ($(e).attr("data-keywords").toLowerCase().indexOf(q) >= 0) {
$(e).show();
} else {
$(e).hide();
}
});
if (q.length == 0) {
filterSigned();
$("#signed-filter .btn").removeClass("disabled").prop("disabled", false);
} else {
$("#signed-filter .btn").addClass("disabled").prop("disabled", true);
}
});
/**
* Bind key up event of search bar
*/
$("#search").on("keyup", function() {
if (window.searchTimeout) { clearTimeout(window.searchTimeout); }
window.searchTimeout = setTimeout(function() { $(window).trigger("search"); }, 500);
});
if (session.request_submission_allowed) {
$("#request_submit").click(function() {
$(this).addClass("busy");
$.ajax({
method: "POST",
contentType: "application/pkcs10",
url: "/api/request/",
data: $("#request_body").val(),
dataType: "text",
complete: function(xhr, status) {
console.info("Request submitted successfully, server returned", xhr.status, status);
$("#request_submit").removeClass("busy");
},
success: function() {
// Clear textarea on success
$("#request_body").val("");
},
error: function(xhr, status, e) {
console.info("Submitting request failed with:", status, e);
alert(e);
}
});
});
}
$("nav .nav-link.dashboard").removeClass("disabled").click(function() {
$("#column-requests").show();
$("#column-signed").show();
$("#column-revoked").show();
$("#column-log").hide();
});
/**
* Fetch log entries
*/
if (session.features.logging) {
if ($("#column-log:visible").length) {
loadLog();
}
$("nav .nav-link.log").removeClass("disabled").click(function() {
loadLog();
$("#column-requests").show();
$("#column-signed").show();
$("#column-revoked").show();
$("#column-log").hide();
});
} else {
console.info("Log disabled");
}
}
});
}
function loadLog() {
if (window.log_initialized) {
console.info("Log already loaded");
return;
}
console.info("Loading log...");
window.log_initialized = true;
$.ajax({
method: "GET",
url: "/api/log/?limit=100",
dataType: "json",
success: function(entries, status, xhr) {
console.info("Got", entries.length, "log entries");
for (var j = entries.length-1; j--; ) {
onLogEntry(entries[j]);
};
source.addEventListener("log-entry", onLogEntry);
$("#column-log .loader-container").hide();
$("#column-log .content").show();
}
});
}
function datetimeFilter(s) {
return new Date(s);
}
function serialFilter(s) {
return s.substring(0,s.length-14) + " " +
s.substring(s.length-14);
}
$(document).ready(function() {
window.env = new nunjucks.Environment();
env.addFilter("datetime", datetimeFilter);
env.addFilter("serial", serialFilter);
onHashChanged();
});

104
static/js/shell.js 100644
View File

@ -0,0 +1,104 @@
function onChooseFile(event) {
if (!(window.File && window.FileReader && window.FileList && window.Blob)) {
alert('The File APIs are not fully supported in this browser.');
}
var files = event.target.files;
var reader = new FileReader();
// Closure to capture the file information.
reader.onload = (function(theFile) {
return function(e) {
// Render thumbnail.
var span = document.createElement('span');
span.innerHTML = ['<img class="thumb" src="', e.target.result,
'" title="', escape(theFile.name), '"/>'].join('');
document.getElementById('list').insertBefore(span, null);
};
})(f);
// Read in the image file as a data URL.
reader.readAsDataURL(f);
}
function init() {
var url = "wss://" + window.location.hostname + "/pipe/" + window.location.hash.substring(1);
console.info("Opening:", url);
var term = new Terminal({rows: 50, cols: 200});
term.open(document.getElementById('terminal'));
term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
const ws = new WebSocket(url);
// Connection opened
ws.addEventListener('open', function (event) {
console.log('Hello Server!');
ws.send(JSON.stringify({"type": "session-start", "cols": 200, "rows": 50}));
});
// Listen for messages
ws.addEventListener('message', function (event) {
var e = JSON.parse(event.data);
console.info(e);
switch (e.type) {
case "stdout":
var buf = atob(e.value);
term.write(buf.replace(/\n/g, '\n\r'));
break
case "exit":
term.write("=== Process finished, no more input accepted ===");
break;
}
});
// Connection opened
ws.addEventListener('close', function (event) {
console.log('Bye Server!');
});
function runFakeTerminal() {
if (term._initialized) {
return;
}
term._initialized = true;
term.prompt = () => {
term.write('\r\n$ ');
};
term.writeln('Welcome to xterm.js');
term.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
term.writeln('Type some keys and commands to play around.');
term.writeln('');
term.prompt();
term.on('key', function(key, ev) {
const printable = !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey;
/* if (ev.keyCode === 13) {
term.prompt();
} else if (ev.keyCode === 8) {
// Do not delete the prompt
if (term._core.buffer.x > 2) {
term.write('\b \b');
}
} else if (printable) {
// term.write(key);
}*/
console.log("Got keypress:", key);
if (key == "\r") key = "\n";
ws.send(JSON.stringify({"type":"stdin", "value":btoa(key)}));
});
term.on('paste', function(data) {
term.write(data);
});
}
runFakeTerminal();
}

View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /