commit d121e8417c816b6a7556d6c87d2a12dec0d96aa6 Author: Lauri Võsandi Date: Thu May 27 13:15:46 2021 +0300 Initial commit diff --git a/.htmlhintrc b/.htmlhintrc new file mode 100644 index 0000000..1d6b2e3 --- /dev/null +++ b/.htmlhintrc @@ -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 +} diff --git a/.htmllintrc b/.htmllintrc new file mode 100644 index 0000000..11de22c --- /dev/null +++ b/.htmllintrc @@ -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 +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..caa5778 --- /dev/null +++ b/.pre-commit-config.yaml @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1fe54fc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM alpine as build +MAINTAINER lauri +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 diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..65af0a0 --- /dev/null +++ b/entrypoint.sh @@ -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;" diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..86a62c2 --- /dev/null +++ b/nginx.conf @@ -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; + } + } +} + diff --git a/static/502.json b/static/502.json new file mode 100644 index 0000000..e36047f --- /dev/null +++ b/static/502.json @@ -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" +} diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..7b14032 --- /dev/null +++ b/static/css/style.css @@ -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; } + diff --git a/static/fonts/gentium-basic.woff2 b/static/fonts/gentium-basic.woff2 new file mode 100644 index 0000000..917e1de Binary files /dev/null and b/static/fonts/gentium-basic.woff2 differ diff --git a/static/fonts/pt-sans.woff2 b/static/fonts/pt-sans.woff2 new file mode 100644 index 0000000..299b6c6 Binary files /dev/null and b/static/fonts/pt-sans.woff2 differ diff --git a/static/fonts/ubuntu-mono.woff2 b/static/fonts/ubuntu-mono.woff2 new file mode 100644 index 0000000..42fed3d Binary files /dev/null and b/static/fonts/ubuntu-mono.woff2 differ diff --git a/static/img/ubuntu-01-edit-connections.png b/static/img/ubuntu-01-edit-connections.png new file mode 100644 index 0000000..2a124f4 Binary files /dev/null and b/static/img/ubuntu-01-edit-connections.png differ diff --git a/static/img/ubuntu-02-network-connections.png b/static/img/ubuntu-02-network-connections.png new file mode 100644 index 0000000..665b887 Binary files /dev/null and b/static/img/ubuntu-02-network-connections.png differ diff --git a/static/img/ubuntu-03-import-saved-config.png b/static/img/ubuntu-03-import-saved-config.png new file mode 100644 index 0000000..fc5a2ef Binary files /dev/null and b/static/img/ubuntu-03-import-saved-config.png differ diff --git a/static/img/ubuntu-04-select-file.png b/static/img/ubuntu-04-select-file.png new file mode 100644 index 0000000..e40727f Binary files /dev/null and b/static/img/ubuntu-04-select-file.png differ diff --git a/static/img/ubuntu-05-profile-imported.png b/static/img/ubuntu-05-profile-imported.png new file mode 100644 index 0000000..4e6f722 Binary files /dev/null and b/static/img/ubuntu-05-profile-imported.png differ diff --git a/static/img/ubuntu-06-ipv4-settings.png b/static/img/ubuntu-06-ipv4-settings.png new file mode 100644 index 0000000..a7a9317 Binary files /dev/null and b/static/img/ubuntu-06-ipv4-settings.png differ diff --git a/static/img/ubuntu-07-disable-default-route.png b/static/img/ubuntu-07-disable-default-route.png new file mode 100644 index 0000000..8da2dab Binary files /dev/null and b/static/img/ubuntu-07-disable-default-route.png differ diff --git a/static/img/ubuntu-08-activate-connection.png b/static/img/ubuntu-08-activate-connection.png new file mode 100644 index 0000000..815533b Binary files /dev/null and b/static/img/ubuntu-08-activate-connection.png differ diff --git a/static/img/windows-01-download-openvpn.png b/static/img/windows-01-download-openvpn.png new file mode 100644 index 0000000..0016834 Binary files /dev/null and b/static/img/windows-01-download-openvpn.png differ diff --git a/static/img/windows-02-install-openvpn.png b/static/img/windows-02-install-openvpn.png new file mode 100644 index 0000000..a484dd4 Binary files /dev/null and b/static/img/windows-02-install-openvpn.png differ diff --git a/static/img/windows-03-move-config-file.png b/static/img/windows-03-move-config-file.png new file mode 100644 index 0000000..1246734 Binary files /dev/null and b/static/img/windows-03-move-config-file.png differ diff --git a/static/img/windows-04-connect.png b/static/img/windows-04-connect.png new file mode 100644 index 0000000..7920383 Binary files /dev/null and b/static/img/windows-04-connect.png differ diff --git a/static/img/windows-05-connected.png b/static/img/windows-05-connected.png new file mode 100644 index 0000000..28aa223 Binary files /dev/null and b/static/img/windows-05-connected.png differ diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..d7d7066 --- /dev/null +++ b/static/index.html @@ -0,0 +1,48 @@ + + + + + Pinecrypt Gateway + + + + + + + + + +
+
+
+

Loading certificate authority...

+
+
+ + + + diff --git a/static/js/certidude.js b/static/js/certidude.js new file mode 100644 index 0000000..027e395 --- /dev/null +++ b/static/js/certidude.js @@ -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('

Server under maintenance

'); + 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(); +}); diff --git a/static/js/shell.js b/static/js/shell.js new file mode 100644 index 0000000..0ed9f44 --- /dev/null +++ b/static/js/shell.js @@ -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 = [''].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(); +} diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/static/shell.html b/static/shell.html new file mode 100644 index 0000000..023ac24 --- /dev/null +++ b/static/shell.html @@ -0,0 +1,17 @@ + + + + + Certidude server + + + + + + + + +
+ + + diff --git a/templates/client/certidude.service b/templates/client/certidude.service new file mode 100644 index 0000000..acda204 --- /dev/null +++ b/templates/client/certidude.service @@ -0,0 +1,6 @@ +[Unit] +Description=Renew certificates and update revocation lists + +[Service] +Type=simple +ExecStart={{ sys.argv[0] }} enroll diff --git a/templates/client/certidude.timer b/templates/client/certidude.timer new file mode 100644 index 0000000..575b946 --- /dev/null +++ b/templates/client/certidude.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Run certidude enroll daily + +[Timer] +OnCalendar=daily +Persistent=true +Unit=certidude-enroll.service + +[Install] +WantedBy=timers.target + diff --git a/templates/client/openvpn-reconnect.service b/templates/client/openvpn-reconnect.service new file mode 100644 index 0000000..ce353f7 --- /dev/null +++ b/templates/client/openvpn-reconnect.service @@ -0,0 +1,8 @@ +[Unit] +Description=Restart OpenVPN after suspend + +[Service] +ExecStart=/usr/bin/pkill --signal SIGHUP --exact openvpn + +[Install] +WantedBy=sleep.target diff --git a/templates/openvpn-client.conf b/templates/openvpn-client.conf new file mode 100644 index 0000000..f615a7b --- /dev/null +++ b/templates/openvpn-client.conf @@ -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}} + + +{{key}} + + +{{cert}} + + +# Revocation list +# Tunnelblick doens't handle inlined CRL +# hard to update as well +; +; + +# Pre-shared key for extra layer of security +; +; + diff --git a/templates/snippets/certidude-client.sh b/templates/snippets/certidude-client.sh new file mode 100644 index 0000000..9ed0283 --- /dev/null +++ b/templates/snippets/certidude-client.sh @@ -0,0 +1,2 @@ +pip3 install --upgrade git+http://git.k-space.ee/pinecrypt/pinecrypt-client.git +certidude provision {{ authority.namespace }} diff --git a/templates/snippets/ios.mobileconfig b/templates/snippets/ios.mobileconfig new file mode 100644 index 0000000..f723ce4 --- /dev/null +++ b/templates/snippets/ios.mobileconfig @@ -0,0 +1,98 @@ + + + + + PayloadDisplayName + {{ gateway }} + PayloadDescription + IPSec IKEv2 VPN connection via {{ gateway }} + + PayloadIdentifier + {{ gateway }} + PayloadUUID + {{ service_uuid }} + PayloadType + Configuration + PayloadVersion + 1 + PayloadContent + + + PayloadIdentifier + {{ gateway }}.conf1 + PayloadUUID + {{ conf_uuid }} + PayloadType + com.apple.vpn.managed + PayloadVersion + 1 + UserDefinedName + {{ gateway }} + VPNType + IKEv2 + IKEv2 + + RemoteAddress + {{ gateway }} + RemoteIdentifier + {{ gateway }} + LocalIdentifier + {{ common_name }} + ServerCertificateIssuerCommonName + {{ authority.certificate.common_name }} + ServerCertificateCommonName + {{ gateway }} + AuthenticationMethod + Certificate + IKESecurityAssociationParameters + + EncryptionAlgorithm + AES-256 + IntegrityAlgorithm + SHA2-384 + DiffieHellmanGroup + 14 + + ChildSecurityAssociationParameters + + EncryptionAlgorithm + AES-128-GCM + IntegrityAlgorithm + SHA2-256 + DiffieHellmanGroup + 14 + + EnablePFS + 1 + PayloadCertificateUUID + {{ p12_uuid }} + + + + PayloadIdentifier + {{ common_name }} + PayloadUUID + {{ p12_uuid }} + PayloadType + com.apple.security.pkcs12 + PayloadVersion + 1 + PayloadContent + {{ p12 }} + + + PayloadIdentifier + {{ authority.certificate.common_name }} + PayloadUUID + {{ ca_uuid }} + PayloadType + com.apple.security.root + PayloadVersion + 1 + PayloadContent + {{ ca }} + + + + + diff --git a/templates/snippets/networkmanager-openvpn.conf b/templates/snippets/networkmanager-openvpn.conf new file mode 100644 index 0000000..74af9bb --- /dev/null +++ b/templates/snippets/networkmanager-openvpn.conf @@ -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 diff --git a/templates/snippets/networkmanager-strongswan.conf b/templates/snippets/networkmanager-strongswan.conf new file mode 100644 index 0000000..04355cf --- /dev/null +++ b/templates/snippets/networkmanager-strongswan.conf @@ -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 diff --git a/templates/snippets/nginx-https-site.conf b/templates/snippets/nginx-https-site.conf new file mode 100644 index 0000000..6bb298a --- /dev/null +++ b/templates/snippets/nginx-https-site.conf @@ -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; + } +} + diff --git a/templates/snippets/nginx-ocsp-cache.timer b/templates/snippets/nginx-ocsp-cache.timer new file mode 100644 index 0000000..82a60c0 --- /dev/null +++ b/templates/snippets/nginx-ocsp-cache.timer @@ -0,0 +1,3 @@ +[Timer] +OnCalendar=*:0/15 +Persistent=true diff --git a/templates/snippets/openvpn-client.conf b/templates/snippets/openvpn-client.conf new file mode 100644 index 0000000..d84b080 --- /dev/null +++ b/templates/snippets/openvpn-client.conf @@ -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 }} + +{% else %}ca /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem{% endif %} +{% if key %} + +{{ key }} + +{% else %}key /etc/certidude/authority/{{ authority.namespace }}/host_key.pem{% endif %} +{% if cert %} + +{{ cert }} + +{% else %}cert /etc/certidude/authority/{{ authority.namespace }}/host_cert.pem{% endif %} diff --git a/templates/snippets/openvpn-client.sh b/templates/snippets/openvpn-client.sh new file mode 100644 index 0000000..9d0ba35 --- /dev/null +++ b/templates/snippets/openvpn-client.sh @@ -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 + +#} diff --git a/templates/snippets/request-client.ps1 b/templates/snippets/request-client.ps1 new file mode 100644 index 0000000..bd50117 --- /dev/null +++ b/templates/snippets/request-client.ps1 @@ -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" + +#} + diff --git a/templates/snippets/request-client.sh b/templates/snippets/request-client.sh new file mode 100644 index 0000000..5c5b1ba --- /dev/null +++ b/templates/snippets/request-client.sh @@ -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' diff --git a/templates/snippets/request-common.sh b/templates/snippets/request-common.sh new file mode 100644 index 0000000..d2a3fc3 --- /dev/null +++ b/templates/snippets/request-common.sh @@ -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 diff --git a/templates/snippets/request-server.sh b/templates/snippets/request-server.sh new file mode 100644 index 0000000..2c62a92 --- /dev/null +++ b/templates/snippets/request-server.sh @@ -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" %} diff --git a/templates/snippets/setup-ocsp-caching.sh b/templates/snippets/setup-ocsp-caching.sh new file mode 100644 index 0000000..514f036 --- /dev/null +++ b/templates/snippets/setup-ocsp-caching.sh @@ -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 diff --git a/templates/snippets/store-authority.sh b/templates/snippets/store-authority.sh new file mode 100644 index 0000000..ec667e7 --- /dev/null +++ b/templates/snippets/store-authority.sh @@ -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 diff --git a/templates/snippets/strongswan-client.sh b/templates/snippets/strongswan-client.sh new file mode 100644 index 0000000..ba9236e --- /dev/null +++ b/templates/snippets/strongswan-client.sh @@ -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 diff --git a/templates/snippets/strongswan-patching.sh b/templates/snippets/strongswan-patching.sh new file mode 100644 index 0000000..a4dc016 --- /dev/null +++ b/templates/snippets/strongswan-patching.sh @@ -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 diff --git a/templates/snippets/submit-request-wait.sh b/templates/snippets/submit-request-wait.sh new file mode 100644 index 0000000..8e84c35 --- /dev/null +++ b/templates/snippets/submit-request-wait.sh @@ -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' diff --git a/templates/snippets/update-trust.ps1 b/templates/snippets/update-trust.ps1 new file mode 100644 index 0000000..0dbc125 --- /dev/null +++ b/templates/snippets/update-trust.ps1 @@ -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 diff --git a/templates/snippets/update-trust.sh b/templates/snippets/update-trust.sh new file mode 100644 index 0000000..4911640 --- /dev/null +++ b/templates/snippets/update-trust.sh @@ -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 diff --git a/templates/snippets/windows.ps1 b/templates/snippets/windows.ps1 new file mode 100644 index 0000000..fa3f5c5 --- /dev/null +++ b/templates/snippets/windows.ps1 @@ -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 +#} diff --git a/templates/views/attributes.html b/templates/views/attributes.html new file mode 100644 index 0000000..deb7fe4 --- /dev/null +++ b/templates/views/attributes.html @@ -0,0 +1,3 @@ +{% for key, value in certificate.attributes %} +{{ value }} +{% endfor %} diff --git a/templates/views/authority.html b/templates/views/authority.html new file mode 100644 index 0000000..8858d16 --- /dev/null +++ b/templates/views/authority.html @@ -0,0 +1,270 @@ + + + + +
+
+

Signed certificates

+ +

Authority administration + {% if authority.certificate.organization %}of {{ authority.certificate.organization }}{% endif %} + allowed for + {% for user in session.authorization.admin_users %}{{ user.given_name }} {{user.surname }}{% 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 + + until + . + Authority certificate can be downloaded from here. + Following certificates have been signed:

+ +
+ + + + +
+ +
+ {% for certificate in session.signed | sort(attribute="signed", reverse=true) %} + {% include "views/signed.html" %} + {% endfor %} +
+ +

Showing - of total - certificates

+
+
+ {% if session.features.token %} +

Tokens

+

Tokens allow enrolling smartphones and third party devices.

+
    +
  • You can issue yourself a token to be used on a mobile device
  • +
  • Enter username to issue a token to issue a token for another user
  • +
  • Enter e-mail address to issue a token to guest users outside domain
  • +
+

+

+ + + + + +
+

+ +

Issued tokens:

+
    + {% for token in session.tokens %} + {% include "views/token.html" %} + {% endfor %} +
+ +
+ {% endif %} + + {% if session.authorization.request_subnets %} +

 

+

Pending requests

+ +

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 here 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 %} + +

+
+ {% for request in session.requests | sort(attribute="submitted", reverse=true) %} + {% include "views/request.html" %} + {% endfor %} +
+ {% endif %} + + {% if session.builder.profiles %} +

LEDE imagebuilder

+

Hit a link to generate machine specific image. Note that this might take couple minutes to finish.

+
    + {% for name, title, filename in session.builder.profiles %} +
  • {{ title }}
  • + {% endfor %} +
+ {% endif %} + +
+
+ +

Revoked certificates

+

Following certificates have been revoked{% if session.features.crl %}, for more information click + here{% endif %}.

+ + {% for certificate in session.revoked | sort(attribute="revoked", reverse=true) %} + {% include "views/revoked.html" %} + {% endfor %} +
+
+
+
+

Loading logs, this might take a while...

+
+ +
+
diff --git a/templates/views/configuration.html b/templates/views/configuration.html new file mode 100644 index 0000000..9ea26af --- /dev/null +++ b/templates/views/configuration.html @@ -0,0 +1,31 @@ + +

Create a rule

+

+ + + + + + Filter + + attaches attribute + + something + +

+ +{% for grouper, items in configuration | groupby('tag_id') %} + +

Filter {{ items[0].match_key }} is {{ items[0].match_value }}

+ + +{% endfor %} + + diff --git a/templates/views/enroll.html b/templates/views/enroll.html new file mode 100644 index 0000000..848146d --- /dev/null +++ b/templates/views/enroll.html @@ -0,0 +1,278 @@ + + + + +
+
+
+

Generating RSA keypair, this will take a while...

+
+ + + +
+
+
+

Ubuntu 16.04+

+

Install OpenVPN plugin for NetworkManager by executing following two command in the terminal: + +

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

+ Fetch OpenVPN profile + +

+ +
+

Open up network connections:

+

+

Hit Add button:

+

+

Select Import a saved VPN configuration...:

+

+

Select downloaded file:

+

+

Once profile is successfully imported following dialog appears:

+

+

By default all traffic is routed via VPN gateway, route only intranet subnets to the gateway select Routes... under IPv4 Settings:

+

+

Check Use this connection only for resources on its network:

+

+

To activate the connection select it under VPN Connections:

+

+
+
+
+
+ +
+
+
+

Ubuntu 18.04+ (advanced)

+

Copy-paste follownig to terminal as root user:

+
{% include "snippets/request-client.sh" %}
+cat << EOF > '/etc/NetworkManager/system-connections/OpenVPN to {{ authority.namespace }}'
+{% include "snippets/networkmanager-openvpn.conf" %}EOF
+
+nmcli con reload
+
+
+
+
+ +
+
+
+

Ubuntu 18.04+ (advanced)

+

Copy-paste follownig to terminal as root user:

+
{% include "snippets/request-client.sh" %}
+cat << EOF > '/etc/NetworkManager/system-connections/IPSec to {{ authority.namespace }}'
+{% include "snippets/networkmanager-strongswan.conf" %}EOF
+
+nmcli con reload
+
+
+
+
+ + +
+
+
+

Fedora

+

Install OpenVPN plugin for NetworkManager by running following two commands:

+
dnf install NetworkManager-openvpn-gnome
+systemctl restart NetworkManager
+ Right click in the NetworkManager icon, select network settings. Hit the + button and select Import from file..., select the downloaded .ovpn file. + Remove the .ovpn file from the Downloads folder.

+ Fetch OpenVPN profile +
+
+
+ +
+
+
+

Windows

+

+ Import PKCS#12 container to your machine trust store. + Import VPN connection profile by moving the downloaded .pbk file to +

%userprofile%\AppData\Roaming\Microsoft\Network\Connections\PBK
+ or +
C:\ProgramData\Microsoft\Network\Connections\Pbk

+ Fetch PKCS#12 container + Fetch IPSec IKEv2 VPN profile +
+
+
+ +
+
+
+

Windows

+

To configure IPSec IKEv2 tunnel on Windows, open PowerShell as administrator and copy-paste following:

+
{% include "snippets/windows.ps1" %}
+
+
+
+ +
+
+
+

Windows

+

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

+ Get OpenVPN community edition + Fetch OpenVPN profile + + +
+

Download OpenVPN from the link supplied above:

+

+ +

Install OpenVPN:

+

+ +

Move the configuraiton file downloaded from the second button above:

+

+ +

Connect from system tray:

+

+ +

Connection is successfully configured:

+

+
+
+
+
+ +
+
+
+

Mac OS X

+

Download Tunnelblick. Tap on the button above and import the profile.

+ Get Tunnelblick + Fetch OpenVPN profile +
+
+
+ +
+
+
+

iPhone/iPad

+

Install OpenVPN Connect app, tap on the button below.

+ Get OpenVPN Connect app + Fetch OpenVPN profile +
+
+
+ +
+
+
+

iPhone/iPad

+

+ Tap the button below, you'll be prompted about configuration profile, tap Allow. + Hit Install in the top-right corner. + Enter your passcode to unlock trust store. + Tap Install and confirm by hitting Install. + Where password for the certificate is prompted, enter 1234. + Hit Done. Go to Settings, open VPN submenu and tap on the VPN profile to connect. +

+ Fetch IPSec IKEv2 VPN profile +
+
+
+ +
+
+
+

Mac OS X

+

+ Click on the button below, you'll be prompted about configuration profile, tap Allow. + Hit Install in the top-right corner. + Enter your passcode to unlock trust store. + Tap Install and confirm by hitting Install. + Where password for the certificate is prompted, enter 1234. + Hit Done. Go to Settings, open VPN submenu and tap on the VPN profile to connect. +

+ Fetch VPN profile +
+
+
+ +
+
+
+

Android

+

Intall OpenVPN Connect app on your device. + Tap on the downloaded .ovpn file, OpenVPN Connect should prompt for import. + Hit Accept and then Connect. + Remember to delete any remaining .ovpn files under the Downloads. +

+ Get OpenVPN Connect app + Fetch OpenVPN profile +
+
+
+ +
+
+
+

Android

+

+ Install strongSwan Client app on your device. + Tap on the downloaded .sswan file, StrongSwan Client should prompt for import. + Hit Import certificate from VPN profile and then Import in the top-right corner. + Remember to delete any remaining .sswan files under the Downloads. +

+ Get strongSwan VPN Client app + Fetch StrongSwan profile +
+
+
+ + +
diff --git a/templates/views/error.html b/templates/views/error.html new file mode 100644 index 0000000..3cff50a --- /dev/null +++ b/templates/views/error.html @@ -0,0 +1,2 @@ +

{{ message.title }}

+

{{ message.description }}

diff --git a/templates/views/insecure.html b/templates/views/insecure.html new file mode 100644 index 0000000..710df38 --- /dev/null +++ b/templates/views/insecure.html @@ -0,0 +1,14 @@ +

You're viewing this page over insecure channel. + You can give it a try and connect over HTTPS, + if that succeeds all subsequents accesses of this page will go over HTTPS. +

+

+ Click here to fetch the certificate of this authority. + Alternatively install certificate on Fedora or Ubuntu with following copy-pastable snippet: +

+ +
+
{% include "snippets/store-authority.sh" %}
+{% include "snippets/update-trust.sh" %}
+
+ diff --git a/templates/views/lease.html b/templates/views/lease.html new file mode 100644 index 0000000..cf53eea --- /dev/null +++ b/templates/views/lease.html @@ -0,0 +1,7 @@ +Last seen + +at +{{ certificate.lease.inner_address }}{% if certificate.lease.outer_address %} +from +{{ certificate.lease.outer_address }}{% endif %}. +See some stats here. diff --git a/templates/views/logentry.html b/templates/views/logentry.html new file mode 100644 index 0000000..731dd59 --- /dev/null +++ b/templates/views/logentry.html @@ -0,0 +1,8 @@ +
  • + + +{{ entry.message }} + +{{ entry.created }} +
  • + diff --git a/templates/views/request.html b/templates/views/request.html new file mode 100644 index 0000000..94f6e01 --- /dev/null +++ b/templates/views/request.html @@ -0,0 +1,64 @@ +
    +
    + {% if certificate.server %} + + {% else %} + + {% endif %} + {{ request.common_name }} +
    +
    +

    + Submitted + + from + {% if request.hostname %}{{request.hostname}} ({{request.address}}){% else %}{{request.address}}{% endif %} +

    +
    + + + + + +
    +
    +

    Use following to fetch the signing request:

    +
    +
    wget http://{{ authority.namespace }}/api/request/{{ request.common_name }}/
    +curl -L http://{{ authority.namespace }}/api/request/{{ request.common_name }}/ \
    +  | openssl req -text -noout
    +
    + +
    + + + + + + + + +
    Common name{{ request.common_name }}
    Submitted{{ request.submitted | datetime }} + {% if request.address %}from {{ request.address }} + {% if request.hostname %} ({{ request.hostname }}){% endif %}{% endif %}
    MD5{{ request.md5sum }}
    SHA1{{ request.sha1sum }}
    SHA256{{ request.sha256sum }}
    +
    + +
    +
    +
    diff --git a/templates/views/revoked.html b/templates/views/revoked.html new file mode 100644 index 0000000..81cbef3 --- /dev/null +++ b/templates/views/revoked.html @@ -0,0 +1,69 @@ +
    +
    +
    + {% if certificate.server %} + + {% else %} + + {% endif %} + {{ certificate.common_name }} +
    +
    +

    + Serial number {{ certificate.serial | serial }}. +

    +

    + Revoked + . + Valid from {{ certificate.signed | datetime }} to {{ certificate.expired | datetime }}. +

    + +
    + + +
    +
    +

    To fetch certificate:

    + +
    +
    wget http://{{ authority.namespace }}/api/revoked/{{ certificate.serial }}/
    +curl http://{{ authority.namespace }}/api/revoked/{{ certificate.serial }}/ \
    +  | openssl x509 -text -noout
    +
    + +

    To perform online certificate status request

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

    + + + + + + + + {% if certificate.lease %} + + {% endif %} + + + + +
    Common name{{ certificate.common_name }}
    Organizational unit{{ certificate.organizational_unit }}
    Serial number{{ certificate.serial }}
    Signed{{ certificate.signed | datetime }} + {% if certificate.signer %}, by {{ certificate.signer }}{% endif %}
    Expired{{ certificate.expired | datetime }}
    Lease{{ certificate.lease.inner_address }} at {{ certificate.lease.last_seen | datetime }} + from {{ certificate.lease.outer_address }} +
    SHA256{{ certificate.sha256sum }}
    +

    +
    +
    +
    +
    diff --git a/templates/views/signed.html b/templates/views/signed.html new file mode 100644 index 0000000..8fd5776 --- /dev/null +++ b/templates/views/signed.html @@ -0,0 +1,134 @@ +
    +
    + {% if certificate.organizational_unit %} + + {{ certificate.organizational_unit }} / + {% endif %} + {% if certificate.extensions.extended_key_usage and "server_auth" in certificate.extensions.extended_key_usage %} + + {% else %} + + {% endif %} + {{ certificate.common_name }} +
    +
    +

    + + + {% if certificate.lease %} + {% include "views/lease.html" %} + {% endif %} + + + Signed + {% if certificate.signer %} by {{ certificate.signer }}{% endif %}, + expires + . +

    +

    + {% if session.tagging %} + + {% include "views/tags.html" %} + + {% endif %} + + {% include "views/attributes.html" %} + +

    + + + +
    + {% if session.tagging %} + + + + {% endif %} +
    + +
    +

    To launch shell for this device, click here +

    To fetch certificate:

    + +
    +
    wget http://{{ authority.namespace }}/api/signed/{{ certificate.common_name }}
    +curl -L http://{{ authority.namespace }}/api/signed/{{ certificate.common_name }}/ \
    +  | openssl x509 -text -noout
    +
    + + {% if session.authorization.ocsp_subnets %} + {% if certificate.responder_url %} +

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

    +
    curl http://{{ authority.namespace }}/api/certificate > session.pem
    +openssl ocsp -issuer session.pem -CAfile session.pem \
    +  -url {{ certificate.responder_url }} \
    +  -serial 0x{{ certificate.serial }}
    + {% else %} +

    Querying OCSP responder disabled for this certificate, see /etc/certidude/profile.conf how to enable if that's desired

    + {% endif %} + {% endif %} + +

    To fetch script:

    +
    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
    + +
    + + + + + + + + {% if certificate.lease %} + + {% endif %} + + + + {% if certificate.key_usage %} + + {% endif %} + {% if certificate.extended_key_usage %} + + {% endif %} + +
    Common name{{ certificate.common_name }}
    Organizational unit{% if certificate.organizational_unit %}{{ certificate.organizational_unit }}{% else %}-{% endif %}
    Serial number{{ certificate.serial | serial }}
    Signed{{ certificate.signed | datetime }}{% if certificate.signer %} by {{ certificate.signer }}{% endif %}
    Expires{{ certificate.expires | datetime }}
    Lease{{ certificate.lease.inner_address }} at {{ certificate.lease.last_seen | datetime }} + from {{ certificate.lease.outer_address }} +
    SHA256{{ certificate.sha256sum }}
    Key usage{{ certificate.key_usage | join(", ") | replace("_", " ") }}
    Extended key usage{{ certificate.extended_key_usage | join(", ") | replace("_", " ") }}
    +
    +
    +
    +
    diff --git a/templates/views/tags.html b/templates/views/tags.html new file mode 100644 index 0000000..de5ca01 --- /dev/null +++ b/templates/views/tags.html @@ -0,0 +1,6 @@ +{% for tag in certificate.tags %} + {{ tag }} +{% endfor %} diff --git a/templates/views/token.html b/templates/views/token.html new file mode 100644 index 0000000..c007076 --- /dev/null +++ b/templates/views/token.html @@ -0,0 +1,10 @@ +
  • + + + {{ token.uuid }}... + {{ token.subject }} + {% if token.issuer %}{% if token.issuer != token.subject %}by {{ token.issuer }}{% else %}by himself{% endif %}{% else %}via shell{% endif %}, + expires + + +