Initial commit
22
.htmlhintrc
Normal 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
Normal 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
|
||||
}
|
13
.pre-commit-config.yaml
Normal 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
Normal 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
Executable 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
Normal 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
Normal 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"
|
||||
}
|
104
static/css/style.css
Normal 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; }
|
||||
|
BIN
static/fonts/gentium-basic.woff2
Normal file
BIN
static/fonts/pt-sans.woff2
Normal file
BIN
static/fonts/ubuntu-mono.woff2
Normal file
BIN
static/img/ubuntu-01-edit-connections.png
Normal file
After Width: | Height: | Size: 393 KiB |
BIN
static/img/ubuntu-02-network-connections.png
Normal file
After Width: | Height: | Size: 368 KiB |
BIN
static/img/ubuntu-03-import-saved-config.png
Normal file
After Width: | Height: | Size: 328 KiB |
BIN
static/img/ubuntu-04-select-file.png
Normal file
After Width: | Height: | Size: 139 KiB |
BIN
static/img/ubuntu-05-profile-imported.png
Normal file
After Width: | Height: | Size: 338 KiB |
BIN
static/img/ubuntu-06-ipv4-settings.png
Normal file
After Width: | Height: | Size: 318 KiB |
BIN
static/img/ubuntu-07-disable-default-route.png
Normal file
After Width: | Height: | Size: 342 KiB |
BIN
static/img/ubuntu-08-activate-connection.png
Normal file
After Width: | Height: | Size: 394 KiB |
BIN
static/img/windows-01-download-openvpn.png
Normal file
After Width: | Height: | Size: 132 KiB |
BIN
static/img/windows-02-install-openvpn.png
Normal file
After Width: | Height: | Size: 502 KiB |
BIN
static/img/windows-03-move-config-file.png
Normal file
After Width: | Height: | Size: 137 KiB |
BIN
static/img/windows-04-connect.png
Normal file
After Width: | Height: | Size: 583 KiB |
BIN
static/img/windows-05-connected.png
Normal file
After Width: | Height: | Size: 638 KiB |
48
static/index.html
Normal 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>
|
||||
|
795
static/js/certidude.js
Normal 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
Normal 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();
|
||||
}
|
2
static/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
17
static/shell.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Certidude server</title>
|
||||
<link href="/assets/css/bundle.css" rel="stylesheet" type="text/css"/>
|
||||
<!-- <link href="/css/shell.css" rel="stylesheet" type="text/css"/> -->
|
||||
<script type="text/javascript" src="/assets/js/bundle.js"></script>
|
||||
<script type="text/javascript" src="/js/shell.js"></script>
|
||||
</head>
|
||||
<body onLoad="init();">
|
||||
<input type='upload' onchange='onChooseFile();' />
|
||||
|
||||
<div id="terminal"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
6
templates/client/certidude.service
Normal file
@ -0,0 +1,6 @@
|
||||
[Unit]
|
||||
Description=Renew certificates and update revocation lists
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart={{ sys.argv[0] }} enroll
|
11
templates/client/certidude.timer
Normal file
@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Run certidude enroll daily
|
||||
|
||||
[Timer]
|
||||
OnCalendar=daily
|
||||
Persistent=true
|
||||
Unit=certidude-enroll.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
|
8
templates/client/openvpn-reconnect.service
Normal file
@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=Restart OpenVPN after suspend
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/pkill --signal SIGHUP --exact openvpn
|
||||
|
||||
[Install]
|
||||
WantedBy=sleep.target
|
57
templates/openvpn-client.conf
Normal file
@ -0,0 +1,57 @@
|
||||
# Copy this file to /etc/certidude/template.ovpn and customize as you see fit
|
||||
|
||||
# Note: don't append comments to lines, Ubuntu 16.04 NetworkManager importer is very picky
|
||||
# See more potential problems here:
|
||||
# https://askubuntu.com/questions/761684/error-the-plugin-does-not-support-import-capability-when-attempting-to-import
|
||||
|
||||
# Run as OpenVPN client, pull routes, DNS server, DNS suffix from gateway
|
||||
client
|
||||
|
||||
# OpenVPN gateway(s)
|
||||
nobind
|
||||
;proto udp
|
||||
;port 1194
|
||||
{% if servers %}
|
||||
remote-random
|
||||
{% for server in servers %}
|
||||
remote {{ server }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
remote 1.2.3.4
|
||||
{% endif %}
|
||||
|
||||
# Virtual network interface settings
|
||||
dev tun
|
||||
persist-tun
|
||||
|
||||
# Customize crypto settings
|
||||
;tls-version-min 1.2
|
||||
;tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384
|
||||
;cipher AES-256-CBC
|
||||
;auth SHA384
|
||||
|
||||
# Check that server presented certificate has TLS Server flag present
|
||||
remote-cert-tls server
|
||||
|
||||
# X.509 business
|
||||
persist-key
|
||||
<ca>
|
||||
{{ca}}
|
||||
</ca>
|
||||
<key>
|
||||
{{key}}
|
||||
</key>
|
||||
<cert>
|
||||
{{cert}}
|
||||
</cert>
|
||||
|
||||
# Revocation list
|
||||
# Tunnelblick doens't handle inlined CRL
|
||||
# hard to update as well
|
||||
;<crl-verify>
|
||||
;</crl-verify>
|
||||
|
||||
# Pre-shared key for extra layer of security
|
||||
;<ta>
|
||||
;</ta>
|
||||
|
2
templates/snippets/certidude-client.sh
Normal file
@ -0,0 +1,2 @@
|
||||
pip3 install --upgrade git+http://git.k-space.ee/pinecrypt/pinecrypt-client.git
|
||||
certidude provision {{ authority.namespace }}
|
98
templates/snippets/ios.mobileconfig
Normal file
@ -0,0 +1,98 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- https://developer.apple.com/library/content/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html -->
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>{{ gateway }}</string>
|
||||
<key>PayloadDescription</key>
|
||||
<string>IPSec IKEv2 VPN connection via {{ gateway }}</string>
|
||||
<!-- This is a reverse-DNS style unique identifier used to detect duplicate profiles -->
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>{{ gateway }}</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>{{ service_uuid }}</string>
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>PayloadContent</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>{{ gateway }}.conf1</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>{{ conf_uuid }}</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.vpn.managed</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>UserDefinedName</key>
|
||||
<string>{{ gateway }}</string>
|
||||
<key>VPNType</key>
|
||||
<string>IKEv2</string>
|
||||
<key>IKEv2</key>
|
||||
<dict>
|
||||
<key>RemoteAddress</key>
|
||||
<string>{{ gateway }}</string>
|
||||
<key>RemoteIdentifier</key>
|
||||
<string>{{ gateway }}</string>
|
||||
<key>LocalIdentifier</key>
|
||||
<string>{{ common_name }}</string>
|
||||
<key>ServerCertificateIssuerCommonName</key>
|
||||
<string>{{ authority.certificate.common_name }}</string>
|
||||
<key>ServerCertificateCommonName</key>
|
||||
<string>{{ gateway }}</string>
|
||||
<key>AuthenticationMethod</key>
|
||||
<string>Certificate</string>
|
||||
<key>IKESecurityAssociationParameters</key>
|
||||
<dict>
|
||||
<key>EncryptionAlgorithm</key>
|
||||
<string>AES-256</string>
|
||||
<key>IntegrityAlgorithm</key>
|
||||
<string>SHA2-384</string>
|
||||
<key>DiffieHellmanGroup</key>
|
||||
<integer>14</integer>
|
||||
</dict>
|
||||
<key>ChildSecurityAssociationParameters</key>
|
||||
<dict>
|
||||
<key>EncryptionAlgorithm</key>
|
||||
<string>AES-128-GCM</string>
|
||||
<key>IntegrityAlgorithm</key>
|
||||
<string>SHA2-256</string>
|
||||
<key>DiffieHellmanGroup</key>
|
||||
<integer>14</integer>
|
||||
</dict>
|
||||
<key>EnablePFS</key>
|
||||
<integer>1</integer>
|
||||
<key>PayloadCertificateUUID</key>
|
||||
<string>{{ p12_uuid }}</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>{{ common_name }}</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>{{ p12_uuid }}</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.security.pkcs12</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>PayloadContent</key>
|
||||
<data>{{ p12 }}</data>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>{{ authority.certificate.common_name }}</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>{{ ca_uuid }}</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.security.root</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>PayloadContent</key>
|
||||
<data>{{ ca }}</data>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
29
templates/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 = {{ authority.namespace }}
|
||||
key = {% if key_path %}{{ key_path }}{% else %}/etc/certidude/authority/{{ authority.namespace }}/host_key.pem{% endif %}
|
||||
cert = {% if certificate_path %}{{ certificate_path }}{% else %}/etc/certidude/authority/{{ authority.namespace }}/host_cert.pem{% endif %}
|
||||
ca = {% if authority_path %}{{ authority_path }}{% else %}/etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem{% endif %}
|
||||
tls-cipher = {{ authority.openvpn.tls_cipher }}
|
||||
cipher = {{ authority.openvpn.cipher }}
|
||||
auth = {{ authority.openvpn.auth }}
|
||||
{% 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
templates/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 = {{ authority.namespace }}
|
||||
userkey = {% if key_path %}{{ key_path }}{% else %}/etc/certidude/authority/{{ authority.namespace }}/host_key.pem{% endif %}
|
||||
usercert = {% if certificate_path %}{{ certificate_path }}{% else %}/etc/certidude/authority/{{ authority.namespace }}/host_cert.pem{% endif %}
|
||||
certificate = {% if authority_path %}{{ authority_path }}{% else %}/etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem{% endif %}
|
||||
ike = {{ authority.strongswan.ike }}
|
||||
esp = {{ authority.strongswan.esp }}
|
||||
proposal = yes
|
||||
|
||||
[ipv4]
|
||||
method = auto
|
||||
;route1 = 0.0.0.0/0
|
31
templates/snippets/nginx-https-site.conf
Normal file
@ -0,0 +1,31 @@
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name {{ common_name }};
|
||||
rewrite ^ https://{{ common_name }}\$request_uri?;
|
||||
}
|
||||
|
||||
server {
|
||||
root /var/www/html;
|
||||
add_header X-Frame-Options "DENY";
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
|
||||
listen 443 ssl;
|
||||
server_name $NAME;
|
||||
client_max_body_size 10G;
|
||||
ssl_certificate {{certificate_path}};
|
||||
ssl_certificate_key {{key_path}};
|
||||
ssl_client_certificate {{authority_path}};
|
||||
|
||||
# Uncomment following to enable mutual authentication with certificates
|
||||
#ssl_crl {{revocations_path}};
|
||||
#ssl_verify_client on;
|
||||
|
||||
location ~ \.php\$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/run/php5-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param REMOTE_USER \$ssl_client_s_dn_cn;
|
||||
include fastcgi_params;
|
||||
}
|
||||
}
|
||||
|
3
templates/snippets/nginx-ocsp-cache.timer
Normal file
@ -0,0 +1,3 @@
|
||||
[Timer]
|
||||
OnCalendar=*:0/15
|
||||
Persistent=true
|
31
templates/snippets/openvpn-client.conf
Normal file
@ -0,0 +1,31 @@
|
||||
client
|
||||
nobind
|
||||
remote {{ authority.namespace }} 1194 udp
|
||||
remote {{ authority.namespace }} 443 tcp
|
||||
proto udp
|
||||
port 1194
|
||||
tls-version-min {{ authority.openvpn.tls_version_min }}
|
||||
tls-cipher {{ authority.openvpn.tls_cipher }}
|
||||
cipher {{ authority.openvpn.cipher }}
|
||||
auth {{authority.openvpn.auth }}
|
||||
mute-replay-warnings
|
||||
reneg-sec 0
|
||||
remote-cert-tls server
|
||||
dev tun
|
||||
persist-tun
|
||||
persist-key
|
||||
{% if ca %}
|
||||
<ca>
|
||||
{{ ca }}
|
||||
</ca>
|
||||
{% else %}ca /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem{% endif %}
|
||||
{% if key %}
|
||||
<key>
|
||||
{{ key }}
|
||||
</key>
|
||||
{% else %}key /etc/certidude/authority/{{ authority.namespace }}/host_key.pem{% endif %}
|
||||
{% if cert %}
|
||||
<cert>
|
||||
{{ cert }}
|
||||
</cert>
|
||||
{% else %}cert /etc/certidude/authority/{{ authority.namespace }}/host_cert.pem{% endif %}
|
20
templates/snippets/openvpn-client.sh
Normal file
@ -0,0 +1,20 @@
|
||||
# Install packages on Ubuntu & Fedora
|
||||
which apt && apt install openvpn
|
||||
which dnf && dnf install openvpn
|
||||
|
||||
# Create OpenVPN configuration file
|
||||
cat > /etc/openvpn/{{ session.authority.namespace }}.conf << EOF
|
||||
{% include "snippets/openvpn-client.conf" %}
|
||||
EOF
|
||||
|
||||
# Restart OpenVPN service
|
||||
systemctl restart openvpn
|
||||
{#
|
||||
|
||||
Some notes:
|
||||
|
||||
- Ubuntu 16.04 ships OpenVPN 2.3 which doesn't support AES-128-GCM
|
||||
- NetworkManager's OpenVPN profile importer doesn't understand multiple remotes
|
||||
- Tunnelblick and OpenVPN Connect apps don't have a method to update CRL
|
||||
|
||||
#}
|
47
templates/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 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://{{ authority.namespace }}:8443/api/token/?token={{ token }}' `
|
||||
-Method PUT `{% else %}
|
||||
-Uri 'https://{{ authority.namespace }}: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"
|
||||
|
||||
#}
|
||||
|
11
templates/snippets/request-client.sh
Normal file
@ -0,0 +1,11 @@
|
||||
# Use short hostname as common name
|
||||
test -e /sbin/uci && NAME=$(uci get system.@system[0].hostname)
|
||||
test -e /bin/hostname && NAME=$(hostname)
|
||||
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
|
||||
|
||||
{% include "snippets/request-common.sh" %}
|
||||
# Submit CSR and save signed certificate
|
||||
curl --cert-status -f -L -H "Content-type: application/pkcs10" \
|
||||
--data-binary @/etc/certidude/authority/{{ authority.namespace }}/host_req.pem \
|
||||
-o /etc/certidude/authority/{{ authority.namespace }}/host_cert.pem \
|
||||
'http://{{ authority.namespace }}/api/request/?wait=yes&autosign=yes'
|
19
templates/snippets/request-common.sh
Normal file
@ -0,0 +1,19 @@
|
||||
# Create directories
|
||||
mkdir -p /etc/certidude/authority/{{ authority.namespace }}
|
||||
|
||||
# Delete CA certificate if checksum doesn't match
|
||||
echo {{ authority.certificate.md5sum }} /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem | md5sum -c \
|
||||
|| rm -fv /etc/certidude/authority/{{ authority.namespace }}/*.pem
|
||||
{% include "snippets/store-authority.sh" %}
|
||||
{% include "snippets/update-trust.sh" %}
|
||||
# Generate private key
|
||||
test -e /etc/certidude/authority/{{ authority.namespace }}/host_key.pem \
|
||||
|| {% if authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \
|
||||
-out /etc/certidude/authority/{{ authority.namespace }}/host_key.pem{% else %}openssl genrsa \
|
||||
-out /etc/certidude/authority/{{ authority.namespace }}/host_key.pem 2048{% endif %}
|
||||
test -e /etc/certidude/authority/{{ authority.namespace }}/host_req.pem \
|
||||
|| openssl req -new -sha384 -subj "/CN=$NAME" \
|
||||
-key /etc/certidude/authority/{{ authority.namespace }}/host_key.pem \
|
||||
-out /etc/certidude/authority/{{ authority.namespace }}/host_req.pem
|
||||
echo "If CSR submission fails, you can copy paste it to Certidude:"
|
||||
cat /etc/certidude/authority/{{ authority.namespace }}/host_req.pem
|
7
templates/snippets/request-server.sh
Normal file
@ -0,0 +1,7 @@
|
||||
# Use fully qualified name
|
||||
test -e /sbin/uci && NAME=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
|
||||
test -e /bin/hostname && NAME=$(hostname -f)
|
||||
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
|
||||
|
||||
{% include "snippets/request-common.sh" %}
|
||||
{% include "snippets/submit-request-wait.sh" %}
|
11
templates/snippets/setup-ocsp-caching.sh
Normal file
@ -0,0 +1,11 @@
|
||||
# See more on http://unmitigatedrisk.com/?p=241 why we're doing this
|
||||
cat << EOF > /etc/systemd/system/nginx-ocsp-cache.service
|
||||
{% include "snippets/nginx-ocsp-cache.service" %}EOF
|
||||
|
||||
cat << EOF > /etc/systemd/system/nginx-ocsp-cache.timer
|
||||
{% include "snippets/nginx-ocsp-cache.timer" %}EOF
|
||||
|
||||
systemctl enable nginx-ocsp-cache.service
|
||||
systemctl enable nginx-ocsp-cache.timer
|
||||
systemctl start nginx-ocsp-cache.service
|
||||
systemctl start nginx-ocsp-cache.timer
|
5
templates/snippets/store-authority.sh
Normal file
@ -0,0 +1,5 @@
|
||||
# Save CA certificate
|
||||
mkdir -p /etc/certidude/authority/{{ authority.namespace }}/
|
||||
test -e /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem \
|
||||
|| cat << EOF > /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem
|
||||
{{ authority.certificate.blob }}EOF
|
28
templates/snippets/strongswan-client.sh
Normal file
@ -0,0 +1,28 @@
|
||||
cat > /etc/ipsec.conf << EOF
|
||||
config setup
|
||||
strictcrlpolicy=yes
|
||||
|
||||
ca {{ authority.namespace }}
|
||||
auto=add
|
||||
cacert=/etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem
|
||||
|
||||
conn client-to-site
|
||||
auto=start
|
||||
right={{ authority.namespace }}
|
||||
rightsubnet=0.0.0.0/0
|
||||
rightca="{{ session.authority.certificate.distinguished_name }}"
|
||||
left=%defaultroute
|
||||
leftcert=/etc/certidude/authority/{{ authority.namespace }}/host_cert.pem
|
||||
leftsourceip=%config
|
||||
leftca="{{ session.authority.certificate.distinguished_name }}"
|
||||
keyexchange=ikev2
|
||||
keyingtries=%forever
|
||||
dpdaction=restart
|
||||
closeaction=restart
|
||||
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
||||
esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
||||
EOF
|
||||
|
||||
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ authority.namespace }}.pem" > /etc/ipsec.secrets
|
||||
|
||||
ipsec restart
|
17
templates/snippets/strongswan-patching.sh
Normal file
@ -0,0 +1,17 @@
|
||||
# Install packages on Ubuntu & Fedora, patch Fedora paths
|
||||
which apt && apt install strongswan
|
||||
which dnf && dnf install strongswan
|
||||
test -e /etc/strongswan && test -e /etc/ipsec.conf || ln -s strongswan/ipsec.conf /etc/ipsec.conf
|
||||
test -e /etc/strongswan && test -e /etc/ipsec.d || ln -s strongswan/ipsec.d /etc/ipsec.d
|
||||
test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets
|
||||
|
||||
# Set SELinux context
|
||||
chcon --type=home_cert_t /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem /etc/ipsec.d/cacerts/{{ authority.namespace }}.pem
|
||||
chcon --type=home_cert_t /etc/certidude/authority/{{ authority.namespace }}/host_cert.pem /etc/ipsec.d/certs/{{ authority.namespace }}.pem
|
||||
chcon --type=home_cert_t /etc/certidude/authority/{{ authority.namespace }}/host_key.pem /etc/ipsec.d/private/{{ authority.namespace }}.pem
|
||||
|
||||
# Patch AppArmor
|
||||
cat << EOF > /etc/apparmor.d/local/usr.lib.ipsec.charon
|
||||
/etc/certidude/authority/** r,
|
||||
EOF
|
||||
systemctl restart apparmor
|
6
templates/snippets/submit-request-wait.sh
Normal file
@ -0,0 +1,6 @@
|
||||
# Submit CSR and save signed certificate
|
||||
curl --cert-status -f -L -H "Content-type: application/pkcs10" \
|
||||
--cacert /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem \
|
||||
--data-binary @/etc/certidude/authority/{{ authority.namespace }}/host_req.pem \
|
||||
-o /etc/certidude/authority/{{ authority.namespace }}/host_cert.pem \
|
||||
'https://{{ authority.namespace }}:8443/api/request/?wait=yes'
|
4
templates/snippets/update-trust.ps1
Normal file
@ -0,0 +1,4 @@
|
||||
# Install CA certificate
|
||||
@"
|
||||
{{ authority.certificate.blob }}"@ | Out-File ca_cert.pem
|
||||
Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root
|
18
templates/snippets/update-trust.sh
Normal file
@ -0,0 +1,18 @@
|
||||
# Insert into Fedora trust store. Applies to curl, Firefox, Chrome, Chromium
|
||||
test -e /etc/pki/ca-trust/source/anchors \
|
||||
&& ln -s /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem /etc/pki/ca-trust/source/anchors/{{ authority.namespace }} \
|
||||
&& update-ca-trust
|
||||
|
||||
# Insert into Ubuntu trust store, only applies to curl
|
||||
test -e /usr/local/share/ca-certificates/ \
|
||||
&& ln -f -s /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem /usr/local/share/ca-certificates/{{ authority.namespace }}.crt \
|
||||
&& update-ca-certificates
|
||||
|
||||
# Patch Firefox trust store on Ubuntu
|
||||
if [ -d /usr/lib/firefox ]; then
|
||||
if [ ! -h /usr/lib/firefox/libnssckbi.so ]; then
|
||||
apt install -y p11-kit p11-kit-modules
|
||||
mv /usr/lib/firefox/libnssckbi.so /usr/lib/firefox/libnssckbi.so.bak
|
||||
ln -s /usr/lib/x86_64-linux-gnu/pkcs11/p11-kit-trust.so /usr/lib/firefox/libnssckbi.so
|
||||
fi
|
||||
fi
|
36
templates/snippets/windows.ps1
Normal file
@ -0,0 +1,36 @@
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
{% include "snippets/update-trust.ps1" %}
|
||||
|
||||
{% include "snippets/request-client.ps1" %}
|
||||
|
||||
# Set up IPSec VPN tunnel to {{ authority.namespace }}
|
||||
Remove-VpnConnection -AllUserConnection -Force "IPSec to {{ authority.namespace }}"
|
||||
Add-VpnConnection `
|
||||
-Name "IPSec to {{ authority.namespace }}" `
|
||||
-ServerAddress {{ authority.namespace }} `
|
||||
-AuthenticationMethod MachineCertificate `
|
||||
-EncryptionLevel Maximum `
|
||||
-SplitTunneling `
|
||||
-TunnelType ikev2 `
|
||||
-PassThru -AllUserConnection
|
||||
|
||||
# Harden VPN configuration
|
||||
Set-VpnConnectionIPsecConfiguration `
|
||||
-ConnectionName "IPSec to {{ authority.namespace }}" `
|
||||
-AuthenticationTransformConstants GCMAES128 `
|
||||
-CipherTransformConstants GCMAES128 `
|
||||
-EncryptionMethod AES256 `
|
||||
-IntegrityCheckMethod SHA384 `
|
||||
-DHGroup {% if authority.certificate.algorithm == "ec" %}ECP384{% else %}Group14{% endif %} `
|
||||
-PfsGroup {% if authority.certificate.algorithm == "ec" %}ECP384{% else %}PFS2048{% endif %} `
|
||||
-PassThru -AllUserConnection -Force
|
||||
|
||||
{#
|
||||
AuthenticationTransformConstants - ESP integrity algorithm, one of: None MD596 SHA196 SHA256128 GCMAES128 GCMAES192 GCMAES256
|
||||
CipherTransformConstants - ESP symmetric cipher, one of: DES DES3 AES128 AES192 AES256 GCMAES128 GCMAES192 GCMAES256
|
||||
EncryptionMethod - IKE symmetric cipher, one of: DES DES3 AES128 AES192 AES256
|
||||
IntegrityCheckMethod - IKE hash algorithm, one of: MD5 SHA196 SHA256 SHA384
|
||||
DHGroup = IKE key exchange, one of: None Group1 Group2 Group14 ECP256 ECP384 Group24
|
||||
PfsGroup = ESP key exchange, one of: None PFS1 PFS2 PFS2048 ECP256 ECP384 PFSMM PFS24
|
||||
#}
|
3
templates/views/attributes.html
Normal file
@ -0,0 +1,3 @@
|
||||
{% for key, value in certificate.attributes %}
|
||||
<span class="badge badge-info" title="{{ key }}={{ value }}">{{ value }}</span>
|
||||
{% endfor %}
|
270
templates/views/authority.html
Normal file
@ -0,0 +1,270 @@
|
||||
<div class="modal fade" id="request_submission_modal" role="dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4 class="modal-title">Request submission</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul class="nav nav-pills" id="myTab" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#snippet-certidude" role="tab" aria-controls="certidude" aria-selected="true">Certidude</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="profile-tab" data-toggle="tab" href="#snippet-windows" role="tab" aria-controls="windows" aria-selected="false">Windows</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-unix" role="tab" aria-controls="unix" aria-selected="false">UNIX</a>
|
||||
</li>
|
||||
|
||||
{% if "openvpn" in session.service.protocols %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-openvpn" role="tab" aria-controls="openvpn" aria-selected="false">OpenVPN</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if "ikev2" in session.service.protocols %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-strongswan" role="tab" aria-controls="strongswan" aria-selected="false">StrongSwan</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-lede" role="tab" aria-controls="lede" aria-selected="false">LEDE</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-copypaste" role="tab" aria-controls="copypaste" aria-selected="false">Copypasta</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<!-- Certidude client -->
|
||||
<div class="tab-pane fade show active" id="snippet-certidude" role="tabpanel" aria-labelledby="certidude">
|
||||
<p>On Ubuntu or Fedora:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>{% include "snippets/certidude-client.sh" %}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Windows -->
|
||||
<div class="tab-pane fade" id="snippet-windows" role="tabpanel" aria-labelledby="windows">
|
||||
<p>On Windows execute following PowerShell script</p>
|
||||
{% if "ikev2" in session.service.protocols %}
|
||||
<div class="highlight"><pre class="code"><code>{% include "snippets/windows.ps1" %}</code></pre></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- UNIX-like -->
|
||||
<div class="tab-pane fade" id="snippet-unix" role="tabpanel" aria-labelledby="unix">
|
||||
<p>For client certificates generate key pair and submit the signing request with common name set to short hostname:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>{% include "snippets/request-client.sh" %}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OpenVPN as client -->
|
||||
<div class="tab-pane fade" id="snippet-openvpn" role="tabpanel" aria-labelledby="openvpn">
|
||||
<p>First acquire certificates using the snippet above.</p>
|
||||
<p>Then install software:</p>
|
||||
<div class="highlight"><pre class="code"><code>{% include "snippets/openvpn-client.sh" %}</code></pre></div>
|
||||
</div>
|
||||
|
||||
<!-- StrongSwan as client -->
|
||||
<div class="tab-pane fade" id="snippet-strongswan" role="tabpanel" aria-labelledby="strongswan">
|
||||
<p>First acquire certificates using the snippet above.</p>
|
||||
|
||||
<p>Then install software:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>{% include "snippets/strongswan-patching.sh" %}</code></pre>
|
||||
</div>
|
||||
|
||||
<p>To configure StrongSwan as roadwarrior:</p>
|
||||
<div class="highlight"><pre class="code"><code>{% include "snippets/strongswan-client.sh" %}</code></pre></div>
|
||||
</div>
|
||||
|
||||
<!-- Copy & paste -->
|
||||
<div class="tab-pane fade" id="snippet-copypaste" role="tabpanel" aria-labelledby="copypaste">
|
||||
<p>Use whatever tools you have available on your platform to generate
|
||||
keypair and just paste ASCII armored PEM file contents here and hit submit:</p>
|
||||
|
||||
<form action="/api/request/" method="post">
|
||||
<textarea id="request_body" style="width:100%; min-height: 10em;"
|
||||
placeholder="-----BEGIN CERTIFICATE REQUEST-----"></textarea>
|
||||
<div class="modal-footer">
|
||||
<div class="btn-group">
|
||||
<button type="button" onclick="onSubmitRequest();" class="btn btn-primary"><i class="fa fa-upload"></i> Submit</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal"><i class="fa fa-ban"></i> Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="revocation_list_modal" role="dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4 class="modal-title">Revocation lists</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>To fetch <a href="http://{{ authority.namespace }}/api/revoked/">certificate revocation list</a>:</p>
|
||||
<pre><code>curl http://{{ authority.namespace }}/api/revoked/ > crl.der
|
||||
curl http://{{ authority.namespace }}/api/revoked/ -L -H "Accept: application/x-pem-file"
|
||||
curl http://{{ authority.namespace }}/api/revoked/?wait=yes -L -H "Accept: application/x-pem-file" > crl.pem</code></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-lg-4 col-xl-3">
|
||||
<h3>Signed certificates</h3>
|
||||
|
||||
<p>Authority administration
|
||||
{% if authority.certificate.organization %}of {{ authority.certificate.organization }}{% endif %}
|
||||
allowed for
|
||||
{% for user in session.authorization.admin_users %}<a href="mailto:{{ user.mail}}">{{ user.given_name }} {{user.surname }}</a>{% if not loop.last %}, {% endif %}{% endfor %} from {% if "0.0.0.0/0" in session.authorization.admin_subnets %}anywhere{% else %}
|
||||
{% for subnet in session.authorization.admin_subnets %}{{ subnet }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}.
|
||||
Authority valid from
|
||||
<time class="timeago" datetime="{{ authority.certificate.signed }}">{{ authority.certificate.signed }}</time>
|
||||
until
|
||||
<time class="timeago" datetime="{{ authority.certificate.expires }}">{{ authority.certificate.expires }}</time>.
|
||||
Authority certificate can be downloaded from <a href="/api/certificate/">here</a>.
|
||||
Following certificates have been signed:</p>
|
||||
|
||||
<div id="signed-filter" class="btn-group-toggle" data-toggle="buttons">
|
||||
<label class="btn btn-primary"><input id="signed-filter-new" type="checkbox" autocomplete="off">New</label>
|
||||
<label class="btn btn-primary active"><input id="signed-filter-online" type="checkbox" autocomplete="off" checked>Online</label>
|
||||
<label class="btn btn-primary"><input id="signed-filter-offline" type="checkbox" autocomplete="off">Lately seen</label>
|
||||
<label class="btn btn-primary"><input id="signed-filter-dead" type="checkbox" autocomplete="off">Gone</label>
|
||||
</div>
|
||||
|
||||
<div id="signed_certificates">
|
||||
{% for certificate in session.signed | sort(attribute="signed", reverse=true) %}
|
||||
{% include "views/signed.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<p>Showing <span id="signed-filter-counter">-</span> of total <span id="signed-total">-</span> certificates</p>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-4 col-xl-3">
|
||||
{% if session.features.token %}
|
||||
<h3>Tokens</h3>
|
||||
<p>Tokens allow enrolling smartphones and third party devices.</p>
|
||||
<ul>
|
||||
<li>You can issue yourself a token to be used on a mobile device</li>
|
||||
<li>Enter username to issue a token to issue a token for another user</li>
|
||||
<li>Enter e-mail address to issue a token to guest users outside domain</li>
|
||||
</ul>
|
||||
<p>
|
||||
<div class="input-group">
|
||||
<input id="token_username" name="username" type="text" class="form-control" placeholder="Username" aria-describedby="sizing-addon2">
|
||||
<input id="token_mail" name="mail" type="mail" class="form-control" placeholder="Optional e-mail" aria-describedby="sizing-addon2">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-secondary" type="button" onClick="onIssueToken();"><i class="fa fa-send"></i> Send token</button>
|
||||
</span>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<p>Issued tokens:</p>
|
||||
<ul class="list-group">
|
||||
{% for token in session.tokens %}
|
||||
{% include "views/token.html" %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div id="token_qrcode"></div>
|
||||
{% endif %}
|
||||
|
||||
{% if session.authorization.request_subnets %}
|
||||
<p> </p>
|
||||
<h3>Pending requests</h3>
|
||||
|
||||
<p>Use Certidude client to apply for a certificate.
|
||||
|
||||
{% if not session.authorization.request_subnets %}
|
||||
Request submission disabled.
|
||||
{% elif "0.0.0.0/0" in session.authorization.request_subnets %}
|
||||
Request submission is enabled.
|
||||
{% else %}
|
||||
Request submission allowed from
|
||||
{% for subnet in session.authorization.request_subnets %}
|
||||
{{ subnet }}{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}.
|
||||
{% endif %}
|
||||
|
||||
See <a href="#request_submission_modal" data-toggle="modal">here</a> for more information on manual signing request upload.
|
||||
|
||||
{% if session.authorization.autosign_subnets %}
|
||||
{% if "0.0.0.0/0" in session.authorization.autosign_subnets %}
|
||||
All requests are automatically signed.
|
||||
{% else %}
|
||||
Requests from
|
||||
{% for subnet in session.authorization.autosign_subnets %}
|
||||
{{ subnet }}{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
are automatically signed.
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
</p>
|
||||
<div id="pending_requests">
|
||||
{% for request in session.requests | sort(attribute="submitted", reverse=true) %}
|
||||
{% include "views/request.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if session.builder.profiles %}
|
||||
<h3>LEDE imagebuilder</h3>
|
||||
<p>Hit a link to generate machine specific image. Note that this might take couple minutes to finish.</p>
|
||||
<ul>
|
||||
{% for name, title, filename in session.builder.profiles %}
|
||||
<li><a href="/api/builder/{{ name }}/{{ filename }}">{{ title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-4 col-xl-3">
|
||||
|
||||
<h3>Revoked certificates</h3>
|
||||
<p>Following certificates have been revoked{% if session.features.crl %}, for more information click
|
||||
<a href="#revocation_list_modal" data-toggle="modal">here</a>{% endif %}.</p>
|
||||
|
||||
{% for certificate in session.revoked | sort(attribute="revoked", reverse=true) %}
|
||||
{% include "views/revoked.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div id="column-log" class="col-sm-6 col-lg-4 col-xl-3 hidden-lg-down">
|
||||
<div class="loader-container">
|
||||
<div class="loader"></div>
|
||||
<p>Loading logs, this might take a while...</p>
|
||||
</div>
|
||||
<div class="content" style="display:none;">
|
||||
<h3>Log</h3>
|
||||
<div class="btn-group-toggle" data-toggle="buttons">
|
||||
<label class="btn btn-primary active"><input id="log-level-critical" type="checkbox" autocomplete="off" checked>Critical</label>
|
||||
<label class="btn btn-primary active"><input id="log-level-error" type="checkbox" autocomplete="off" checked>Error</label>
|
||||
<label class="btn btn-primary active"><input id="log-level-warning" type="checkbox" autocomplete="off" checked>Warn</label>
|
||||
<label class="btn btn-primary active"><input id="log-level-info" type="checkbox" autocomplete="off" checked>Info</label>
|
||||
<label class="btn btn-primary"><input id="log-level-debug" type="checkbox" autocomplete="off">Debug</label>
|
||||
</div>
|
||||
<ul id="log-entries" class="list-group">
|
||||
</ul>
|
||||
<p>Click here to load more entries</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
31
templates/views/configuration.html
Normal file
@ -0,0 +1,31 @@
|
||||
|
||||
<h1>Create a rule</h1>
|
||||
<p>
|
||||
|
||||
<datalist id="tag_autocomplete">
|
||||
|
||||
</datalist>
|
||||
|
||||
<span>Filter</span>
|
||||
<select id="tags_autocomplete"></select>
|
||||
attaches attribute
|
||||
<select>
|
||||
{% include 'views/tagtypes.html' %}
|
||||
</select>
|
||||
<span contenteditable>something</span>
|
||||
<button>Add rule</button>
|
||||
</p>
|
||||
|
||||
{% for grouper, items in configuration | groupby('tag_id') %}
|
||||
|
||||
<h1>Filter {{ items[0].match_key }} is {{ items[0].match_value }}</h1>
|
||||
<ul>
|
||||
|
||||
{% for item in items %}
|
||||
<li>Attach {{ item.key }} attribute {{ item.value }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
|
278
templates/views/enroll.html
Normal file
@ -0,0 +1,278 @@
|
||||
<!-- https://wiki.strongswan.org/projects/strongswan/wiki/AppleIKEv2Profile#Certificate-authentication -->
|
||||
|
||||
<!--
|
||||
|
||||
Browser status
|
||||
|
||||
- Edge doesn't work because they think data: urls are insecure
|
||||
- iphone QR code scanner's webview is constrained, cant download data: links
|
||||
- outlook.com via iphone mail client works
|
||||
- android gmail app works
|
||||
- chrome works
|
||||
- firefox works
|
||||
|
||||
OS/soft status
|
||||
|
||||
- OpenVPN works on everything
|
||||
- StrongSwan app works on Android
|
||||
- NetworkManager doesn't support importing .sswan files yet, so no IPSec support for Ubuntu or Fedora here yet
|
||||
|
||||
-->
|
||||
|
||||
<div id="enroll" class="row">
|
||||
<div class="loader-container">
|
||||
<div class="loader"></div>
|
||||
<p>Generating RSA keypair, this will take a while...</p>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 mt-3 edge-broken" style="display:none;">
|
||||
<!-- https://stackoverflow.com/questions/33154646/data-uri-link-a-href-data-doesnt-work-in-microsoft-edge?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa -->
|
||||
Microsoft Edge not supported, open the link with Chrome or Firefox
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 mt-3 option ubuntu linux openvpn">
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<h3 class="card-title">Ubuntu 16.04+</h3>
|
||||
<p class="card-text">Install OpenVPN plugin for NetworkManager by executing following two command in the terminal:
|
||||
|
||||
<pre><code># Ubuntu 16.04 ships with older OpenVPN 2.3, to support newer ciphers add OpenVPN's repo
|
||||
if [ $(lsb_relase -cs) == "xenial" ]; then
|
||||
wget -O - https://swupdate.openvpn.net/repos/repo-public.gpg|apt-key add -
|
||||
echo "deb http://build.openvpn.net/debian/openvpn/release/2.4 xenial main" > /etc/apt/sources.list.d/openvpn-aptrepo.list
|
||||
apt update
|
||||
apt install openvpn
|
||||
fi
|
||||
|
||||
sudo apt install -y network-manager-openvpn-gnome
|
||||
sudo systemctl restart network-manager
|
||||
</code></pre>
|
||||
|
||||
<p>
|
||||
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
|
||||
<button class="btn btn-secondary" type="button" data-toggle="collapse" data-target="#ubuntu-screenshots" aria-expanded="false" aria-controls="ubuntu-screenshots">
|
||||
Screenshots
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<div class="collapse" id="ubuntu-screenshots">
|
||||
<p>Open up network connections:</p>
|
||||
<p><img src="/img/ubuntu-01-edit-connections.png"/></p>
|
||||
<p>Hit <i>Add button</i>:</p>
|
||||
<p><img src="/img/ubuntu-02-network-connections.png"/></p>
|
||||
<p>Select <i>Import a saved VPN configuration...</i>:</p>
|
||||
<p><img src="/img/ubuntu-03-import-saved-config.png"/></p>
|
||||
<p>Select downloaded file:</p>
|
||||
<p><img src="/img/ubuntu-04-select-file.png"/></p>
|
||||
<p>Once profile is successfully imported following dialog appears:</p>
|
||||
<p><img src="/img/ubuntu-05-profile-imported.png"/></p>
|
||||
<p>By default all traffic is routed via VPN gateway, route only intranet subnets to the gateway select <i>Routes...</i> under <i>IPv4 Settings</i>:</p>
|
||||
<p><img src="/img/ubuntu-06-ipv4-settings.png"/></p>
|
||||
<p>Check <i>Use this connection only for resources on its network</i>:</p>
|
||||
<p><img src="/img/ubuntu-07-disable-default-route.png"/></p>
|
||||
<p>To activate the connection select it under <i>VPN Connections</i>:</p>
|
||||
<p><img src="/img/ubuntu-08-activate-connection.png"/></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 mt-3 option ubuntu linux openvpn advanced">
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<h3 class="card-title">Ubuntu 18.04+ (advanced)</h3>
|
||||
<p class="card-text">Copy-paste follownig to terminal as root user:</p>
|
||||
<pre><code>{% include "snippets/request-client.sh" %}
|
||||
cat << EOF > '/etc/NetworkManager/system-connections/OpenVPN to {{ authority.namespace }}'
|
||||
{% 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 {{ authority.namespace }}'
|
||||
{% 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">
|
||||
<div class="card-block">
|
||||
<h3 class="card-title">Fedora</h3>
|
||||
<p class="card-text">Install OpenVPN plugin for NetworkManager by running following two commands:</p>
|
||||
<pre><code>dnf install NetworkManager-openvpn-gnome
|
||||
systemctl restart NetworkManager</code></pre>
|
||||
Right click in the NetworkManager icon, select network settings. Hit the + button and select <i>Import from file...</i>, select the downloaded .ovpn file.
|
||||
Remove the .ovpn file from the Downloads folder.</p>
|
||||
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 mt-3 option windows ipsec">
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<h3 class="card-title">Windows</h3>
|
||||
<p class="card-text">
|
||||
Import PKCS#12 container to your machine trust store.
|
||||
Import VPN connection profile by moving the downloaded .pbk file to
|
||||
<pre><code>%userprofile%\AppData\Roaming\Microsoft\Network\Connections\PBK</code></pre>
|
||||
or
|
||||
<pre><code>C:\ProgramData\Microsoft\Network\Connections\Pbk</code></pre></p>
|
||||
<a href="javascript:onEnroll('p12');" class="btn btn-primary">Fetch PKCS#12 container</a>
|
||||
<a href="#" class="btn btn-secondary">Fetch IPSec IKEv2 VPN profile</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 mt-3 option windows ikev2">
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<h3 class="card-title">Windows</h3>
|
||||
<p>To configure IPSec IKEv2 tunnel on Windows, open PowerShell as administrator and copy-paste following:</p>
|
||||
<div class="highlight"><pre class="code"><code>{% include "snippets/windows.ps1" %}</code></pre></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 mt-3 option windows openvpn">
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<h3 class="card-title">Windows</h3>
|
||||
<p class="card-text">
|
||||
Install OpenVPN community edition client.
|
||||
Move the downloaded .ovpn file to C:\Program Files\OpenVPN\config and
|
||||
right click in the system tray on OpenVPN icon and select Connect from the menu.
|
||||
For finishing touch adjust the file permissions so only local
|
||||
administrator can read that file, remove regular user access to the file.
|
||||
</p>
|
||||
<a href="https://openvpn.net/index.php/download/community-downloads.html" class="btn btn-secondary">Get OpenVPN community edition</a>
|
||||
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
|
||||
<button class="btn btn-secondary" type="button" data-toggle="collapse" data-target="#windows-screenshots" aria-expanded="false" aria-controls="windows-screenshots">
|
||||
Screenshots
|
||||
</button>
|
||||
|
||||
<div class="collapse" id="windows-screenshots">
|
||||
<p>Download OpenVPN from the link supplied above:</p>
|
||||
<p><img src="/img/windows-01-download-openvpn.png"/></p>
|
||||
|
||||
<p>Install OpenVPN:</p>
|
||||
<p><img src="/img/windows-02-install-openvpn.png"/></p>
|
||||
|
||||
<p>Move the configuraiton file downloaded from the second button above:</p>
|
||||
<p><img src="/img/windows-03-move-config-file.png"/></p>
|
||||
|
||||
<p>Connect from system tray:</p>
|
||||
<p><img src="/img/windows-04-connect.png"/></p>
|
||||
|
||||
<p>Connection is successfully configured:</p>
|
||||
<p><img src="/img/windows-05-connected.png"/></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 mt-3 option mac openvpn">
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<h3 class="card-title">Mac OS X</h3>
|
||||
<p class="card-text">Download Tunnelblick. Tap on the button above and import the profile.</p>
|
||||
<a href="https://tunnelblick.net/" target="_blank" class="btn btn-secondary">Get Tunnelblick</a>
|
||||
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 mt-3 option iphone ipad openvpn">
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<h3 class="card-title">iPhone/iPad</h3>
|
||||
<p class="card-text">Install OpenVPN Connect app, tap on the button below.</p>
|
||||
<a href="https://itunes.apple.com/us/app/openvpn-connect/id590379981?mt=8" target="_blank" class="btn btn-secondary">Get OpenVPN Connect app</a>
|
||||
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 mt-3 option iphone ipad ikev2">
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<h3 class="card-title">iPhone/iPad</h3>
|
||||
<p class="card-text">
|
||||
Tap the button below, you'll be prompted about configuration profile, tap <i>Allow</i>.
|
||||
Hit <i>Install</i> in the top-right corner.
|
||||
Enter your passcode to unlock trust store.
|
||||
Tap <i>Install</i> and confirm by hitting <i>Install</i>.
|
||||
Where password for the certificate is prompted, enter 1234.
|
||||
Hit <i>Done</i>. Go to <i>Settings</i>, open VPN submenu and tap on the VPN profile to connect.
|
||||
</p>
|
||||
<a href="javascript:onEnroll('mobileconfig');" class="btn btn-primary">Fetch IPSec IKEv2 VPN profile</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 mt-3 option mac ikev2">
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<h3 class="card-title">Mac OS X</h3>
|
||||
<p class="card-text">
|
||||
Click on the button below, you'll be prompted about configuration profile, tap <i>Allow</i>.
|
||||
Hit <i>Install</i> in the top-right corner.
|
||||
Enter your passcode to unlock trust store.
|
||||
Tap <i>Install</i> and confirm by hitting <i>Install</i>.
|
||||
Where password for the certificate is prompted, enter 1234.
|
||||
Hit <i>Done</i>. Go to <i>Settings</i>, open VPN submenu and tap on the VPN profile to connect.
|
||||
</p>
|
||||
<a href="javascript:onEnroll('mobileconfig');" class="btn btn-primary">Fetch VPN profile</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 mt-3 option android openvpn">
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<h3 class="card-title">Android</h3>
|
||||
<p class="card-text">Intall OpenVPN Connect app on your device.
|
||||
Tap on the downloaded .ovpn file, OpenVPN Connect should prompt for import.
|
||||
Hit <i>Accept</i> and then <i>Connect</i>.
|
||||
Remember to delete any remaining .ovpn files under the <i>Downloads</i>.
|
||||
</p>
|
||||
<a href="https://play.google.com/store/apps/details?id=net.openvpn.openvpn" target="_blank" class="btn btn-secondary">Get OpenVPN Connect app</a>
|
||||
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 mt-3 option android ikev2">
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<h3 class="card-title">Android</h3>
|
||||
<p class="card-text">
|
||||
Install strongSwan Client app on your device.
|
||||
Tap on the downloaded .sswan file, StrongSwan Client should prompt for import.
|
||||
Hit <i>Import certificate from VPN profile</i> and then <i>Import</i> in the top-right corner.
|
||||
Remember to delete any remaining .sswan files under the <i>Downloads</i>.
|
||||
</p>
|
||||
<a href="https://play.google.com/store/apps/details?id=org.strongswan.android" class="btn btn-secondary">Get strongSwan VPN Client app</a>
|
||||
<a href="javascript:onEnroll('sswan');" class="btn btn-primary">Fetch StrongSwan profile</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 mt-3 option any">
|
||||
<a href="javascript:$('.option').show();">I did't find an appropriate option for me, show all options</a>
|
||||
</div>
|
||||
|
||||
</div>
|
2
templates/views/error.html
Normal file
@ -0,0 +1,2 @@
|
||||
<h1>{{ message.title }}</h1>
|
||||
<p>{{ message.description }}</p>
|
14
templates/views/insecure.html
Normal file
@ -0,0 +1,14 @@
|
||||
<p>You're viewing this page over insecure channel.
|
||||
You can give it a try and <a href="https://{{ authority.hostname }}">connect over HTTPS</a>,
|
||||
if that succeeds all subsequents accesses of this page will go over HTTPS.
|
||||
</p>
|
||||
<p>
|
||||
Click <a href="/api/certificate">here</a> to fetch the certificate of this authority.
|
||||
Alternatively install certificate on Fedora or Ubuntu with following copy-pastable snippet:
|
||||
</p>
|
||||
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>{% include "snippets/store-authority.sh" %}
|
||||
{% include "snippets/update-trust.sh" %}</code></pre>
|
||||
</div>
|
||||
|
7
templates/views/lease.html
Normal file
@ -0,0 +1,7 @@
|
||||
Last seen
|
||||
<time class="timeago" datetime="{{ certificate.lease.last_seen }}">{{ certificate.lease.last_seen }}</time>
|
||||
at
|
||||
<a target="_blank" href="http://{{ certificate.lease.inner_address }}">{{ certificate.lease.inner_address }}</a>{% if certificate.lease.outer_address %}
|
||||
from
|
||||
<a target="{{ certificate.lease.outer_address }}" href="https://geoiplookup.net/ip/{{ certificate.lease.outer_address }}">{{ certificate.lease.outer_address }}</a>{% endif %}.
|
||||
See some stats <a href="http://172.20.1.19:19999/host/{{ certificate.common_name }}/" target="_blank">here</a>.
|
8
templates/views/logentry.html
Normal file
@ -0,0 +1,8 @@
|
||||
<li id="log_entry_{{ entry.id }}" data-keywords="{{ entry.message }}" class="list-group-item justify-content-between filterable{% if entry.fresh %} fresh{% endif %}">
|
||||
<span>
|
||||
<i class="fa fa-{{ entry.severity }}-circle"></i>
|
||||
{{ entry.message }}
|
||||
</span>
|
||||
<span class="badge badge-default badge-pill">{{ entry.created }}</span>
|
||||
</li>
|
||||
|
64
templates/views/request.html
Normal file
@ -0,0 +1,64 @@
|
||||
<div id="request-{{ request.id }}" class="card filterable mt-3"
|
||||
data-keywords="{{ request.common_name }}|" data-id="{{request.id}}">
|
||||
<div class="card-header">
|
||||
{% if certificate.server %}
|
||||
<i class="fa fa-server"></i>
|
||||
{% else %}
|
||||
<i class="fa fa-laptop"></i>
|
||||
{% endif %}
|
||||
{{ request.common_name }}
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<p class="mb-1">
|
||||
Submitted
|
||||
<time class="timeago" datetime="{{ request.submitted }}">Request was submitted {{ request.submitted }}</time>
|
||||
from
|
||||
{% if request.hostname %}{{request.hostname}} ({{request.address}}){% else %}{{request.address}}{% endif %}
|
||||
</p>
|
||||
<div class="btn-group">
|
||||
<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="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="onSignRequest(event, '{{ request.common_name }}', '{{ request.sha256sum }}');">
|
||||
<i class="fa fa-thumbs-up"></i> Approve</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>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
{% for p in authority.signature.profiles %}
|
||||
<a class="dropdown-item{% if not request.common_name.match(p.common_name) %} disabled{% endif %}"
|
||||
{% if not request.common_name.match(p.common_name) %} title="Common name doesn't match expression {{ p.common_name }}"{% endif %}
|
||||
href="#" onclick="javascript:$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}&profile={{ p.key }}',type:'post'});">
|
||||
{{ p.key }}, expires in {{ p.lifetime }} days</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse" id="details-{{ request.sha256sum }}">
|
||||
<p>Use following to fetch the signing request:</p>
|
||||
<div class="bd-example">
|
||||
<pre><code class="language-sh" data-lang="sh">wget <a href="/api/request/{{ request.common_name }}/">http://{{ authority.namespace }}/api/request/{{ request.common_name }}/</a>
|
||||
curl -L http://{{ authority.namespace }}/api/request/{{ request.common_name }}/ \
|
||||
| openssl req -text -noout</code></pre>
|
||||
</div>
|
||||
|
||||
<div style="overflow: auto; max-width: 100%;">
|
||||
<table class="table" id="signed_certificates">
|
||||
<tbody>
|
||||
<tr><th>Common name</th><td>{{ request.common_name }}</td></tr>
|
||||
<tr><th>Submitted</th><td>{{ request.submitted | datetime }}
|
||||
{% if request.address %}from {{ request.address }}
|
||||
{% if request.hostname %} ({{ request.hostname }}){% endif %}{% endif %}</td></tr>
|
||||
<tr><th>MD5</th><td>{{ request.md5sum }}</td></tr>
|
||||
<tr><th>SHA1</th><td>{{ request.sha1sum }}</td></tr>
|
||||
<tr><th>SHA256</th><td>{{ request.sha256sum }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
69
templates/views/revoked.html
Normal file
@ -0,0 +1,69 @@
|
||||
<div id="certificate-{{ certificate.common_name | replace('@', '--') | replace('.', '-') }}" class="card filterable mt-3"
|
||||
data-keywords="{{ certificate.common_name }}|">
|
||||
<div class="card-body">
|
||||
<div class="card-header">
|
||||
{% if certificate.server %}
|
||||
<i class="fa fa-server"></i>
|
||||
{% else %}
|
||||
<i class="fa fa-laptop"></i>
|
||||
{% endif %}
|
||||
{{ certificate.common_name }}
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<p>
|
||||
Serial number {{ certificate.serial | serial }}.
|
||||
</p>
|
||||
<p>
|
||||
Revoked
|
||||
<time class="timeago" datetime="{{ certificate.revoked }}">Certificate revoked {{ certificate.revoked }}</time>.
|
||||
Valid from {{ certificate.signed | datetime }} to {{ certificate.expired | datetime }}.
|
||||
</p>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-secondary" data-toggle="collapse" data-target="#details-{{ certificate.sha256sum }}"><i class="fa fa-list"></i> Details</button>
|
||||
<div class="btn-group">
|
||||
<a href="/api/signed/{{ certificate.common_name }}/" class="btn btn-secondary hidden-xs-down"><i class="fa fa-download"></i> Download</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse" id="details-{{ certificate.sha256sum }}">
|
||||
<p>To fetch certificate:</p>
|
||||
|
||||
<div class="bd-example">
|
||||
<pre><code class="language-sh" data-lang="sh">wget <a href="/api/revoked/{{ certificate.serial }}/">http://{{ authority.namespace }}/api/revoked/{{ certificate.serial }}/</a>
|
||||
curl http://{{ authority.namespace }}/api/revoked/{{ certificate.serial }}/ \
|
||||
| openssl x509 -text -noout</code></pre>
|
||||
</div>
|
||||
|
||||
<p>To perform online certificate status request</p>
|
||||
<pre><code class="language-bash" data-lang="bash">curl http://{{ authority.namespace }}/api/certificate/ > session.pem
|
||||
openssl ocsp -issuer session.pem -CAfile session.pem \
|
||||
-url http://{{ authority.namespace }}/api/ocsp/ \
|
||||
-serial 0x{{ certificate.serial }}</span></code></pre>
|
||||
|
||||
<p>
|
||||
<table class="table" id="signed_certificates">
|
||||
<tbody>
|
||||
<tr><th>Common name</th><td>{{ certificate.common_name }}</td></tr>
|
||||
<tr><th>Organizational unit</th><td>{{ certificate.organizational_unit }}</td></tr>
|
||||
<tr><th>Serial number</th><td>{{ certificate.serial }}</td></tr>
|
||||
<tr><th>Signed</th><td>{{ certificate.signed | datetime }}
|
||||
{% if certificate.signer %}, by {{ certificate.signer }}{% endif %}</td></tr>
|
||||
<tr><th>Expired</th><td>{{ certificate.expired | datetime }}</td></tr>
|
||||
{% if certificate.lease %}
|
||||
<tr><th>Lease</th><td><a href="http://{{ certificate.lease.inner_address }}">{{ certificate.lease.inner_address }}</a> at {{ certificate.lease.last_seen | datetime }}
|
||||
from <a href="https://geoiptool.com/en/?ip={{ certificate.lease.outer_address }}" target="_blank">{{ certificate.lease.outer_address }}</a>
|
||||
</td></tr>
|
||||
{% endif %}
|
||||
|
||||
<!--
|
||||
<tr><th>MD5</th><td>{{ certificate.md5sum }}</td></tr>
|
||||
<tr><th>SHA1</th><td>{{ certificate.sha1sum }}</td></tr>
|
||||
-->
|
||||
<tr><th>SHA256</th><td>{{ certificate.sha256sum }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
134
templates/views/signed.html
Normal file
@ -0,0 +1,134 @@
|
||||
<div id="certificate-{{ certificate.id }}" class="card filterable mt-3"
|
||||
{% if certificate.lease %}data-last-seen={{ certificate.lease.last_seen }}{% endif %}
|
||||
data-keywords="{{ certificate.common_name }}|{% if session.tagging %}{% for tag in certificate.tags %}{{ tag.id }}|{% endfor %}{% endif %}{% for key, value in certificate.attributes %}{{ key }}={{ value }}|{% endfor %}" data-id="{{certificate.id}}">
|
||||
<div class="card-header">
|
||||
{% if certificate.organizational_unit %}
|
||||
<i class="fa fa-folder" aria-hidden="true"></i>
|
||||
{{ certificate.organizational_unit }} /
|
||||
{% endif %}
|
||||
{% if certificate.extensions.extended_key_usage and "server_auth" in certificate.extensions.extended_key_usage %}
|
||||
<i class="fa fa-server"></i>
|
||||
{% else %}
|
||||
<i class="fa fa-laptop"></i>
|
||||
{% endif %}
|
||||
{{ certificate.common_name }}
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<p>
|
||||
<i class="fa fa-circle"></i>
|
||||
<span class="lease">
|
||||
{% if certificate.lease %}
|
||||
{% include "views/lease.html" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
Signed
|
||||
<time class="timeago" datetime="{{ certificate.signed }}">Certificate was signed {{ certificate.signed }}</time>{% if certificate.signer %} by {{ certificate.signer }}{% endif %},
|
||||
expires
|
||||
<time class="timeago" datetime="{{ certificate.expires }}">Certificate expires {{ certificate.expires }}</time>.
|
||||
</p>
|
||||
<p>
|
||||
{% if session.tagging %}
|
||||
<span class="tags" data-cn="{{ certificate.common_name }}">
|
||||
{% include "views/tags.html" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="attributes" data-cn="{{ certificate.common_name }}">
|
||||
{% include "views/attributes.html" %}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-secondary" data-toggle="collapse" data-target="#details-{{ certificate.sha256sum }}"><i class="fa fa-list"></i> Details</button>
|
||||
<button type="button" class="btn btn-danger"
|
||||
onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}',type:'delete'});">
|
||||
<i class="fa fa-ban"></i> Revoke</button>
|
||||
<button type="button" class="btn btn-danger dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#"
|
||||
onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=key_compromise',type:'delete'});">Revoke due to key compromise</a>
|
||||
<a class="dropdown-item" href="#"
|
||||
onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=cessation_of_operation',type:'delete'});">Revoke due to cessation of operation</a>
|
||||
<a class="dropdown-item" href="#"
|
||||
onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=privilege_withdrawn',type:'delete'});">Revoke due to withdrawn privilege</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
{% if session.tagging %}
|
||||
<button type="button" class="btn btn-default" onclick="onNewTagClicked(event);" data-key="other" data-cn="{{ certificate.common_name }}">
|
||||
<i class="fa fa-tag"></i> Tag</button>
|
||||
<button type="button" class="btn btn-default dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
{% for tag_category in session.tagging %}
|
||||
<a class="dropdown-item" href="#" data-key="{{ tag_category.name }}" data-cn="{{ certificate.common_name }}"
|
||||
onclick="onNewTagClicked(event);">{{ tag_category.title }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="collapse" id="details-{{ certificate.sha256sum }}">
|
||||
<p>To launch shell for this device, click <a href="#" onclick="onLaunchShell('{{ certificate.common_name }}')">here</a>
|
||||
<p>To fetch certificate:</p>
|
||||
|
||||
<div class="bd-example">
|
||||
<pre><code class="language-sh" data-lang="sh">wget <a href="/api/signed/{{ certificate.common_name }}/">http://{{ authority.namespace }}/api/signed/{{ certificate.common_name }}</a>
|
||||
curl -L http://{{ authority.namespace }}/api/signed/{{ certificate.common_name }}/ \
|
||||
| openssl x509 -text -noout</code></pre>
|
||||
</div>
|
||||
|
||||
{% if session.authorization.ocsp_subnets %}
|
||||
{% if certificate.responder_url %}
|
||||
<p>To perform online certificate status request{% if "0.0.0.0/0" not in session.authorization.ocsp_subnets %}
|
||||
from whitelisted {{ session.authorization.ocsp_subnets }} subnets{% endif %}:</p>
|
||||
<pre><code class="language-bash" data-lang="bash">curl http://{{ authority.namespace }}/api/certificate > session.pem
|
||||
openssl ocsp -issuer session.pem -CAfile session.pem \
|
||||
-url {{ certificate.responder_url }} \
|
||||
-serial 0x{{ certificate.serial }}</code></pre>
|
||||
{% else %}
|
||||
<p>Querying OCSP responder disabled for this certificate, see /etc/certidude/profile.conf how to enable if that's desired</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<p>To fetch script:</p>
|
||||
<pre><code class="language-bash" data-lang="bash">curl -L --cert-status https://{{ authority.namespace }}:8443/api/signed/{{ certificate.common_name }}/script/ \
|
||||
--cacert /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem \
|
||||
--key /etc/certidude/authority/{{ authority.namespace }}/host_key.pem \
|
||||
--cert /etc/certidude/authority/{{ authority.namespace }}/host_cert.pem</pre></code>
|
||||
|
||||
<div style="overflow: auto; max-width: 100%;">
|
||||
<table class="table" id="signed_certificates">
|
||||
<tbody>
|
||||
<tr><th>Common name</th><td>{{ certificate.common_name }}</td></tr>
|
||||
<tr><th>Organizational unit</th><td>{% if certificate.organizational_unit %}{{ certificate.organizational_unit }}{% else %}-{% endif %}</td></tr>
|
||||
<tr><th>Serial number</th><td style="word-wrap:break-word;">{{ certificate.serial | serial }}</td></tr>
|
||||
<tr><th>Signed</th><td>{{ certificate.signed | datetime }}{% if certificate.signer %} by {{ certificate.signer }}{% endif %}</td></tr>
|
||||
<tr><th>Expires</th><td>{{ certificate.expires | datetime }}</td></tr>
|
||||
{% if certificate.lease %}
|
||||
<tr><th>Lease</th><td><a href="http://{{ certificate.lease.inner_address }}">{{ certificate.lease.inner_address }}</a> at {{ certificate.lease.last_seen | datetime }}
|
||||
from <a href="https://geoiptool.com/en/?ip={{ certificate.lease.outer_address }}" target="_blank">{{ certificate.lease.outer_address }}</a>
|
||||
</td></tr>
|
||||
{% endif %}
|
||||
|
||||
<!--
|
||||
<tr><th>MD5</th><td>{{ certificate.md5sum }}</td></tr>
|
||||
<tr><th>SHA1</th><td>{{ certificate.sha1sum }}</td></tr>
|
||||
-->
|
||||
<tr><th>SHA256</th><td style="word-wrap:break-word; overflow-wrap: break-word; ">{{ certificate.sha256sum }}</td></tr>
|
||||
{% if certificate.key_usage %}
|
||||
<tr><th>Key usage</th><td>{{ certificate.key_usage | join(", ") | replace("_", " ") }}</td></tr>
|
||||
{% endif %}
|
||||
{% if certificate.extended_key_usage %}
|
||||
<tr><th>Extended key usage</th><td>{{ certificate.extended_key_usage | join(", ") | replace("_", " ") }}</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
6
templates/views/tags.html
Normal file
@ -0,0 +1,6 @@
|
||||
{% for tag in certificate.tags %}
|
||||
<span data-cn="{{ certificate.common_name }}"
|
||||
title="{{ tag }}"
|
||||
class="badge badge-default"
|
||||
onClick="onTagClicked(event);">{{ tag }}</span>
|
||||
{% endfor %}
|
10
templates/views/token.html
Normal file
@ -0,0 +1,10 @@
|
||||
<li id="token-{{ token.subject }}-{{ token.uuid }}" class="list-group-item filterable" data-keywords="">
|
||||
<span>
|
||||
<i class="fas fa-ticket-alt"></i>
|
||||
{{ token.uuid }}...
|
||||
<a href="mailto:{{ token.mail }}">{{ token.subject }}</a>
|
||||
{% if token.issuer %}{% if token.issuer != token.subject %}by {{ token.issuer }}{% else %}by himself{% endif %}{% else %}via shell{% endif %},
|
||||
expires
|
||||
<time class="timeago" datetime="{{ token.expires }}">{{ token.expires }}</time>
|
||||
</span>
|
||||
</li>
|