Initial commit

This commit is contained in:
Lauri Võsandi 2021-05-27 13:15:46 +03:00
commit d121e8417c
66 changed files with 2782 additions and 0 deletions

22
.htmlhintrc Normal file
View File

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

73
.htmllintrc Normal file
View File

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

13
.pre-commit-config.yaml Normal file
View File

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

15
Dockerfile Normal file
View File

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

5
entrypoint.sh Executable file
View File

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

156
nginx.conf Normal file
View File

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

4
static/502.json Normal file
View File

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

104
static/css/style.css Normal file
View File

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

Binary file not shown.

BIN
static/fonts/pt-sans.woff2 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 KiB

48
static/index.html Normal file
View File

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

795
static/js/certidude.js Normal file
View File

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

104
static/js/shell.js Normal file
View File

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

2
static/robots.txt Normal file
View File

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

17
static/shell.html Normal file
View 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>

View File

@ -0,0 +1,6 @@
[Unit]
Description=Renew certificates and update revocation lists
[Service]
Type=simple
ExecStart={{ sys.argv[0] }} enroll

View File

@ -0,0 +1,11 @@
[Unit]
Description=Run certidude enroll daily
[Timer]
OnCalendar=daily
Persistent=true
Unit=certidude-enroll.service
[Install]
WantedBy=timers.target

View File

@ -0,0 +1,8 @@
[Unit]
Description=Restart OpenVPN after suspend
[Service]
ExecStart=/usr/bin/pkill --signal SIGHUP --exact openvpn
[Install]
WantedBy=sleep.target

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

View File

@ -0,0 +1,2 @@
pip3 install --upgrade git+http://git.k-space.ee/pinecrypt/pinecrypt-client.git
certidude provision {{ authority.namespace }}

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

View File

@ -0,0 +1,29 @@
[connection]
certidude managed = true
id = {{ session.service.title }}
uuid = {{ uuid }}
type = vpn
[vpn]
service-type = org.freedesktop.NetworkManager.openvpn
connection-type = tls
cert-pass-flags 0
tap-dev = no
remote-cert-tls = server
remote = {{ 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

View File

@ -0,0 +1,23 @@
[connection]
certidude managed = true
id = {{ session.service.title }}
uuid = {{ uuid }}
type = {{ vpn }}
[vpn]
service-type = org.freedesktop.NetworkManager.strongswan
encap = no
virtual = yes
method = key
ipcomp = no
address = {{ 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

View 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;
}
}

View File

@ -0,0 +1,3 @@
[Timer]
OnCalendar=*:0/15
Persistent=true

View 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 %}

View 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
#}

View File

@ -0,0 +1,47 @@
# Generate keypair and submit CSR
{% if common_name %}$NAME = "{{ common_name }}"
{% else %}$NAME = $env:computername.toLower()
{% endif %}
@"
[NewRequest]
Subject = "CN=$NAME"
Exportable = FALSE
KeySpec = 1
KeyUsage = 0xA0
MachineKeySet = True
ProviderType = 12
RequestType = PKCS10
{% if 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"
#}

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

View 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

View 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" %}

View 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

View 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

View 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

View 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

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

View 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

View 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

View 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
#}

View File

@ -0,0 +1,3 @@
{% for key, value in certificate.attributes %}
<span class="badge badge-info" title="{{ key }}={{ value }}">{{ value }}</span>
{% endfor %}

View 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">&times;</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">&times;</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>&nbsp;</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>

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

View File

@ -0,0 +1,2 @@
<h1>{{ message.title }}</h1>
<p>{{ message.description }}</p>

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

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

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

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

View 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
View 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&nbsp;name</th><td>{{ certificate.common_name }}</td></tr>
<tr><th>Organizational&nbsp;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>

View 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 %}

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