Initial commit
							
								
								
									
										22
									
								
								.htmlhintrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,22 @@ | ||||
| { | ||||
|     "attr-lowercase": true, | ||||
|     "attr-no-duplication": true, | ||||
|     "attr-unsafe-chars": true, | ||||
|     "attr-value-double-quotes": true, | ||||
|     "attr-value-not-empty": true, | ||||
|     "doctype-first": false, | ||||
|     "head-script-disabled": true, | ||||
|     "href-abs-or-rel": true, | ||||
|     "id-class-ad-disabled": true, | ||||
|     "id-class-value": true, | ||||
|     "id-unique": true, | ||||
|     "img-alt-require": true, | ||||
|     "space-tab-mixed-disabled": true, | ||||
|     "spec-char-escape": true, | ||||
|     "src-not-empty": true, | ||||
|     "style-disabled": true, | ||||
|     "tag-pair": true, | ||||
|     "tag-self-close": true, | ||||
|     "tagname-lowercase": true, | ||||
|     "title-require": true | ||||
| } | ||||
							
								
								
									
										73
									
								
								.htmllintrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,73 @@ | ||||
| { | ||||
|     "plugins": [],  // npm modules to load | ||||
|  | ||||
|     "maxerr": false, | ||||
|     "raw-ignore-regex": false, | ||||
|     "attr-bans": [ | ||||
|         "align", | ||||
|         "background", | ||||
|         "bgcolor", | ||||
|         "border", | ||||
|         "frameborder", | ||||
|         "longdesc", | ||||
|         "marginwidth", | ||||
|         "marginheight", | ||||
|         "scrolling", | ||||
|         "style", | ||||
|         "width" | ||||
|     ], | ||||
|     "indent-delta": false, | ||||
|     "indent-style": "nonmixed", | ||||
|     "indent-width": 2, | ||||
|     "indent-width-cont": false, | ||||
|     "spec-char-escape": true, | ||||
|     "text-ignore-regex": false, | ||||
|     "tag-bans": [ | ||||
|         "style", | ||||
|         "b" | ||||
|     ], | ||||
|     "tag-close": true, | ||||
|     "tag-name-lowercase": true, | ||||
|     "tag-name-match": true, | ||||
|     "tag-self-close": false, | ||||
|     "doctype-first": false, | ||||
|     "doctype-html5": false, | ||||
|     "attr-name-style": "dash", | ||||
|     "attr-name-ignore-regex": false, | ||||
|     "attr-no-dup": true, | ||||
|     "attr-no-unsafe-char": true, | ||||
|     "attr-order": false, | ||||
|     "attr-quote-style": "double", | ||||
|     "attr-req-value": true, | ||||
|     "attr-new-line": false, | ||||
|     "attr-validate": true, | ||||
|     "id-no-dup": true, | ||||
|     "id-class-style": false, | ||||
|     "id-class-no-ad": true, | ||||
|     "class-style": false, | ||||
|     "class-no-dup": false, | ||||
|     "id-class-ignore-regex": false, | ||||
|     "img-req-alt": true, | ||||
|     "img-req-src": true, | ||||
|     "html-valid-content-model": true, | ||||
|     "head-valid-content-model": true, | ||||
|     "href-style": false, | ||||
|     "link-req-noopener": true, | ||||
|     "label-req-for": true, | ||||
|     "line-end-style": "lf", | ||||
|     "line-no-trailing-whitespace": true, | ||||
|     "line-max-len": false, | ||||
|     "line-max-len-ignore-regex": false, | ||||
|     "head-req-title": true, | ||||
|     "title-no-dup": true, | ||||
|     "title-max-len": 60, | ||||
|     "html-req-lang": false, | ||||
|     "lang-style": "case", | ||||
|     "fig-req-figcaption": false, | ||||
|     "focusable-tabindex-style": false, | ||||
|     "input-radio-req-name": true, | ||||
|     "input-req-label": false, | ||||
|     "table-req-caption": false, | ||||
|     "table-req-header": false, | ||||
|     "tag-req-attr": false | ||||
| } | ||||
							
								
								
									
										13
									
								
								.pre-commit-config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | ||||
| repos: | ||||
| -   repo: https://github.com/jorisroovers/gitlint | ||||
|     rev: v0.15.1 | ||||
|     hooks: | ||||
|     -   id: gitlint | ||||
|  | ||||
| -   repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs | ||||
|     rev: v1.1.1 | ||||
|     hooks: | ||||
|     -   id: htmlhint | ||||
|         args: [--config, .htmlhintrc] | ||||
|     -   id: htmllint | ||||
|     -   id: dockerfile_lint | ||||
							
								
								
									
										15
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | ||||
| FROM alpine as build | ||||
| MAINTAINER lauri <lauri@pinecrypt.com> | ||||
| RUN apk add --update npm nginx rsync bash | ||||
| RUN npm install --silent --no-optional -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg xterm | ||||
| COPY nginx.conf /etc/nginx/nginx.conf | ||||
| EXPOSE 80 443 8443 | ||||
| WORKDIR /var/lib/nginx/html/ | ||||
| RUN rsync -avq /usr/lib/node_modules/font-awesome/fonts/ fonts/ | ||||
| COPY static ./ | ||||
| COPY templates templates | ||||
| RUN nunjucks-precompile --include snippets --include views templates >> js/bundle.js | ||||
| RUN bash -c 'cat /usr/lib/node_modules/{jquery/dist/jquery.min.js,tether/dist/js/tether.min.js,bootstrap/dist/js/bootstrap.min.js,node-forge/dist/forge.all.min.js,qrcode-svg/dist/qrcode.min.js,timeago/jquery.timeago.js,nunjucks/browser/nunjucks-slim.min.js,xterm/lib/xterm.js} >> js/bundle.js' | ||||
| RUN bash -c 'cat /usr/lib/node_modules/{tether/dist/css/tether.min.css,bootstrap/dist/css/bootstrap.min.css,font-awesome/css/font-awesome.min.css,xterm/css/xterm.css} >> css/bundle.css' | ||||
| COPY entrypoint.sh /entrypoint.sh | ||||
| ENTRYPOINT /entrypoint.sh | ||||
							
								
								
									
										5
									
								
								entrypoint.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | ||||
| #!/bin/sh | ||||
| while [ ! -f /var/lib/certidude/server-secrets/self_cert.pem ]; do | ||||
|     sleep 1 | ||||
| done | ||||
| exec nginx -g "daemon off; error_log /dev/stdout info;" | ||||
							
								
								
									
										156
									
								
								nginx.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,156 @@ | ||||
| user nginx; | ||||
| worker_processes auto; | ||||
| pid /run/nginx.pid; | ||||
| include /etc/nginx/modules-enabled/*.conf; | ||||
|  | ||||
| events { | ||||
|     worker_connections 768; | ||||
| } | ||||
|  | ||||
| http { | ||||
|     upstream read-write { | ||||
|         server 127.0.0.1:4001; | ||||
|     } | ||||
|  | ||||
|     upstream ocsp-responder { | ||||
|         server 127.0.0.1:5001; | ||||
|     } | ||||
|  | ||||
|     upstream builder { | ||||
|         server 127.0.0.1:7001; | ||||
|     } | ||||
|      | ||||
|     upstream event { | ||||
|         server 127.0.0.1:8001; | ||||
|     } | ||||
|  | ||||
|     resolver 127.0.0.11; | ||||
|     sendfile on; | ||||
|     tcp_nopush on; | ||||
|     tcp_nodelay on; | ||||
|     keepalive_timeout 65; | ||||
|     types_hash_max_size 2048; | ||||
|  | ||||
|     include /etc/nginx/mime.types; | ||||
|     default_type application/octet-stream; | ||||
|  | ||||
|     ## | ||||
|     # SSL Settings | ||||
|     ## | ||||
|     ssl_protocols TLSv1.2 TLSv1.3; | ||||
|     ssl_prefer_server_ciphers on; | ||||
|  | ||||
|     ## | ||||
|     # Gzip Settings | ||||
|     ## | ||||
|     gzip on; | ||||
|  | ||||
|     # Basic DoS prevention measures | ||||
|     limit_conn addr 100; | ||||
|     client_body_timeout 5s; | ||||
|     client_header_timeout 5s; | ||||
|     limit_conn_zone $binary_remote_addr zone=addr:10m; | ||||
|  | ||||
|     # Backend configuration | ||||
|     proxy_set_header Host $host; | ||||
|     proxy_set_header X-Real-IP $remote_addr; | ||||
|     proxy_set_header X-SSL-CERT $ssl_client_cert; | ||||
|     proxy_set_header X-Forwarded-Proto $scheme; | ||||
|     proxy_connect_timeout 600; | ||||
|     proxy_send_timeout 600; | ||||
|     proxy_read_timeout 600; | ||||
|     send_timeout 600; | ||||
|  | ||||
|     # To use CA-s own certificate for frontend and mutually authenticated connections | ||||
|     ssl_certificate /var/lib/certidude/server-secrets/self_cert.pem; | ||||
|     ssl_certificate_key /var/lib/certidude/server-secrets/self_key.pem; | ||||
|  | ||||
|     server { | ||||
|         # Section for serving insecure HTTP, note that this is suitable for | ||||
|         # OCSP, CRL-s etc which is already covered by PKI protection mechanisms. | ||||
|  | ||||
|         listen 80 default_server; | ||||
|  | ||||
|         # Proxy pass OCSP responder | ||||
|         location /api/ocsp/ { | ||||
|             proxy_pass http://ocsp-responder; | ||||
|         } | ||||
|  | ||||
|         # Event server | ||||
|         location /api/event/ { | ||||
|             proxy_buffering off; | ||||
|             proxy_cache off; | ||||
|             proxy_pass http://event; | ||||
|         } | ||||
|  | ||||
|         # Proxy pass to backend | ||||
|         location /api/ { | ||||
|             proxy_pass http://read-write; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     server { | ||||
|         # Section for accessing web interface over HTTPS | ||||
|         listen 127.0.0.1:1443 ssl http2 default_server; | ||||
|  | ||||
|         # HSTS header below should make sure web interface will be accessed over HTTPS only | ||||
|         # once it has been configured | ||||
|         add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"; | ||||
|  | ||||
|         #proxy pass event | ||||
|         location /api/event/ { | ||||
|             proxy_buffering off; | ||||
|             proxy_cache off; | ||||
|             proxy_pass http://event; | ||||
|         } | ||||
|  | ||||
|         #Proxy pass longpoll | ||||
|         location /api/longpoll/ { | ||||
|             proxy_buffering off; | ||||
|             proxy_cache off; | ||||
|             proxy_pass http://event; | ||||
|         } | ||||
|  | ||||
|         # OpenWrt image builder | ||||
|         location /api/build/ { | ||||
|             proxy_pass http://builder; | ||||
|         } | ||||
|  | ||||
|         # Proxy pass to backend | ||||
|         location /api/ { | ||||
|             proxy_pass http://read-write; | ||||
|         } | ||||
|  | ||||
|         # This is for Let's Encrypt enroll/renewal | ||||
|         location /.well-known/ { | ||||
|             alias /var/www/html/.well-known/; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     server { | ||||
|         # Section for certificate authenticated HTTPS clients, | ||||
|         # for submitting information to CA eg. leases, | ||||
|         # for delivering scripts to clients, | ||||
|         # for exchanging messages over WebSockets | ||||
|         server_name $hostname; | ||||
|         listen 8443 ssl http2; | ||||
|  | ||||
|         # Enforce OCSP stapling for the server certificate | ||||
|         # Note that even nginx 1.14.0 doesn't immideately populate the OCSP cache | ||||
|         # You need to run separate cronjob to populate the OCSP response cache | ||||
|         ssl_stapling on; | ||||
|         ssl_stapling_verify on; | ||||
|  | ||||
|         # Allow client authentication with certificate, | ||||
|         # backend must still check if certificate was used for TLS handshake | ||||
|         ssl_verify_client optional; | ||||
|         ssl_client_certificate /var/lib/certidude/server-secrets/ca_cert.pem; | ||||
|  | ||||
|         # Proxy pass to backend | ||||
|         location /api/ { | ||||
|             proxy_pass http://read-write; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										4
									
								
								static/502.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| { | ||||
|   "title": "502 Bad Gateway", | ||||
|   "description": "It seems the server had bit of a hiccup, perhaps this helps: systemctl restart certidude-backend && journalctl -f" | ||||
| } | ||||
							
								
								
									
										104
									
								
								static/css/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,104 @@ | ||||
|  | ||||
| @keyframes fresh { | ||||
|     from { background-color: #ffc107; } | ||||
|     to { background-color: white; } | ||||
| } | ||||
|  | ||||
| .fresh { | ||||
|     animation-name: fresh; | ||||
|     animation-duration: 30s; | ||||
| } | ||||
|  | ||||
| .loader-container { | ||||
|     margin: 20% auto 0 auto; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| .loader { | ||||
|     border: 16px solid #f3f3f3; /* Light grey */ | ||||
|     border-top: 16px solid #3498db; /* Blue */ | ||||
|     border-radius: 50%; | ||||
|     width: 120px; | ||||
|     height: 120px; | ||||
|     animation: spin 2s linear infinite; | ||||
|     display: inline-block; | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
|   font-family: 'PT Sans Narrow'; | ||||
|   font-style: normal; | ||||
|   font-weight: 400; | ||||
|   src: local('PT Sans Narrow'), local('PTSans-Narrow'), url('../fonts/pt-sans.woff2') format('woff2'); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
|   font-family: 'Ubuntu Mono'; | ||||
|   font-style: normal; | ||||
|   font-weight: 400; | ||||
|   src: local('Ubuntu Mono'), local('UbuntuMono-Regular'), url('../fonts/ubuntu-mono.woff2') format('woff2'); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
|   font-family: 'Gentium Basic'; | ||||
|   font-style: normal; | ||||
|   font-weight: 400; | ||||
|   src: local('Gentium Basic'), local('GentiumBasic'), url('../fonts/gentium-basic.woff2') format('woff2'); | ||||
| } | ||||
|  | ||||
| @keyframes spin { | ||||
|     0% { transform: rotate(0deg); } | ||||
|     100% { transform: rotate(360deg); } | ||||
| } | ||||
|  | ||||
| body, input { | ||||
| } | ||||
|  | ||||
| body, input { | ||||
| } | ||||
|  | ||||
| pre { | ||||
|     font-family: 'Ubuntu Mono'; | ||||
|     background: #333; | ||||
|     overflow: auto; | ||||
|     border: 1px solid #292929; | ||||
|     border-radius: 4px; | ||||
|     color: #ddd; | ||||
|     padding: 6px 10px; | ||||
| } | ||||
|  | ||||
| pre code a { | ||||
|     color: #eef; | ||||
| } | ||||
|  | ||||
| h1, h2 { | ||||
| } | ||||
|  | ||||
|  | ||||
| #view { | ||||
|     margin: 5em auto 5em auto; | ||||
|  | ||||
| } | ||||
|  | ||||
| footer div { | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| svg { | ||||
|     position: relative; | ||||
| } | ||||
|  | ||||
| .badge { | ||||
|     cursor: pointer; | ||||
| } | ||||
|  | ||||
| .disabled { | ||||
|     pointer-events: none; | ||||
|     opacity: 0.4; | ||||
|     cursor: not-allowed; | ||||
| } | ||||
|  | ||||
| #signed_certificates .filterable .fa-circle { color: #888888; } | ||||
| #signed_certificates .filterable[data-state='online'] .fa-circle { color: #5cb85c; } | ||||
| #signed_certificates .filterable[data-state='offline'] .fa-circle { color: #0275d8; } | ||||
| #signed_certificates .filterable[data-state='dead'] .fa-circle { color: #d9534f; } | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								static/fonts/gentium-basic.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/pt-sans.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/ubuntu-mono.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/img/ubuntu-01-edit-connections.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 393 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/img/ubuntu-02-network-connections.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 368 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/img/ubuntu-03-import-saved-config.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 328 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/img/ubuntu-04-select-file.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 139 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/img/ubuntu-05-profile-imported.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 338 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/img/ubuntu-06-ipv4-settings.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 318 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/img/ubuntu-07-disable-default-route.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 342 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/img/ubuntu-08-activate-connection.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 394 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/img/windows-01-download-openvpn.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 132 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/img/windows-02-install-openvpn.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 502 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/img/windows-03-move-config-file.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 137 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/img/windows-04-connect.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 583 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/img/windows-05-connected.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 638 KiB | 
							
								
								
									
										48
									
								
								static/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,48 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="utf-8"/> | ||||
|     <title>Pinecrypt Gateway</title> | ||||
|     <link href="/css/bundle.css" rel="stylesheet" type="text/css"/> | ||||
|     <link href="/css/style.css" rel="stylesheet" type="text/css"/> | ||||
|     <script type="text/javascript" src="/js/bundle.js"></script> | ||||
|     <script type="text/javascript" src="/js/certidude.js"></script> | ||||
|     <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"> | ||||
|   </head> | ||||
|   <body onhashchange="onHashChanged();"> | ||||
|     <nav class="navbar navbar-toggleable-md navbar-inverse bg-inverse fixed-top"> | ||||
|       <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation"> | ||||
|         <span class="navbar-toggler-icon"></span> | ||||
|       </button> | ||||
|       <a class="navbar-brand" href="#columns=2">Pinecrypt Gateway</a> | ||||
|  | ||||
|       <div class="collapse navbar-collapse" id="navbarsExampleDefault"> | ||||
|         <ul class="navbar-nav mr-auto"> | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link disabled dashboard" href="#">Dashboard</a> | ||||
|           </li> | ||||
|           <li class="nav-item hidden-xl-up"> | ||||
|             <a class="nav-link" href="#">Log</a> | ||||
|           </li> | ||||
|         </ul> | ||||
|         <div class="form-inline my-2 my-lg-0"> | ||||
|           <input id="search" class="form-control mr-sm-2" style="display:none;" type="search" placeholder="🔍"> | ||||
|         </div> | ||||
|       </div> | ||||
|     </nav> | ||||
|     <div id="view-dashboard" class="container-fluid" style="margin: 5em 0 0 0;"> | ||||
|         <div class="loader-container"> | ||||
|           <div class="loader"></div> | ||||
|           <p>Loading certificate authority...</p> | ||||
|         </div> | ||||
|     </div> | ||||
|     <footer class="footer"> | ||||
|       <div class="container"> | ||||
|         Pinecrypt Gateway by | ||||
|         <a href="https://pinecrypt.com">Pinecrypt Labs</a> | ||||
|       </div> | ||||
|     </footer> | ||||
|   </body> | ||||
| </html> | ||||
|  | ||||
							
								
								
									
										795
									
								
								static/js/certidude.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,795 @@ | ||||
|  | ||||
| 'use strict'; | ||||
|  | ||||
| const KEY_SIZE = 2048; | ||||
| const DEVICE_KEYWORDS = ["Android", "iPhone", "iPad", "Windows", "Ubuntu", "Fedora", "Mac", "Linux"]; | ||||
|  | ||||
| jQuery.timeago.settings.allowFuture = true; | ||||
|  | ||||
| function onLaunchShell(common_name) { | ||||
|     $.post({ | ||||
|       url: "/api/signed/" + common_name + "/shell/", | ||||
|       error: function() { | ||||
|         alert("Failed to launch shell for: " + common_name); | ||||
|       }, | ||||
|       success: function(blob) { | ||||
|         console.info(blob); | ||||
|         if (blob.action == "start-session") { | ||||
|           console.info("Returned shell blob:", blob); | ||||
|           var win = window.open("/shell.html#" + blob.source + "/" + blob.target, '_blank'); | ||||
|           if (win) { | ||||
|             win.focus(); | ||||
|           } else { | ||||
|             alert("Please disable popup blocker for this site!"); | ||||
|           } | ||||
| 	} else { | ||||
|           alert("Failed to start session"); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|     return false; | ||||
| } | ||||
|  | ||||
| function onRejectRequest(e, common_name, sha256sum) { | ||||
|   $(this).button('loading'); | ||||
|   $.ajax({ | ||||
|     url: "/api/request/" + common_name + "/?sha256sum=" + sha256sum, | ||||
|     type: "delete" | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function onSignRequest(e, common_name, sha256sum) { | ||||
|   e.preventDefault(); | ||||
|   $(e.target).button('loading'); | ||||
|   $.ajax({ | ||||
|     url: "/api/request/" + common_name + "/?sha256sum=" + sha256sum, | ||||
|     type: "post" | ||||
|   }); | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| function normalizeCommonName(j) { | ||||
|     return j.replace("@", "--").split(".").join("-"); // dafuq ?! | ||||
| } | ||||
|  | ||||
| function onShowAll() { | ||||
|   var options = document.querySelectorAll(".option"); | ||||
|   for (i = 0; i < options.length; i++) { | ||||
|       options[i].style.display = "block"; | ||||
|   } | ||||
| } | ||||
|  | ||||
| function onKeyGen() { | ||||
|   if (window.navigator.userAgent.indexOf(" Edge/") >= 0) { | ||||
|     $("#enroll .loader-container").hide(); | ||||
|     $("#enroll .edge-broken").show(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   window.keys = forge.pki.rsa.generateKeyPair(KEY_SIZE); | ||||
|   console.info('Key-pair created.'); | ||||
|  | ||||
|   window.csr = forge.pki.createCertificationRequest(); | ||||
|   csr.publicKey = keys.publicKey; | ||||
|   csr.setSubject([{ | ||||
|     name: 'commonName', value: common_name | ||||
|   }]); | ||||
|  | ||||
|   csr.sign(keys.privateKey, forge.md.sha384.create()); | ||||
|   console.info('Certification request created'); | ||||
|  | ||||
|  | ||||
|   $("#enroll .loader-container").hide(); | ||||
|  | ||||
|   var prefix = null; | ||||
|   for (i in DEVICE_KEYWORDS) { | ||||
|     var keyword = DEVICE_KEYWORDS[i]; | ||||
|     if (window.navigator.userAgent.indexOf(keyword) >= 0) { | ||||
|       prefix = keyword.toLowerCase(); | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (prefix == null) { | ||||
|       $(".option").show(); | ||||
|       return; | ||||
|   } | ||||
|  | ||||
|   var protocols = query.protocols.split(","); | ||||
|   console.info("Showing snippets for:", protocols); | ||||
|   for (var j = 0; j < protocols.length; j++) { | ||||
|       var options = document.querySelectorAll(".option." + protocols[j] + "." + prefix); | ||||
|       for (i = 0; i < options.length; i++) { | ||||
|           options[i].style.display = "block"; | ||||
|       } | ||||
|   } | ||||
|   $(".option.any").show(); | ||||
| } | ||||
|  | ||||
| function blobToUuid(blob) { | ||||
|   var md = forge.md.md5.create(); | ||||
|   md.update(blob); | ||||
|   var digest = md.digest().toHex(); | ||||
|   return digest.substring(0, 8) + "-" + | ||||
|       digest.substring(8, 12) + "-" + | ||||
|       digest.substring(12, 16) + "-" + | ||||
|       digest.substring(16,20) + "-" + | ||||
|       digest.substring(20); | ||||
| } | ||||
|  | ||||
| function onEnroll(encoding) { | ||||
|   console.info("Service name:", query.title); | ||||
|  | ||||
|   console.info("User agent:", window.navigator.userAgent); | ||||
|   var xhr = new XMLHttpRequest(); | ||||
|   xhr.open('GET', "/api/certificate/"); | ||||
|   xhr.onload = function() { | ||||
|     if (xhr.status === 200) { | ||||
|       var ca = forge.pki.certificateFromPem(xhr.responseText); | ||||
|       console.info("Got CA certificate:"); | ||||
|       var xhr2 = new XMLHttpRequest(); | ||||
|       xhr2.open("PUT", "/api/token/?token=" + query.token ); | ||||
|       xhr2.onload = function() { | ||||
|         if (xhr2.status === 200) { | ||||
|           var a = document.createElement("a"); | ||||
|           var cert = forge.pki.certificateFromPem(xhr2.responseText); | ||||
|           console.info("Got signed certificate:", xhr2.responseText); | ||||
|           var p12 = forge.asn1.toDer(forge.pkcs12.toPkcs12Asn1( | ||||
|             keys.privateKey, [cert, ca], "", {algorithm: '3des'})).getBytes(); | ||||
|  | ||||
|           switch(encoding) { | ||||
|             case 'p12': | ||||
|               var buf = forge.asn1.toDer(p12).getBytes(); | ||||
|               var mimetype = "application/x-pkcs12" | ||||
|               a.download = query.title + ".p12"; | ||||
|               break | ||||
|             case 'sswan': | ||||
|               var buf = JSON.stringify({ | ||||
|                   uuid: blobToUuid(authority.namespace), | ||||
|                   name: authority.namespace, | ||||
|                   type: "ikev2-cert", | ||||
|                   'ike-proposal': 'aes256-sha384-prfsha384-modp2048', | ||||
|                   'esp-proposal': 'aes128gcm16-modp2048', | ||||
|                   remote: { | ||||
|                       addr: authority.namespace, | ||||
|                       revocation: { | ||||
|                           crl: false, | ||||
|                           strict: true | ||||
|                       } | ||||
|                   }, | ||||
|                   local: { | ||||
|                       p12: forge.util.encode64(p12) | ||||
|                   } | ||||
|               }); | ||||
|               console.info("Buf is:", buf); | ||||
|               var mimetype = "application/vnd.strongswan.profile" | ||||
|               a.download = query.title + ".sswan"; | ||||
|               break | ||||
|             case 'ovpn': | ||||
|               var buf = nunjucks.render('snippets/openvpn-client.conf', { | ||||
|                   authority: authority, | ||||
|                   key: forge.pki.privateKeyToPem(keys.privateKey), | ||||
|                   cert: xhr2.responseText, | ||||
|                   ca: xhr.responseText | ||||
|               }); | ||||
|               var mimetype = "application/x-openvpn-profile"; | ||||
|               a.download = query.title + ".ovpn"; | ||||
|               break | ||||
|             case 'mobileconfig': | ||||
|               var p12 = forge.asn1.toDer(forge.pkcs12.toPkcs12Asn1( | ||||
|                   keys.privateKey, [cert, ca], "1234", {algorithm: '3des'})).getBytes(); | ||||
|               var buf = nunjucks.render('snippets/ios.mobileconfig', { | ||||
|                   authority: authority, | ||||
|                   service_uuid: blobToUuid(query.title), | ||||
|                   conf_uuid: blobToUuid(query.title + " conf1"), | ||||
|                   title: query.title, | ||||
|                   common_name: common_name, | ||||
|                   gateway: authority.namespace, | ||||
|                   p12_uuid: blobToUuid(p12), | ||||
|                   p12: forge.util.encode64(p12), | ||||
|                   ca_uuid: blobToUuid(forge.asn1.toDer(forge.pki.certificateToAsn1(ca)).getBytes()), | ||||
|                   ca: forge.util.encode64(forge.asn1.toDer(forge.pki.certificateToAsn1(ca)).getBytes()) | ||||
|               }); | ||||
|               var mimetype = "application/x-apple-aspen-config"; | ||||
|               a.download = query.title + ".mobileconfig"; | ||||
|               break | ||||
|           } | ||||
|           a.href = "data:" + mimetype + ";base64," + forge.util.encode64(buf); | ||||
|           console.info("Offering bundle for download"); | ||||
|           document.body.appendChild(a); // Firefox needs this! | ||||
|           a.click(); | ||||
|         } else { | ||||
|           if (xhr2.status == 403) { alert("Token used or expired"); } | ||||
|           console.info('Request failed.  Returned status of ' + xhr2.status); | ||||
|           try { | ||||
|             var r = JSON.parse(xhr2.responseText); | ||||
|             console.info("Server said: " + r.title); | ||||
|             console.info(r.description); | ||||
|           } catch(e) { | ||||
|              console.info("Server said: " + xhr2.statusText); | ||||
|           } | ||||
|         } | ||||
|       }; | ||||
|       xhr2.send(forge.pki.certificationRequestToPem(csr)); | ||||
|     } | ||||
|   } | ||||
|   xhr.send(); | ||||
| } | ||||
|  | ||||
| function onHashChanged() { | ||||
|  | ||||
|     window.query = {}; | ||||
|     var a = location.hash.substring(1).split('&'); | ||||
|     for (var i = 0; i < a.length; i++) { | ||||
|         var b = a[i].split('='); | ||||
|         query[decodeURIComponent(b[0])] = decodeURIComponent(b[1] || ''); | ||||
|     } | ||||
|  | ||||
|     console.info("Hash is now:", query); | ||||
|  | ||||
|     $.get({ | ||||
|         method: "GET", | ||||
|         url: "/api/bootstrap/", | ||||
|         error: function(response) { | ||||
|             if (response.responseJSON) { | ||||
|                 var msg = response.responseJSON | ||||
|             } else { | ||||
|                 var msg = { title: "Error " + response.status, description: response.statusText } | ||||
|             } | ||||
|             $("#view-dashboard").html(env.render('views/error.html', { message: msg })); | ||||
|         }, | ||||
|         success: function(authority) { | ||||
|           window.authority = authority | ||||
|  | ||||
|           // Device identifier | ||||
|           var dig = forge.md.sha384.create(); | ||||
|           dig.update(window.navigator.userAgent); | ||||
|  | ||||
|           var prefix = "unknown"; | ||||
|           for (i in DEVICE_KEYWORDS) { | ||||
|             var keyword = DEVICE_KEYWORDS[i]; | ||||
|             if (window.navigator.userAgent.indexOf(keyword) >= 0) { | ||||
|               prefix = keyword.toLowerCase(); | ||||
|               break; | ||||
|             } | ||||
|           } | ||||
|  | ||||
|           window.common_name = prefix + "-" + dig.digest().toHex().substring(0, 5); | ||||
|           console.info("Device identifier:", common_name); | ||||
|  | ||||
|           if (window.location.protocol != "https:") { | ||||
|               $("#view-dashboard").html(env.render('views/insecure.html', {authority:authority})); | ||||
|           } else { | ||||
|               if (query.action == "enroll") { | ||||
|                   $("#view-dashboard").html(env.render('views/enroll.html', { | ||||
|                     common_name: common_name, | ||||
|                     authority: authority, | ||||
|                     token: query.token, | ||||
|                   })); | ||||
|                   var options = document.querySelectorAll(".option"); | ||||
|                   for (i = 0; i < options.length; i++) { | ||||
|                       options[i].style.display = "none"; | ||||
|                   } | ||||
|                   setTimeout(onKeyGen, 100); | ||||
|                   console.info("Generating key pair..."); | ||||
|               } else { | ||||
|                   loadAuthority(query); | ||||
|               } | ||||
|           } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
| } | ||||
|  | ||||
| function onTagClicked(e) { | ||||
|     e.preventDefault(); | ||||
|     var cn = $(e.target).attr("data-cn"); | ||||
|     var id = $(e.target).attr("title"); | ||||
|     var value = $(e.target).html(); | ||||
|     var updated = prompt("Enter new tag or clear to remove the tag", value); | ||||
|     if (updated == "") { | ||||
|         $(event.target).addClass("disabled"); | ||||
|         $.ajax({ | ||||
|             method: "DELETE", | ||||
|             url: "/api/signed/" + cn + "/tag/" + id + "/" | ||||
|         }); | ||||
|     } else if (updated && updated != value) { | ||||
|         $(e.target).addClass("disabled"); | ||||
|         $.ajax({ | ||||
|             method: "PUT", | ||||
|             url: "/api/signed/" + cn + "/tag/" + id + "/", | ||||
|             data: { value: updated }, | ||||
|             dataType: "text", | ||||
|             complete: function(xhr, status) { | ||||
|                 console.info("Tag added successfully", xhr.status,  status); | ||||
|             }, | ||||
|             success: function() { | ||||
|             }, | ||||
|             error: function(xhr, status, e) { | ||||
|                 console.info("Submitting request failed with:", status, e); | ||||
|                 alert(e); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|     return false; | ||||
| } | ||||
|  | ||||
| function onNewTagClicked(e) { | ||||
|     e.preventDefault(); | ||||
|     var cn = $(e.target).attr("data-cn"); | ||||
|     var key = $(e.target).attr("data-key"); | ||||
|     var value = prompt("Enter new " + key + " tag for " + cn); | ||||
|     if (!value) return; | ||||
|     if (value.length == 0) return; | ||||
|     var $container = $(".tags[data-cn='" + cn + "']"); | ||||
|     $container.addClass("disabled"); | ||||
|     $.ajax({ | ||||
|         method: "POST", | ||||
|         url: "/api/signed/" + cn + "/tag/", | ||||
|         data: { value: value, key: key }, | ||||
|         dataType: "text", | ||||
|         complete: function(xhr, status) { | ||||
|             console.info("Tag added successfully", xhr.status,  status); | ||||
|         }, | ||||
|         success: function() { | ||||
|             $container.removeClass("disabled"); | ||||
|         }, | ||||
|         error: function(xhr, status, e) { | ||||
|             console.info("Submitting request failed with:", status, e); | ||||
|             alert(e); | ||||
|         } | ||||
|     }); | ||||
|     return false; | ||||
| } | ||||
|  | ||||
| function onTagFilterChanged() { | ||||
|     var key = $(event.target).val(); | ||||
|     console.info("New key is:", key); | ||||
| } | ||||
|  | ||||
| function onLogEntry (e) { | ||||
|     if (e.data) { | ||||
|         e = JSON.parse(e.data); | ||||
|         e.fresh = true; | ||||
|     } | ||||
|  | ||||
|     if ($("#log-level-" + e.severity).prop("checked")) { | ||||
|         $("#log-entries").prepend(env.render("views/logentry.html", { | ||||
|             entry: { | ||||
|                 created: new Date(e.created).toLocaleString(), | ||||
|                 message: e.message, | ||||
|                 severity: e.severity, | ||||
|                 fresh: e.fresh, | ||||
|                 keywords: e.message.toLowerCase().split(/,?[ <>/]+/).join("|") | ||||
|             } | ||||
|         })); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| function onRequestSubmitted(e) { | ||||
|     console.log("Request submitted:", e.data); | ||||
|     $.ajax({ | ||||
|         method: "GET", | ||||
|         url: "/api/request/id/" + e.data + "/", | ||||
|         dataType: "json", | ||||
|         success: function(request, status, xhr) { | ||||
|             console.info("Going to prepend:", request); | ||||
|             onRequestDeleted(request.id); // Delete any existing ones just in case | ||||
|             $("#pending_requests").prepend( | ||||
|                 env.render('views/request.html', { request: request, authority: authority })); | ||||
|             $("#pending_requests time").timeago(); | ||||
|         }, | ||||
|         error: function(response) { | ||||
|             console.info("Failed to retrieve certificate:", response); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function onRequestDeleted(e) { | ||||
|     console.log("Removing deleted request", e.data); | ||||
|     $("#request-" + e.data).remove(); | ||||
| } | ||||
|  | ||||
| function onLeaseUpdate(e) { | ||||
|     var slug = normalizeCommonName(e.data); | ||||
|     console.log("Lease updated:", e.data); | ||||
|     $.ajax({ | ||||
|         method: "GET", | ||||
|         url: "/api/signed/" + e.data + "/lease/", | ||||
|         dataType: "json", | ||||
|         success: function(lease, status, xhr) { | ||||
|             console.info("Retrieved lease update details:", lease); | ||||
|             lease.age = (new Date() - new Date(lease.last_seen)) / 1000.0 | ||||
|             var $lease = $("#certificate-" + slug + " .lease"); | ||||
|             $lease.html(env.render('views/lease.html', { | ||||
|                 certificate: { | ||||
|                     lease: lease }})); | ||||
|             $("time", $lease).timeago(); | ||||
|             filterSigned(); | ||||
|         }, | ||||
|         error: function(response) { | ||||
|             console.info("Failed to retrieve certificate:", response); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function onRequestSigned(e) { | ||||
|     console.log("Request signed:", e.data); | ||||
|     var id = e.data | ||||
|     //var slug = normalizeCommonName(e.data); | ||||
|     //console.log("Removing:", slug); | ||||
|     console.log("Removing:", e.data); | ||||
|  | ||||
|     $("#request-" + id).slideUp("normal", function() { $(this).remove(); }); | ||||
|     $("#certificate-" + id).slideUp("normal", function() { $(this).remove(); }); | ||||
|  | ||||
|     $.ajax({ | ||||
|         method: "GET", | ||||
|         url: "/api/signed/id/" + e.data + "/", | ||||
|         dataType: "json", | ||||
|         success: function(certificate, status, xhr) { | ||||
|             console.info("Retrieved certificate:", certificate); | ||||
|             $("#signed_certificates").prepend( | ||||
|                 env.render('views/signed.html', { certificate: certificate, session: session })); | ||||
|             $("#signed_certificates time").timeago(); // TODO: optimize? | ||||
|             filterSigned(); | ||||
|         }, | ||||
|         error: function(response) { | ||||
|             console.info("Failed to retrieve certificate:", response); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function onCertificateRevoked(e) { | ||||
|     console.log("Removing revoked certificate", e.data); | ||||
|     $("#certificate-" + e.data).slideUp("normal", function() { $(this).remove(); }); | ||||
| } | ||||
|  | ||||
| function onTagUpdated(e) { | ||||
|     var cn = e.data; | ||||
|     console.log("Tag updated event recevied", cn); | ||||
|     $.ajax({ | ||||
|         method: "GET", | ||||
|         url: "/api/signed/" + cn + "/tag/", | ||||
|         dataType: "json", | ||||
|         success:function(tags, status, xhr) { | ||||
|             console.info("Updated", cn, "tags", tags); | ||||
|             $(".tags[data-cn='" + cn+"']").html( | ||||
|                 env.render('views/tags.html', { | ||||
|                     certificate: { | ||||
|                         common_name: cn, | ||||
|                         tags:tags }})); | ||||
|         } | ||||
|     }) | ||||
| } | ||||
|  | ||||
| function onAttributeUpdated(e) { | ||||
|     var cn = e.data; | ||||
|     console.log("Attributes updated", cn); | ||||
|     $.ajax({ | ||||
|         method: "GET", | ||||
|         url: "/api/signed/" + cn + "/attr/", | ||||
|         dataType: "json", | ||||
|         success:function(attributes, status, xhr) { | ||||
|             console.info("Updated", cn, "attributes", attributes); | ||||
|             $(".attributes[data-cn='" + cn + "']").html( | ||||
|                 env.render('views/attributes.html', { | ||||
|                     certificate: { | ||||
|                         common_name: cn, | ||||
|                         attributes:attributes }})); | ||||
|         } | ||||
|     }) | ||||
| } | ||||
|  | ||||
| function onSubmitRequest() { | ||||
|     $.ajax({ | ||||
|         method: "POST", | ||||
|         url: "/api/request/", | ||||
|         headers: { | ||||
|             "Accept": "application/json; charset=utf-8", | ||||
|             "Content-Type": "application/pkcs10" | ||||
|         }, | ||||
|         data: $("#request_body").val(), | ||||
|  | ||||
|         success:function(attributes, status, xhr) { | ||||
|             // Close the modal | ||||
|             $("[data-dismiss=modal]").trigger({ type: "click" }); | ||||
|         }, | ||||
|         error: function(xhr, status, e) { | ||||
|             console.info("Submitting request failed with:", status, e); | ||||
|             alert(e); | ||||
|         } | ||||
|     }) | ||||
| } | ||||
|  | ||||
| function onServerStarted() { | ||||
|     console.info("Server started"); | ||||
|     location.reload(); | ||||
| } | ||||
|  | ||||
| function onServerStopped() { | ||||
|     $("#view-dashboard").html('<div class="loader"></div><p>Server under maintenance</p>'); | ||||
|     console.info("Server stopped"); | ||||
|  | ||||
| } | ||||
|  | ||||
| function onIssueToken() { | ||||
|     $.ajax({ | ||||
|         method: "POST", | ||||
|         url: "/api/token/", | ||||
|         data: { username: $("#token_username").val(), mail: $("#token_mail").val() }, | ||||
|         dataType: "text", | ||||
|         complete: function(xhr, status) { | ||||
|             console.info("Token sent successfully", xhr.status, status); | ||||
|         }, | ||||
|         success: function(data) { | ||||
|             var url = JSON.parse(data).url; | ||||
|             console.info("DATA:", url); | ||||
|             var code = new QRCode({ | ||||
|                 content: url, | ||||
|                 width: 512, | ||||
|                 height: 512, | ||||
|             }); | ||||
|             document.getElementById("token_qrcode").innerHTML = code.svg(); | ||||
|  | ||||
|         }, | ||||
|         error: function(xhr, status, e) { | ||||
|             console.info("Submitting request failed with:", status, e); | ||||
|             alert(e); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function filterSigned() { | ||||
|     if ($("#search").val() != "") { | ||||
|         console.info("Not filtering by state since keyword filter is active"); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     $("#signed_certificates .filterable").each(function(i,j) { | ||||
|         var last_seen = j.getAttribute("data-last-seen"); | ||||
|         var state = "new"; | ||||
|         if (last_seen) { | ||||
|             var age = (new Date() - new Date(last_seen)) / 1000; | ||||
|             if (age > 172800) { | ||||
|                 state = "dead"; | ||||
|             } else if (age > 10800) { | ||||
|                 state = "offline"; | ||||
|             } else { | ||||
|                 state = "online"; | ||||
|             } | ||||
|         } | ||||
|         j.setAttribute("data-state", state); | ||||
|         j.style.display = $("#signed-filter-" + state).prop("checked") ? "block" : "none"; | ||||
|     }); | ||||
|         $("#signed-total").html($("#signed_certificates .filterable").length); | ||||
|         $("#signed-filter-counter").html($("#signed_certificates .filterable:visible").length); | ||||
| } | ||||
|  | ||||
| function loadAuthority(query) { | ||||
|     console.info("Loading CA, to debug: curl " + window.location.href + " --negotiate -u : -H 'Accept: application/json'"); | ||||
|     $.ajax({ | ||||
|         method: "GET", | ||||
|         url: "/api/session/", | ||||
|         dataType: "json", | ||||
|         error: function(response) { | ||||
|             if (response.responseJSON) { | ||||
|                 var msg = response.responseJSON | ||||
|             } else { | ||||
|                 var msg = { title: "Error " + response.status, description: response.statusText } | ||||
|             } | ||||
|             $("#view-dashboard").html(env.render('views/error.html', { message: msg })); | ||||
|         }, | ||||
|         success: function(session, status, xhr) { | ||||
|             window.session = session; | ||||
|  | ||||
|             console.info("Loaded:", session); | ||||
|             $("#login").hide(); | ||||
|             $("#search").show(); | ||||
|  | ||||
|             /** | ||||
|              * Render authority views | ||||
|              **/ | ||||
|             $("#view-dashboard").html(env.render('views/authority.html', { | ||||
|                 session: session, | ||||
|                 authority: authority, | ||||
|                 window: window, | ||||
|  | ||||
|                 // Parameters for unified snippets | ||||
|                 dhparam_path: "/etc/ssl/dhparam.pem", | ||||
|                 key_path: "/etc/certidude/authority/" + window.authority.namespace + "/host_key.pem", | ||||
|                 certificate_path: "/etc/certidude/authority/" + window.authority.namespace + "/host_cert.pem", | ||||
|                 authority_path: "/etc/certidude/authority/" + window.authority.namespace + "/ca_cert.pem", | ||||
|                 revocations_path: "/etc/certidude/authority/" + window.authority.namespace + "/crl.pem", | ||||
|                 common_name: "$NAME" | ||||
|             })); | ||||
|  | ||||
|             // Initial filtering | ||||
|             $("#signed-filter .btn").on('change', filterSigned); | ||||
|             filterSigned(); | ||||
|  | ||||
|             // Attach timeago events | ||||
|             $("time").timeago(); | ||||
|  | ||||
|             if (window.authority) { | ||||
|                 $("#log input").each(function(i, e) { | ||||
|                     console.info("e.checked:", e.checked , "and", e.id, "@localstorage is", localStorage[e.id], "setting to:", localStorage[e.id] || e.checked, "bool:", localStorage[e.id] || e.checked == "true"); | ||||
|                     e.checked = localStorage[e.id] ? localStorage[e.id] == "true" : e.checked; | ||||
|                 }); | ||||
|  | ||||
|                 $("#log input").change(function() { | ||||
|                     localStorage[this.id] = this.checked; | ||||
|                 }); | ||||
|  | ||||
|                 console.info("Opening EventSource from:", window.session.events); | ||||
|  | ||||
|                 window.source = new EventSource(window.session.events); | ||||
|  | ||||
|                 source.onmessage = function(event) { | ||||
|                     console.log("Received server-sent event:", event); | ||||
|                 } | ||||
|  | ||||
|  | ||||
|                 source.addEventListener("lease-update", onLeaseUpdate); | ||||
|                 source.addEventListener("request-deleted", onRequestDeleted); | ||||
|                 source.addEventListener("request-submitted", onRequestSubmitted); | ||||
|                 source.addEventListener("request-signed", onRequestSigned); | ||||
|                 source.addEventListener("certificate-revoked", onCertificateRevoked); | ||||
|                 source.addEventListener("tag-update", onTagUpdated); | ||||
|                 source.addEventListener("attribute-update", onAttributeUpdated); | ||||
|                 source.addEventListener("server-started", onServerStarted); | ||||
|                 source.addEventListener("server-stopped", onServerStopped); | ||||
|  | ||||
|             } | ||||
|  | ||||
|             $("nav#menu li").click(function(e) { | ||||
|                 $("section").hide(); | ||||
|                 $("section#" + $(e.target).attr("data-section")).show(); | ||||
|             }); | ||||
|  | ||||
|  | ||||
|  | ||||
|             $("#enroll").click(function() { | ||||
|                 var keys = forge.pki.rsa.generateKeyPair(1024); | ||||
|  | ||||
|                 $.ajax({ | ||||
|                     method: "POST", | ||||
|                     url: "/api/token/", | ||||
|                     data: "username=" + session.user.name, | ||||
|                     complete: function(xhr, status) { | ||||
|                         console.info("Token generated successfully:", xhr, status); | ||||
|  | ||||
|                     }, | ||||
|                     error: function(xhr, status, e) { | ||||
|                         console.info("Token generation failed:", status, e); | ||||
|                         alert(e); | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|  | ||||
|  | ||||
|                 var privateKeyBuffer = forge.pki.privateKeyToPem(keys.privateKey); | ||||
|             }); | ||||
|  | ||||
|             /** | ||||
|              * Set up search bar | ||||
|               */ | ||||
|             $(window).on("search", function() { | ||||
|                 var q = $("#search").val(); | ||||
|                 $(".filterable").each(function(i, e) { | ||||
|                     if ($(e).attr("data-keywords").toLowerCase().indexOf(q) >= 0) { | ||||
|                         $(e).show(); | ||||
|                     } else { | ||||
|                         $(e).hide(); | ||||
|                     } | ||||
|                 }); | ||||
|                 if (q.length == 0) { | ||||
|                     filterSigned(); | ||||
|                     $("#signed-filter .btn").removeClass("disabled").prop("disabled", false); | ||||
|                 } else { | ||||
|                     $("#signed-filter .btn").addClass("disabled").prop("disabled", true); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|             /** | ||||
|              * Bind key up event of search bar | ||||
|              */ | ||||
|             $("#search").on("keyup", function() { | ||||
|                 if (window.searchTimeout) { clearTimeout(window.searchTimeout); } | ||||
|                 window.searchTimeout = setTimeout(function() { $(window).trigger("search"); }, 500); | ||||
|             }); | ||||
|  | ||||
|             if (session.request_submission_allowed) { | ||||
|                 $("#request_submit").click(function() { | ||||
|                     $(this).addClass("busy"); | ||||
|                     $.ajax({ | ||||
|                         method: "POST", | ||||
|                         contentType: "application/pkcs10", | ||||
|                         url: "/api/request/", | ||||
|                         data: $("#request_body").val(), | ||||
|                         dataType: "text", | ||||
|                         complete: function(xhr, status) { | ||||
|                             console.info("Request submitted successfully, server returned", xhr.status,  status); | ||||
|                             $("#request_submit").removeClass("busy"); | ||||
|                         }, | ||||
|                         success: function() { | ||||
|                             // Clear textarea on success | ||||
|                             $("#request_body").val(""); | ||||
|                         }, | ||||
|                         error: function(xhr, status, e) { | ||||
|                             console.info("Submitting request failed with:", status, e); | ||||
|                             alert(e); | ||||
|                         } | ||||
|                     }); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             $("nav .nav-link.dashboard").removeClass("disabled").click(function() { | ||||
|                 $("#column-requests").show(); | ||||
|                 $("#column-signed").show(); | ||||
|                 $("#column-revoked").show(); | ||||
|                 $("#column-log").hide(); | ||||
|             }); | ||||
|  | ||||
|             /** | ||||
|              * Fetch log entries | ||||
|              */ | ||||
|             if (session.features.logging) { | ||||
|                 if ($("#column-log:visible").length) { | ||||
|                     loadLog(); | ||||
|                 } | ||||
|                 $("nav .nav-link.log").removeClass("disabled").click(function() { | ||||
|                     loadLog(); | ||||
|                     $("#column-requests").show(); | ||||
|                     $("#column-signed").show(); | ||||
|                     $("#column-revoked").show(); | ||||
|                     $("#column-log").hide(); | ||||
|                 }); | ||||
|             } else { | ||||
|                 console.info("Log disabled"); | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function loadLog() { | ||||
|     if (window.log_initialized) { | ||||
|         console.info("Log already loaded"); | ||||
|         return; | ||||
|     } | ||||
|     console.info("Loading log..."); | ||||
|     window.log_initialized = true; | ||||
|     $.ajax({ | ||||
|         method: "GET", | ||||
|         url: "/api/log/?limit=100", | ||||
|         dataType: "json", | ||||
|         success: function(entries, status, xhr) { | ||||
|             console.info("Got", entries.length, "log entries"); | ||||
|             for (var j = entries.length-1; j--; ) { | ||||
|                 onLogEntry(entries[j]); | ||||
|             }; | ||||
|             source.addEventListener("log-entry", onLogEntry); | ||||
|             $("#column-log .loader-container").hide(); | ||||
|             $("#column-log .content").show(); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function datetimeFilter(s) { | ||||
|     return new Date(s); | ||||
| } | ||||
|  | ||||
| function serialFilter(s) { | ||||
|     return s.substring(0,s.length-14) + " " + | ||||
|         s.substring(s.length-14); | ||||
| } | ||||
|  | ||||
| $(document).ready(function() { | ||||
|     window.env = new nunjucks.Environment(); | ||||
|     env.addFilter("datetime", datetimeFilter); | ||||
|     env.addFilter("serial", serialFilter); | ||||
|     onHashChanged(); | ||||
| }); | ||||
							
								
								
									
										104
									
								
								static/js/shell.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,104 @@ | ||||
| function onChooseFile(event) { | ||||
|   if (!(window.File && window.FileReader && window.FileList && window.Blob)) { | ||||
|     alert('The File APIs are not fully supported in this browser.'); | ||||
|   } | ||||
|  | ||||
|     var files = event.target.files; | ||||
|  | ||||
|  | ||||
|       var reader = new FileReader(); | ||||
|  | ||||
|       // Closure to capture the file information. | ||||
|       reader.onload = (function(theFile) { | ||||
|         return function(e) { | ||||
|           // Render thumbnail. | ||||
|           var span = document.createElement('span'); | ||||
|           span.innerHTML = ['<img class="thumb" src="', e.target.result, | ||||
|                             '" title="', escape(theFile.name), '"/>'].join(''); | ||||
|           document.getElementById('list').insertBefore(span, null); | ||||
|         }; | ||||
|       })(f); | ||||
|  | ||||
|       // Read in the image file as a data URL. | ||||
|       reader.readAsDataURL(f);     | ||||
| } | ||||
|  | ||||
| function init() { | ||||
|     var url = "wss://" + window.location.hostname + "/pipe/" + window.location.hash.substring(1); | ||||
|     console.info("Opening:", url); | ||||
|      | ||||
|     var term = new Terminal({rows: 50, cols: 200}); | ||||
|     term.open(document.getElementById('terminal')); | ||||
|     term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ') | ||||
|  | ||||
|     const ws = new WebSocket(url); | ||||
|  | ||||
|     // Connection opened | ||||
|     ws.addEventListener('open', function (event) { | ||||
|         console.log('Hello Server!'); | ||||
|     ws.send(JSON.stringify({"type": "session-start", "cols": 200, "rows": 50})); | ||||
|     }); | ||||
|  | ||||
|     // Listen for messages | ||||
|     ws.addEventListener('message', function (event) { | ||||
|         var e = JSON.parse(event.data); | ||||
|         console.info(e); | ||||
|         switch (e.type) { | ||||
|           case "stdout": | ||||
|             var buf = atob(e.value); | ||||
|             term.write(buf.replace(/\n/g, '\n\r')); | ||||
|             break | ||||
|           case "exit": | ||||
|             term.write("=== Process finished, no more input accepted ==="); | ||||
|             break; | ||||
|         } | ||||
|          | ||||
|     }); | ||||
|  | ||||
|     // Connection opened | ||||
|     ws.addEventListener('close', function (event) { | ||||
|         console.log('Bye Server!'); | ||||
|     }); | ||||
|  | ||||
|     function runFakeTerminal() { | ||||
|         if (term._initialized) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         term._initialized = true; | ||||
|  | ||||
|         term.prompt = () => { | ||||
|             term.write('\r\n$ '); | ||||
|         }; | ||||
|  | ||||
|         term.writeln('Welcome to xterm.js'); | ||||
|         term.writeln('This is a local terminal emulation, without a real terminal in the back-end.'); | ||||
|         term.writeln('Type some keys and commands to play around.'); | ||||
|         term.writeln(''); | ||||
|         term.prompt(); | ||||
|  | ||||
|         term.on('key', function(key, ev) { | ||||
|             const printable = !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey; | ||||
|  | ||||
| /*            if (ev.keyCode === 13) { | ||||
|                 term.prompt(); | ||||
|             } else if (ev.keyCode === 8) { | ||||
|                 // Do not delete the prompt | ||||
|                 if (term._core.buffer.x > 2) { | ||||
|                     term.write('\b \b'); | ||||
|                 } | ||||
|             } else if (printable) { | ||||
| //                term.write(key); | ||||
|             }*/ | ||||
|             console.log("Got keypress:", key); | ||||
|             if (key == "\r") key = "\n"; | ||||
|             ws.send(JSON.stringify({"type":"stdin", "value":btoa(key)})); | ||||
|         }); | ||||
|  | ||||
|         term.on('paste', function(data) { | ||||
|             term.write(data); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     runFakeTerminal(); | ||||
| } | ||||
							
								
								
									
										2
									
								
								static/robots.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| User-agent: * | ||||
| Disallow: / | ||||
							
								
								
									
										17
									
								
								static/shell.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="utf-8"/> | ||||
|     <title>Certidude server</title> | ||||
|     <link href="/assets/css/bundle.css" rel="stylesheet" type="text/css"/> | ||||
| <!--    <link href="/css/shell.css" rel="stylesheet" type="text/css"/> --> | ||||
|     <script type="text/javascript" src="/assets/js/bundle.js"></script> | ||||
|     <script type="text/javascript" src="/js/shell.js"></script> | ||||
|   </head> | ||||
|   <body onLoad="init();"> | ||||
|     <input type='upload' onchange='onChooseFile();' /> | ||||
|  | ||||
|     <div id="terminal"></div> | ||||
|   </body> | ||||
| </html> | ||||
|  | ||||
							
								
								
									
										6
									
								
								templates/client/certidude.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| [Unit] | ||||
| Description=Renew certificates and update revocation lists | ||||
|  | ||||
| [Service] | ||||
| Type=simple | ||||
| ExecStart={{ sys.argv[0] }} enroll | ||||
							
								
								
									
										11
									
								
								templates/client/certidude.timer
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| [Unit] | ||||
| Description=Run certidude enroll daily | ||||
|  | ||||
| [Timer] | ||||
| OnCalendar=daily | ||||
| Persistent=true | ||||
| Unit=certidude-enroll.service | ||||
|  | ||||
| [Install] | ||||
| WantedBy=timers.target | ||||
|  | ||||
							
								
								
									
										8
									
								
								templates/client/openvpn-reconnect.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | ||||
| [Unit] | ||||
| Description=Restart OpenVPN after suspend | ||||
|  | ||||
| [Service] | ||||
| ExecStart=/usr/bin/pkill --signal SIGHUP --exact openvpn | ||||
|  | ||||
| [Install] | ||||
| WantedBy=sleep.target | ||||
							
								
								
									
										57
									
								
								templates/openvpn-client.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,57 @@ | ||||
| # Copy this file to /etc/certidude/template.ovpn and customize as you see fit | ||||
|  | ||||
| # Note: don't append comments to lines, Ubuntu 16.04 NetworkManager importer is very picky | ||||
| # See more potential problems here: | ||||
| # https://askubuntu.com/questions/761684/error-the-plugin-does-not-support-import-capability-when-attempting-to-import | ||||
|  | ||||
| # Run as OpenVPN client, pull routes, DNS server, DNS suffix from gateway | ||||
| client | ||||
|  | ||||
| # OpenVPN gateway(s) | ||||
| nobind | ||||
| ;proto udp | ||||
| ;port 1194 | ||||
| {% if servers %} | ||||
| remote-random | ||||
| {% for server in servers %} | ||||
| remote {{ server }} | ||||
| {% endfor %} | ||||
| {% else %} | ||||
| remote 1.2.3.4 | ||||
| {% endif %} | ||||
|  | ||||
| # Virtual network interface settings | ||||
| dev tun | ||||
| persist-tun | ||||
|  | ||||
| # Customize crypto settings | ||||
| ;tls-version-min 1.2 | ||||
| ;tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384 | ||||
| ;cipher AES-256-CBC | ||||
| ;auth SHA384 | ||||
|  | ||||
| # Check that server presented certificate has TLS Server flag present | ||||
| remote-cert-tls server | ||||
|  | ||||
| # X.509 business | ||||
| persist-key | ||||
| <ca> | ||||
| {{ca}} | ||||
| </ca> | ||||
| <key> | ||||
| {{key}} | ||||
| </key> | ||||
| <cert> | ||||
| {{cert}} | ||||
| </cert> | ||||
|  | ||||
| # Revocation list | ||||
| # Tunnelblick doens't handle inlined CRL | ||||
| # hard to update as well | ||||
| ;<crl-verify> | ||||
| ;</crl-verify> | ||||
|  | ||||
| # Pre-shared key for extra layer of security | ||||
| ;<ta> | ||||
| ;</ta> | ||||
|  | ||||
							
								
								
									
										2
									
								
								templates/snippets/certidude-client.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| pip3 install --upgrade git+http://git.k-space.ee/pinecrypt/pinecrypt-client.git | ||||
| certidude provision {{ authority.namespace }} | ||||
							
								
								
									
										98
									
								
								templates/snippets/ios.mobileconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,98 @@ | ||||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||||
| <plist version="1.0"> | ||||
| <dict> | ||||
|     <!-- https://developer.apple.com/library/content/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html --> | ||||
|     <key>PayloadDisplayName</key> | ||||
|     <string>{{ gateway }}</string> | ||||
|     <key>PayloadDescription</key> | ||||
|     <string>IPSec IKEv2 VPN connection via {{ gateway }}</string> | ||||
|     <!-- This is a reverse-DNS style unique identifier used to detect duplicate profiles --> | ||||
|     <key>PayloadIdentifier</key> | ||||
|     <string>{{ gateway }}</string> | ||||
|     <key>PayloadUUID</key> | ||||
|     <string>{{ service_uuid }}</string> | ||||
|     <key>PayloadType</key> | ||||
|     <string>Configuration</string> | ||||
|     <key>PayloadVersion</key> | ||||
|     <integer>1</integer> | ||||
|     <key>PayloadContent</key> | ||||
|     <array> | ||||
|         <dict> | ||||
|             <key>PayloadIdentifier</key> | ||||
|             <string>{{ gateway }}.conf1</string> | ||||
|             <key>PayloadUUID</key> | ||||
|             <string>{{ conf_uuid }}</string> | ||||
|             <key>PayloadType</key> | ||||
|             <string>com.apple.vpn.managed</string> | ||||
|             <key>PayloadVersion</key> | ||||
|             <integer>1</integer> | ||||
|             <key>UserDefinedName</key> | ||||
|             <string>{{ gateway }}</string> | ||||
|             <key>VPNType</key> | ||||
|             <string>IKEv2</string> | ||||
|             <key>IKEv2</key> | ||||
|             <dict> | ||||
|                 <key>RemoteAddress</key> | ||||
|                 <string>{{ gateway }}</string> | ||||
|                 <key>RemoteIdentifier</key> | ||||
|                 <string>{{ gateway }}</string> | ||||
|                 <key>LocalIdentifier</key> | ||||
|                 <string>{{ common_name }}</string> | ||||
|                 <key>ServerCertificateIssuerCommonName</key> | ||||
|                 <string>{{ authority.certificate.common_name }}</string> | ||||
|                 <key>ServerCertificateCommonName</key> | ||||
|                 <string>{{ gateway }}</string> | ||||
|                 <key>AuthenticationMethod</key> | ||||
|                 <string>Certificate</string> | ||||
|                 <key>IKESecurityAssociationParameters</key> | ||||
|                 <dict> | ||||
|                     <key>EncryptionAlgorithm</key> | ||||
|                     <string>AES-256</string> | ||||
|                     <key>IntegrityAlgorithm</key> | ||||
|                     <string>SHA2-384</string> | ||||
|                     <key>DiffieHellmanGroup</key> | ||||
|                     <integer>14</integer> | ||||
|                 </dict> | ||||
|                 <key>ChildSecurityAssociationParameters</key> | ||||
|                 <dict> | ||||
|                     <key>EncryptionAlgorithm</key> | ||||
|                     <string>AES-128-GCM</string> | ||||
|                     <key>IntegrityAlgorithm</key> | ||||
|                     <string>SHA2-256</string> | ||||
|                     <key>DiffieHellmanGroup</key> | ||||
|                     <integer>14</integer> | ||||
|                 </dict> | ||||
|                 <key>EnablePFS</key> | ||||
|                 <integer>1</integer> | ||||
|                 <key>PayloadCertificateUUID</key> | ||||
|                 <string>{{ p12_uuid }}</string> | ||||
|             </dict> | ||||
|         </dict> | ||||
|         <dict> | ||||
|             <key>PayloadIdentifier</key> | ||||
|             <string>{{ common_name }}</string> | ||||
|             <key>PayloadUUID</key> | ||||
|             <string>{{ p12_uuid }}</string> | ||||
|             <key>PayloadType</key> | ||||
|             <string>com.apple.security.pkcs12</string> | ||||
|             <key>PayloadVersion</key> | ||||
|             <integer>1</integer> | ||||
|             <key>PayloadContent</key> | ||||
|             <data>{{ p12 }}</data> | ||||
|         </dict> | ||||
|         <dict> | ||||
|             <key>PayloadIdentifier</key> | ||||
|             <string>{{ authority.certificate.common_name }}</string> | ||||
|             <key>PayloadUUID</key> | ||||
|             <string>{{ ca_uuid }}</string> | ||||
|             <key>PayloadType</key> | ||||
|             <string>com.apple.security.root</string> | ||||
|             <key>PayloadVersion</key> | ||||
|             <integer>1</integer> | ||||
|             <key>PayloadContent</key> | ||||
|             <data>{{ ca }}</data> | ||||
|         </dict> | ||||
|     </array> | ||||
| </dict> | ||||
| </plist> | ||||
|  | ||||
							
								
								
									
										29
									
								
								templates/snippets/networkmanager-openvpn.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,29 @@ | ||||
| [connection] | ||||
| certidude managed = true | ||||
| id = {{ session.service.title }} | ||||
| uuid = {{ uuid }} | ||||
| type = vpn | ||||
|  | ||||
| [vpn] | ||||
| service-type = org.freedesktop.NetworkManager.openvpn | ||||
| connection-type = tls | ||||
| cert-pass-flags 0 | ||||
| tap-dev = no | ||||
| remote-cert-tls = server | ||||
| remote = {{ authority.namespace }} | ||||
| key = {% if key_path %}{{ key_path }}{% else %}/etc/certidude/authority/{{ authority.namespace }}/host_key.pem{% endif %} | ||||
| cert = {% if certificate_path %}{{ certificate_path }}{% else %}/etc/certidude/authority/{{ authority.namespace }}/host_cert.pem{% endif %} | ||||
| ca = {% if authority_path %}{{ authority_path }}{% else %}/etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem{% endif %} | ||||
| tls-cipher = {{ authority.openvpn.tls_cipher }} | ||||
| cipher = {{ authority.openvpn.cipher }} | ||||
| auth = {{ authority.openvpn.auth }} | ||||
| {% if port %};port = {{ port }}{% else %};port = 1194{% endif %} | ||||
| {% if not proto or not proto.startswith('tcp') %};{% endif %}proto-tcp = yes | ||||
|  | ||||
| [ipv4] | ||||
| # Route only pushed subnets to tunnel | ||||
| never-default = true | ||||
| method = auto | ||||
|  | ||||
| [ipv6] | ||||
| method = auto | ||||
							
								
								
									
										23
									
								
								templates/snippets/networkmanager-strongswan.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | ||||
| [connection] | ||||
| certidude managed = true | ||||
| id = {{ session.service.title }} | ||||
| uuid = {{ uuid }} | ||||
| type = {{ vpn }} | ||||
|  | ||||
| [vpn] | ||||
| service-type = org.freedesktop.NetworkManager.strongswan | ||||
| encap = no | ||||
| virtual = yes | ||||
| method = key | ||||
| ipcomp = no | ||||
| address = {{ authority.namespace }} | ||||
| userkey = {% if key_path %}{{ key_path }}{% else %}/etc/certidude/authority/{{ authority.namespace }}/host_key.pem{% endif %} | ||||
| usercert = {% if certificate_path %}{{ certificate_path }}{% else %}/etc/certidude/authority/{{ authority.namespace }}/host_cert.pem{% endif %} | ||||
| certificate = {% if authority_path %}{{ authority_path }}{% else %}/etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem{% endif %} | ||||
| ike = {{ authority.strongswan.ike }} | ||||
| esp = {{ authority.strongswan.esp }} | ||||
| proposal = yes | ||||
|  | ||||
| [ipv4] | ||||
| method = auto | ||||
| ;route1 = 0.0.0.0/0 | ||||
							
								
								
									
										31
									
								
								templates/snippets/nginx-https-site.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,31 @@ | ||||
|  | ||||
| server { | ||||
|     listen 80; | ||||
|     server_name {{ common_name }}; | ||||
|     rewrite ^ https://{{ common_name }}\$request_uri?; | ||||
| } | ||||
|  | ||||
| server { | ||||
|     root /var/www/html; | ||||
|     add_header X-Frame-Options "DENY"; | ||||
|     add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"; | ||||
|     listen 443 ssl; | ||||
|     server_name $NAME; | ||||
|     client_max_body_size 10G; | ||||
|     ssl_certificate {{certificate_path}}; | ||||
|     ssl_certificate_key {{key_path}}; | ||||
|     ssl_client_certificate {{authority_path}}; | ||||
|  | ||||
|     # Uncomment following to enable mutual authentication with certificates | ||||
|     #ssl_crl {{revocations_path}}; | ||||
|     #ssl_verify_client on; | ||||
|  | ||||
|     location ~ \.php\$ { | ||||
|         fastcgi_split_path_info ^(.+\.php)(/.+)$; | ||||
|         fastcgi_pass unix:/run/php5-fpm.sock; | ||||
|         fastcgi_index index.php; | ||||
|         fastcgi_param REMOTE_USER \$ssl_client_s_dn_cn; | ||||
|         include fastcgi_params; | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										3
									
								
								templates/snippets/nginx-ocsp-cache.timer
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| [Timer] | ||||
| OnCalendar=*:0/15 | ||||
| Persistent=true | ||||
							
								
								
									
										31
									
								
								templates/snippets/openvpn-client.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,31 @@ | ||||
| client | ||||
| nobind | ||||
| remote {{ authority.namespace }} 1194 udp | ||||
| remote {{ authority.namespace }} 443 tcp | ||||
| proto udp | ||||
| port 1194 | ||||
| tls-version-min {{ authority.openvpn.tls_version_min }} | ||||
| tls-cipher {{ authority.openvpn.tls_cipher }} | ||||
| cipher {{ authority.openvpn.cipher }} | ||||
| auth {{authority.openvpn.auth }} | ||||
| mute-replay-warnings | ||||
| reneg-sec 0 | ||||
| remote-cert-tls server | ||||
| dev tun | ||||
| persist-tun | ||||
| persist-key | ||||
| {% if ca %} | ||||
| <ca> | ||||
| {{ ca }} | ||||
| </ca> | ||||
| {% else %}ca /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem{% endif %} | ||||
| {% if key %} | ||||
| <key> | ||||
| {{ key }} | ||||
| </key> | ||||
| {% else %}key /etc/certidude/authority/{{ authority.namespace }}/host_key.pem{% endif %} | ||||
| {% if cert %} | ||||
| <cert> | ||||
| {{ cert }} | ||||
| </cert> | ||||
| {% else %}cert /etc/certidude/authority/{{ authority.namespace }}/host_cert.pem{% endif %} | ||||
							
								
								
									
										20
									
								
								templates/snippets/openvpn-client.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,20 @@ | ||||
| # Install packages on Ubuntu & Fedora | ||||
| which apt && apt install openvpn | ||||
| which dnf && dnf install openvpn | ||||
|  | ||||
| # Create OpenVPN configuration file | ||||
| cat > /etc/openvpn/{{ session.authority.namespace }}.conf << EOF | ||||
| {% include "snippets/openvpn-client.conf" %} | ||||
| EOF | ||||
|  | ||||
| # Restart OpenVPN service | ||||
| systemctl restart openvpn | ||||
| {# | ||||
|  | ||||
| Some notes: | ||||
|  | ||||
| - Ubuntu 16.04 ships OpenVPN 2.3 which doesn't support AES-128-GCM | ||||
| - NetworkManager's OpenVPN profile importer doesn't understand multiple remotes | ||||
| - Tunnelblick and OpenVPN Connect apps don't have a method to update CRL | ||||
|  | ||||
| #} | ||||
							
								
								
									
										47
									
								
								templates/snippets/request-client.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,47 @@ | ||||
| # Generate keypair and submit CSR | ||||
| {% if common_name %}$NAME = "{{ common_name }}" | ||||
| {% else %}$NAME = $env:computername.toLower() | ||||
| {% endif %} | ||||
| @" | ||||
| [NewRequest] | ||||
| Subject = "CN=$NAME" | ||||
| Exportable = FALSE | ||||
| KeySpec = 1 | ||||
| KeyUsage = 0xA0 | ||||
| MachineKeySet = True | ||||
| ProviderType = 12 | ||||
| RequestType = PKCS10 | ||||
| {% if authority.certificate.algorithm == "ec" %}ProviderName = "Microsoft Software Key Storage Provider" | ||||
| KeyAlgorithm = ECDSA_P384 | ||||
| {% else %}ProviderName = "Microsoft RSA SChannel Cryptographic Provider" | ||||
| KeyLength = 2048 | ||||
| {% endif %}"@ | Out-File req.inf | ||||
| C:\Windows\system32\certreq.exe -new -f -q req.inf host_csr.pem | ||||
| Invoke-WebRequest `{% if token %} | ||||
|   -Uri 'https://{{ authority.namespace }}:8443/api/token/?token={{ token }}' ` | ||||
|   -Method PUT `{% else %} | ||||
|   -Uri 'https://{{ authority.namespace }}:8443/api/request/?wait=yes&autosign=yes' ` | ||||
|   -Method POST `{% endif %} | ||||
|   -TimeoutSec 900 ` | ||||
|   -InFile host_csr.pem ` | ||||
|   -ContentType application/pkcs10 ` | ||||
|   -MaximumRedirection 3 -OutFile host_cert.pem | ||||
|  | ||||
| # Import certificate | ||||
| Import-Certificate -FilePath host_cert.pem -CertStoreLocation Cert:\LocalMachine\My | ||||
| {# | ||||
|  | ||||
| On Windows 7 the Import-Certificate cmdlet is missing, | ||||
| but certutil.exe can be used instead: | ||||
|  | ||||
| C:\Windows\system32\certutil.exe -addstore My host_cert.pem | ||||
|  | ||||
| Everything seems to work except after importing the certificate | ||||
| it is not properly associated with the private key, | ||||
| that means "You have private key that corresponds to this certificate" is not | ||||
| shown under "Valid from ... to ..." in MMC. | ||||
| This results in error code 13806 during IKEv2 handshake and error message | ||||
| "IKE failed to find valid machine certificate" | ||||
|  | ||||
| #} | ||||
|  | ||||
							
								
								
									
										11
									
								
								templates/snippets/request-client.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| # Use short hostname as common name | ||||
| test -e /sbin/uci && NAME=$(uci get system.@system[0].hostname) | ||||
| test -e /bin/hostname && NAME=$(hostname) | ||||
| test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname) | ||||
|  | ||||
| {% include "snippets/request-common.sh" %} | ||||
| # Submit CSR and save signed certificate | ||||
| curl --cert-status -f -L -H "Content-type: application/pkcs10" \ | ||||
|   --data-binary @/etc/certidude/authority/{{ authority.namespace }}/host_req.pem \ | ||||
|   -o /etc/certidude/authority/{{ authority.namespace }}/host_cert.pem \ | ||||
|   'http://{{ authority.namespace }}/api/request/?wait=yes&autosign=yes' | ||||
							
								
								
									
										19
									
								
								templates/snippets/request-common.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | ||||
| # Create directories | ||||
| mkdir -p /etc/certidude/authority/{{ authority.namespace }} | ||||
|  | ||||
| # Delete CA certificate if checksum doesn't match | ||||
| echo {{ authority.certificate.md5sum }} /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem | md5sum -c \ | ||||
|  || rm -fv /etc/certidude/authority/{{ authority.namespace }}/*.pem | ||||
| {% include "snippets/store-authority.sh" %} | ||||
| {% include "snippets/update-trust.sh" %} | ||||
| # Generate private key | ||||
| test -e /etc/certidude/authority/{{ authority.namespace }}/host_key.pem \ | ||||
|  || {% if authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \ | ||||
|  -out /etc/certidude/authority/{{ authority.namespace }}/host_key.pem{% else %}openssl genrsa \ | ||||
|  -out /etc/certidude/authority/{{ authority.namespace }}/host_key.pem 2048{% endif %} | ||||
| test -e /etc/certidude/authority/{{ authority.namespace }}/host_req.pem \ | ||||
|  || openssl req -new -sha384 -subj "/CN=$NAME" \ | ||||
|  -key /etc/certidude/authority/{{ authority.namespace }}/host_key.pem \ | ||||
|  -out /etc/certidude/authority/{{ authority.namespace }}/host_req.pem | ||||
| echo "If CSR submission fails, you can copy paste it to Certidude:" | ||||
| cat /etc/certidude/authority/{{ authority.namespace }}/host_req.pem | ||||
							
								
								
									
										7
									
								
								templates/snippets/request-server.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | ||||
| # Use fully qualified name | ||||
| test -e /sbin/uci && NAME=$(nslookup $(uci get network.wan.ipaddr) |  grep "name =" | head -n1 | cut -d "=" -f 2 | xargs) | ||||
| test -e /bin/hostname && NAME=$(hostname -f) | ||||
| test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname) | ||||
|  | ||||
| {% include "snippets/request-common.sh" %} | ||||
| {% include "snippets/submit-request-wait.sh" %} | ||||
							
								
								
									
										11
									
								
								templates/snippets/setup-ocsp-caching.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| # See more on http://unmitigatedrisk.com/?p=241 why we're doing this | ||||
| cat << EOF > /etc/systemd/system/nginx-ocsp-cache.service | ||||
| {% include "snippets/nginx-ocsp-cache.service" %}EOF | ||||
|  | ||||
| cat << EOF > /etc/systemd/system/nginx-ocsp-cache.timer | ||||
| {% include "snippets/nginx-ocsp-cache.timer" %}EOF | ||||
|  | ||||
| systemctl enable nginx-ocsp-cache.service | ||||
| systemctl enable nginx-ocsp-cache.timer | ||||
| systemctl start nginx-ocsp-cache.service | ||||
| systemctl start nginx-ocsp-cache.timer | ||||
							
								
								
									
										5
									
								
								templates/snippets/store-authority.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | ||||
| # Save CA certificate | ||||
| mkdir -p /etc/certidude/authority/{{ authority.namespace }}/ | ||||
| test -e /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem \ | ||||
|  || cat << EOF > /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem | ||||
| {{ authority.certificate.blob }}EOF | ||||
							
								
								
									
										28
									
								
								templates/snippets/strongswan-client.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,28 @@ | ||||
| cat > /etc/ipsec.conf << EOF | ||||
| config setup | ||||
|     strictcrlpolicy=yes | ||||
|  | ||||
| ca {{ authority.namespace }} | ||||
|     auto=add | ||||
|     cacert=/etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem | ||||
|  | ||||
| conn client-to-site | ||||
|     auto=start | ||||
|     right={{ authority.namespace }} | ||||
|     rightsubnet=0.0.0.0/0 | ||||
|     rightca="{{ session.authority.certificate.distinguished_name }}" | ||||
|     left=%defaultroute | ||||
|     leftcert=/etc/certidude/authority/{{ authority.namespace }}/host_cert.pem | ||||
|     leftsourceip=%config | ||||
|     leftca="{{ session.authority.certificate.distinguished_name }}" | ||||
|     keyexchange=ikev2 | ||||
|     keyingtries=%forever | ||||
|     dpdaction=restart | ||||
|     closeaction=restart | ||||
|     ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! | ||||
|     esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! | ||||
| EOF | ||||
|  | ||||
| echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ authority.namespace }}.pem" > /etc/ipsec.secrets | ||||
|  | ||||
| ipsec restart | ||||
							
								
								
									
										17
									
								
								templates/snippets/strongswan-patching.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | ||||
| # Install packages on Ubuntu & Fedora, patch Fedora paths | ||||
| which apt && apt install strongswan | ||||
| which dnf && dnf install strongswan | ||||
| test -e /etc/strongswan && test -e /etc/ipsec.conf || ln -s strongswan/ipsec.conf /etc/ipsec.conf | ||||
| test -e /etc/strongswan && test -e /etc/ipsec.d || ln -s strongswan/ipsec.d /etc/ipsec.d | ||||
| test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets | ||||
|  | ||||
| # Set SELinux context | ||||
| chcon --type=home_cert_t /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem /etc/ipsec.d/cacerts/{{ authority.namespace }}.pem | ||||
| chcon --type=home_cert_t  /etc/certidude/authority/{{ authority.namespace }}/host_cert.pem /etc/ipsec.d/certs/{{ authority.namespace }}.pem | ||||
| chcon --type=home_cert_t  /etc/certidude/authority/{{ authority.namespace }}/host_key.pem /etc/ipsec.d/private/{{ authority.namespace }}.pem | ||||
|  | ||||
| # Patch AppArmor | ||||
| cat << EOF > /etc/apparmor.d/local/usr.lib.ipsec.charon | ||||
| /etc/certidude/authority/** r, | ||||
| EOF | ||||
| systemctl restart apparmor | ||||
							
								
								
									
										6
									
								
								templates/snippets/submit-request-wait.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| # Submit CSR and save signed certificate | ||||
| curl --cert-status -f -L -H "Content-type: application/pkcs10" \ | ||||
|     --cacert /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem \ | ||||
|     --data-binary @/etc/certidude/authority/{{ authority.namespace }}/host_req.pem \ | ||||
|     -o /etc/certidude/authority/{{ authority.namespace }}/host_cert.pem \ | ||||
|     'https://{{ authority.namespace }}:8443/api/request/?wait=yes' | ||||
							
								
								
									
										4
									
								
								templates/snippets/update-trust.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| # Install CA certificate | ||||
| @" | ||||
| {{ authority.certificate.blob }}"@ | Out-File ca_cert.pem | ||||
| Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root | ||||
							
								
								
									
										18
									
								
								templates/snippets/update-trust.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,18 @@ | ||||
| # Insert into Fedora trust store. Applies to curl, Firefox, Chrome, Chromium | ||||
| test -e /etc/pki/ca-trust/source/anchors \ | ||||
|  && ln -s /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem /etc/pki/ca-trust/source/anchors/{{ authority.namespace }} \ | ||||
|  && update-ca-trust | ||||
|  | ||||
| # Insert into Ubuntu trust store, only applies to curl | ||||
| test -e /usr/local/share/ca-certificates/ \ | ||||
|  && ln -f -s /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem /usr/local/share/ca-certificates/{{ authority.namespace }}.crt \ | ||||
|  && update-ca-certificates | ||||
|  | ||||
| # Patch Firefox trust store on Ubuntu | ||||
| if [ -d /usr/lib/firefox ]; then | ||||
|   if [ ! -h /usr/lib/firefox/libnssckbi.so ]; then | ||||
|     apt install -y p11-kit p11-kit-modules | ||||
|     mv /usr/lib/firefox/libnssckbi.so /usr/lib/firefox/libnssckbi.so.bak | ||||
|     ln -s /usr/lib/x86_64-linux-gnu/pkcs11/p11-kit-trust.so /usr/lib/firefox/libnssckbi.so | ||||
|   fi | ||||
| fi | ||||
							
								
								
									
										36
									
								
								templates/snippets/windows.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,36 @@ | ||||
| [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 | ||||
|  | ||||
| {% include "snippets/update-trust.ps1" %} | ||||
|  | ||||
| {% include "snippets/request-client.ps1" %} | ||||
|  | ||||
| # Set up IPSec VPN tunnel to {{ authority.namespace }} | ||||
| Remove-VpnConnection -AllUserConnection -Force "IPSec to {{ authority.namespace }}" | ||||
| Add-VpnConnection ` | ||||
|     -Name "IPSec to {{ authority.namespace }}" ` | ||||
|     -ServerAddress {{ authority.namespace }} ` | ||||
|     -AuthenticationMethod MachineCertificate ` | ||||
|     -EncryptionLevel Maximum ` | ||||
|     -SplitTunneling ` | ||||
|     -TunnelType ikev2 ` | ||||
|     -PassThru -AllUserConnection | ||||
|  | ||||
| # Harden VPN configuration | ||||
| Set-VpnConnectionIPsecConfiguration ` | ||||
|     -ConnectionName "IPSec to {{ authority.namespace }}" ` | ||||
|     -AuthenticationTransformConstants GCMAES128 ` | ||||
|     -CipherTransformConstants GCMAES128 ` | ||||
|     -EncryptionMethod AES256 ` | ||||
|     -IntegrityCheckMethod SHA384 ` | ||||
|     -DHGroup {% if authority.certificate.algorithm == "ec" %}ECP384{% else %}Group14{% endif %} ` | ||||
|     -PfsGroup {% if authority.certificate.algorithm == "ec" %}ECP384{% else %}PFS2048{% endif %} ` | ||||
|     -PassThru -AllUserConnection -Force | ||||
|  | ||||
| {# | ||||
| AuthenticationTransformConstants - ESP integrity algorithm, one of: None MD596 SHA196 SHA256128 GCMAES128 GCMAES192 GCMAES256 | ||||
| CipherTransformConstants - ESP symmetric cipher, one of: DES DES3 AES128 AES192 AES256 GCMAES128 GCMAES192 GCMAES256 | ||||
| EncryptionMethod - IKE symmetric cipher, one of: DES DES3 AES128 AES192 AES256 | ||||
| IntegrityCheckMethod - IKE hash algorithm, one of: MD5 SHA196 SHA256 SHA384 | ||||
| DHGroup = IKE key exchange, one of: None Group1 Group2 Group14 ECP256 ECP384 Group24 | ||||
| PfsGroup = ESP key exchange, one of: None PFS1 PFS2 PFS2048 ECP256 ECP384 PFSMM PFS24 | ||||
| #} | ||||
							
								
								
									
										3
									
								
								templates/views/attributes.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| {% for key, value in certificate.attributes %} | ||||
| <span class="badge badge-info" title="{{ key }}={{ value }}">{{ value }}</span> | ||||
| {% endfor %} | ||||
							
								
								
									
										270
									
								
								templates/views/authority.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,270 @@ | ||||
| <div class="modal fade" id="request_submission_modal" role="dialog"> | ||||
|   <div class="modal-dialog modal-lg"> | ||||
|     <div class="modal-content"> | ||||
|       <div class="modal-header"> | ||||
|         <button type="button" class="close" data-dismiss="modal">×</button> | ||||
|         <h4 class="modal-title">Request submission</h4> | ||||
|       </div> | ||||
|       <div class="modal-body"> | ||||
|         <ul class="nav nav-pills" id="myTab" role="tablist"> | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link active" id="home-tab" data-toggle="tab" href="#snippet-certidude" role="tab" aria-controls="certidude" aria-selected="true">Certidude</a> | ||||
|           </li> | ||||
|  | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link" id="profile-tab" data-toggle="tab" href="#snippet-windows" role="tab" aria-controls="windows" aria-selected="false">Windows</a> | ||||
|           </li> | ||||
|  | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-unix" role="tab" aria-controls="unix" aria-selected="false">UNIX</a> | ||||
|           </li> | ||||
|  | ||||
|           {% if "openvpn" in session.service.protocols %} | ||||
|             <li class="nav-item"> | ||||
|               <a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-openvpn" role="tab" aria-controls="openvpn" aria-selected="false">OpenVPN</a> | ||||
|             </li> | ||||
|           {% endif %} | ||||
|  | ||||
|           {% if "ikev2" in session.service.protocols %} | ||||
|             <li class="nav-item"> | ||||
|               <a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-strongswan" role="tab" aria-controls="strongswan" aria-selected="false">StrongSwan</a> | ||||
|             </li> | ||||
|           {% endif %} | ||||
|  | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-lede" role="tab" aria-controls="lede" aria-selected="false">LEDE</a> | ||||
|           </li> | ||||
|  | ||||
|           <li class="nav-item"> | ||||
|             <a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-copypaste" role="tab" aria-controls="copypaste" aria-selected="false">Copypasta</a> | ||||
|           </li> | ||||
|  | ||||
|         </ul> | ||||
|         <div class="tab-content" id="myTabContent"> | ||||
|           <!-- Certidude client --> | ||||
|           <div class="tab-pane fade show active" id="snippet-certidude" role="tabpanel" aria-labelledby="certidude"> | ||||
|             <p>On Ubuntu or Fedora:</p> | ||||
|             <div class="highlight"> | ||||
|               <pre class="code"><code>{% include "snippets/certidude-client.sh" %}</code></pre> | ||||
|             </div> | ||||
|           </div> | ||||
|  | ||||
|           <!-- Windows --> | ||||
|           <div class="tab-pane fade" id="snippet-windows" role="tabpanel" aria-labelledby="windows"> | ||||
|             <p>On Windows execute following PowerShell script</p> | ||||
|             {% if "ikev2" in session.service.protocols %} | ||||
|               <div class="highlight"><pre class="code"><code>{% include "snippets/windows.ps1" %}</code></pre></div> | ||||
|             {% endif %} | ||||
|           </div> | ||||
|  | ||||
|           <!-- UNIX-like --> | ||||
|           <div class="tab-pane fade" id="snippet-unix" role="tabpanel" aria-labelledby="unix"> | ||||
|             <p>For client certificates generate key pair and submit the signing request with common name set to short hostname:</p> | ||||
|             <div class="highlight"> | ||||
|               <pre class="code"><code>{% include "snippets/request-client.sh" %}</code></pre> | ||||
|             </div> | ||||
|           </div> | ||||
|  | ||||
|           <!-- OpenVPN as client --> | ||||
|           <div class="tab-pane fade" id="snippet-openvpn" role="tabpanel" aria-labelledby="openvpn"> | ||||
|             <p>First acquire certificates using the snippet above.</p> | ||||
|             <p>Then install software:</p> | ||||
|             <div class="highlight"><pre class="code"><code>{% include "snippets/openvpn-client.sh" %}</code></pre></div> | ||||
|           </div> | ||||
|  | ||||
|           <!-- StrongSwan as client --> | ||||
|           <div class="tab-pane fade" id="snippet-strongswan" role="tabpanel" aria-labelledby="strongswan"> | ||||
|             <p>First acquire certificates using the snippet above.</p> | ||||
|  | ||||
|             <p>Then install software:</p> | ||||
|             <div class="highlight"> | ||||
|               <pre class="code"><code>{% include "snippets/strongswan-patching.sh" %}</code></pre> | ||||
|             </div> | ||||
|  | ||||
|             <p>To configure StrongSwan as roadwarrior:</p> | ||||
|             <div class="highlight"><pre class="code"><code>{% include "snippets/strongswan-client.sh" %}</code></pre></div> | ||||
|           </div> | ||||
|  | ||||
|           <!-- Copy & paste --> | ||||
|           <div class="tab-pane fade" id="snippet-copypaste" role="tabpanel" aria-labelledby="copypaste"> | ||||
|             <p>Use whatever tools you have available on your platform to generate | ||||
|             keypair and just paste ASCII armored PEM file contents here and hit submit:</p> | ||||
|  | ||||
|             <form action="/api/request/" method="post"> | ||||
|               <textarea id="request_body" style="width:100%; min-height: 10em;" | ||||
|                 placeholder="-----BEGIN CERTIFICATE REQUEST-----"></textarea> | ||||
|               <div class="modal-footer"> | ||||
|                 <div class="btn-group"> | ||||
|                   <button type="button" onclick="onSubmitRequest();" class="btn btn-primary"><i class="fa fa-upload"></i> Submit</button> | ||||
|                   <button type="button" class="btn btn-secondary" data-dismiss="modal"><i class="fa fa-ban"></i> Close</button> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </form> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <div class="modal fade" id="revocation_list_modal" role="dialog"> | ||||
|   <div class="modal-dialog modal-lg"> | ||||
|     <div class="modal-content"> | ||||
|       <div class="modal-header"> | ||||
|         <button type="button" class="close" data-dismiss="modal">×</button> | ||||
|         <h4 class="modal-title">Revocation lists</h4> | ||||
|       </div> | ||||
|       <div class="modal-body"> | ||||
|         <p>To fetch <a href="http://{{ authority.namespace }}/api/revoked/">certificate revocation list</a>:</p> | ||||
| <pre><code>curl http://{{ authority.namespace }}/api/revoked/ > crl.der | ||||
| curl http://{{ authority.namespace }}/api/revoked/ -L -H "Accept: application/x-pem-file" | ||||
| curl http://{{ authority.namespace }}/api/revoked/?wait=yes -L -H "Accept: application/x-pem-file" > crl.pem</code></pre> | ||||
|       </div> | ||||
|       <div class="modal-footer"> | ||||
|         <button type="button" class="btn" data-dismiss="modal">Close</button> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <div class="row"> | ||||
|   <div class="col-sm-6 col-lg-4 col-xl-3"> | ||||
|     <h3>Signed certificates</h3> | ||||
|  | ||||
|     <p>Authority administration | ||||
|       {% if authority.certificate.organization %}of {{ authority.certificate.organization }}{% endif %} | ||||
|         allowed for | ||||
|         {% for user in session.authorization.admin_users %}<a href="mailto:{{ user.mail}}">{{ user.given_name }} {{user.surname }}</a>{% if not loop.last %}, {% endif %}{% endfor %} from {% if "0.0.0.0/0" in session.authorization.admin_subnets %}anywhere{% else %} | ||||
|         {% for subnet in session.authorization.admin_subnets %}{{ subnet }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}. | ||||
|         Authority valid from | ||||
|         <time class="timeago" datetime="{{ authority.certificate.signed }}">{{ authority.certificate.signed }}</time> | ||||
|         until | ||||
|         <time class="timeago" datetime="{{ authority.certificate.expires }}">{{ authority.certificate.expires }}</time>. | ||||
|         Authority certificate can be downloaded from <a href="/api/certificate/">here</a>. | ||||
| 		Following certificates have been signed:</p> | ||||
|  | ||||
|     <div id="signed-filter" class="btn-group-toggle" data-toggle="buttons"> | ||||
|       <label class="btn btn-primary"><input id="signed-filter-new" type="checkbox" autocomplete="off">New</label> | ||||
|       <label class="btn btn-primary active"><input id="signed-filter-online" type="checkbox" autocomplete="off" checked>Online</label> | ||||
|       <label class="btn btn-primary"><input id="signed-filter-offline" type="checkbox" autocomplete="off">Lately seen</label> | ||||
|       <label class="btn btn-primary"><input id="signed-filter-dead" type="checkbox" autocomplete="off">Gone</label> | ||||
|     </div> | ||||
|  | ||||
|     <div id="signed_certificates"> | ||||
|     {% for certificate in session.signed | sort(attribute="signed", reverse=true) %} | ||||
|       {% include "views/signed.html" %} | ||||
|     {% endfor %} | ||||
|     </div> | ||||
|  | ||||
|     <p>Showing <span id="signed-filter-counter">-</span> of total <span id="signed-total">-</span> certificates</p> | ||||
|   </div> | ||||
|   <div class="col-sm-6 col-lg-4 col-xl-3"> | ||||
|     {% if session.features.token %} | ||||
|       <h3>Tokens</h3> | ||||
|       <p>Tokens allow enrolling smartphones and third party devices.</p> | ||||
|       <ul> | ||||
|         <li>You can issue yourself a token to be used on a mobile device</li> | ||||
|         <li>Enter username to issue a token to issue a token for another user</li> | ||||
|         <li>Enter e-mail address to issue a token to guest users outside domain</li> | ||||
|       </ul> | ||||
|       <p> | ||||
|         <div class="input-group"> | ||||
|             <input id="token_username" name="username" type="text" class="form-control" placeholder="Username" aria-describedby="sizing-addon2"> | ||||
|             <input id="token_mail" name="mail" type="mail" class="form-control" placeholder="Optional e-mail" aria-describedby="sizing-addon2"> | ||||
|             <span class="input-group-btn"> | ||||
|                 <button class="btn btn-secondary" type="button" onClick="onIssueToken();"><i class="fa fa-send"></i> Send token</button> | ||||
|             </span> | ||||
|         </div> | ||||
|       </p> | ||||
|  | ||||
|       <p>Issued tokens:</p> | ||||
|       <ul class="list-group"> | ||||
|         {% for token in session.tokens %} | ||||
|           {% include "views/token.html" %} | ||||
|         {% endfor %} | ||||
|       </ul> | ||||
|  | ||||
|       <div id="token_qrcode"></div> | ||||
|     {% endif %} | ||||
|  | ||||
|     {% if session.authorization.request_subnets %} | ||||
|       <p> </p> | ||||
|       <h3>Pending requests</h3> | ||||
|  | ||||
|       <p>Use Certidude client to apply for a certificate. | ||||
|  | ||||
|       {% if not session.authorization.request_subnets %} | ||||
|          Request submission disabled. | ||||
|       {% elif "0.0.0.0/0" in session.authorization.request_subnets %} | ||||
|          Request submission is enabled. | ||||
|       {% else %} | ||||
|          Request submission allowed from | ||||
|          {% for subnet in session.authorization.request_subnets %} | ||||
|            {{ subnet }}{% if not loop.last %}, {% endif %} | ||||
|          {% endfor %}. | ||||
|       {% endif %} | ||||
|  | ||||
|       See <a href="#request_submission_modal" data-toggle="modal">here</a> for more information on manual signing request upload. | ||||
|  | ||||
|       {% if session.authorization.autosign_subnets %} | ||||
|           {% if "0.0.0.0/0" in session.authorization.autosign_subnets %} | ||||
|               All requests are automatically signed. | ||||
|           {% else %} | ||||
|              Requests from | ||||
|                   {% for subnet in session.authorization.autosign_subnets %} | ||||
|                       {{ subnet }}{% if not loop.last %}, {% endif %} | ||||
|                   {% endfor %} | ||||
|              are automatically signed. | ||||
|           {% endif %} | ||||
|       {% endif %} | ||||
|  | ||||
|       </p> | ||||
|       <div id="pending_requests"> | ||||
|         {% for request in session.requests | sort(attribute="submitted", reverse=true) %} | ||||
|           {% include "views/request.html" %} | ||||
|         {% endfor %} | ||||
|       </div> | ||||
|     {% endif %} | ||||
|  | ||||
|     {% if session.builder.profiles %} | ||||
|       <h3>LEDE imagebuilder</h3> | ||||
|       <p>Hit a link to generate machine specific image. Note that this might take couple minutes to finish.</p> | ||||
|       <ul> | ||||
|         {% for name, title, filename in session.builder.profiles %} | ||||
|           <li><a href="/api/builder/{{ name }}/{{ filename }}">{{ title }}</a></li> | ||||
|         {% endfor %} | ||||
|       </ul> | ||||
|     {% endif %} | ||||
|  | ||||
|   </div> | ||||
|   <div class="col-sm-6 col-lg-4 col-xl-3"> | ||||
|  | ||||
|     <h3>Revoked certificates</h3> | ||||
|     <p>Following certificates have been revoked{% if session.features.crl %}, for more information click | ||||
|     <a href="#revocation_list_modal" data-toggle="modal">here</a>{% endif %}.</p> | ||||
|  | ||||
|     {% for certificate in session.revoked | sort(attribute="revoked", reverse=true) %} | ||||
|       {% include "views/revoked.html" %} | ||||
|     {% endfor %} | ||||
|   </div> | ||||
|   <div id="column-log" class="col-sm-6 col-lg-4 col-xl-3 hidden-lg-down"> | ||||
|     <div class="loader-container"> | ||||
|       <div class="loader"></div> | ||||
|       <p>Loading logs, this might take a while...</p> | ||||
|     </div> | ||||
|     <div class="content" style="display:none;"> | ||||
|       <h3>Log</h3> | ||||
|       <div class="btn-group-toggle" data-toggle="buttons"> | ||||
|         <label class="btn btn-primary active"><input id="log-level-critical" type="checkbox" autocomplete="off" checked>Critical</label> | ||||
|         <label class="btn btn-primary active"><input id="log-level-error" type="checkbox" autocomplete="off" checked>Error</label> | ||||
|         <label class="btn btn-primary active"><input id="log-level-warning" type="checkbox" autocomplete="off" checked>Warn</label> | ||||
|         <label class="btn btn-primary active"><input id="log-level-info" type="checkbox" autocomplete="off" checked>Info</label> | ||||
|         <label class="btn btn-primary"><input id="log-level-debug" type="checkbox" autocomplete="off">Debug</label> | ||||
|       </div> | ||||
|       <ul id="log-entries" class="list-group"> | ||||
|       </ul> | ||||
|       <p>Click here to load more entries</p> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
							
								
								
									
										31
									
								
								templates/views/configuration.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,31 @@ | ||||
|  | ||||
| <h1>Create a rule</h1> | ||||
| <p> | ||||
|  | ||||
|     <datalist id="tag_autocomplete"> | ||||
|  | ||||
|     </datalist> | ||||
|  | ||||
|      <span>Filter</span> | ||||
|       <select id="tags_autocomplete"></select> | ||||
|       attaches attribute | ||||
|       <select> | ||||
|         {% include 'views/tagtypes.html' %} | ||||
|       </select> | ||||
|       <span contenteditable>something</span> | ||||
|       <button>Add rule</button> | ||||
| </p> | ||||
|  | ||||
| {% for grouper, items in configuration | groupby('tag_id') %} | ||||
|  | ||||
| <h1>Filter {{ items[0].match_key }} is {{ items[0].match_value }}</h1> | ||||
| <ul> | ||||
|  | ||||
| {% for item in items %} | ||||
|     <li>Attach {{ item.key }} attribute {{ item.value }}</li> | ||||
| {% endfor %} | ||||
| </ul> | ||||
|  | ||||
| {% endfor %} | ||||
|  | ||||
|  | ||||
							
								
								
									
										278
									
								
								templates/views/enroll.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,278 @@ | ||||
| <!-- https://wiki.strongswan.org/projects/strongswan/wiki/AppleIKEv2Profile#Certificate-authentication --> | ||||
|  | ||||
| <!-- | ||||
|  | ||||
| Browser status | ||||
|  | ||||
| - Edge doesn't work because they think data: urls are insecure | ||||
| - iphone QR code scanner's webview is constrained, cant download data: links | ||||
| - outlook.com via iphone mail client works | ||||
| - android gmail app works | ||||
| - chrome works | ||||
| - firefox works | ||||
|  | ||||
| OS/soft status | ||||
|  | ||||
| - OpenVPN works on everything | ||||
| - StrongSwan app works on Android | ||||
| - NetworkManager doesn't support importing .sswan files yet, so no IPSec support for Ubuntu or Fedora here yet | ||||
|  | ||||
| --> | ||||
|  | ||||
| <div id="enroll" class="row"> | ||||
|   <div class="loader-container"> | ||||
|     <div class="loader"></div> | ||||
|     <p>Generating RSA keypair, this will take a while...</p> | ||||
|   </div> | ||||
|  | ||||
|   <div class="col-sm-12 mt-3 edge-broken" style="display:none;"> | ||||
|     <!-- https://stackoverflow.com/questions/33154646/data-uri-link-a-href-data-doesnt-work-in-microsoft-edge?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa --> | ||||
|     Microsoft Edge not supported, open the link with Chrome or Firefox | ||||
|   </div> | ||||
|  | ||||
|   <div class="col-sm-12 mt-3 option ubuntu linux openvpn"> | ||||
|     <div class="card"> | ||||
|       <div class="card-block"> | ||||
|         <h3 class="card-title">Ubuntu 16.04+</h3> | ||||
|         <p class="card-text">Install OpenVPN plugin for NetworkManager by executing following two command in the terminal: | ||||
|  | ||||
|         <pre><code># Ubuntu 16.04 ships with older OpenVPN 2.3, to support newer ciphers add OpenVPN's repo | ||||
| if [ $(lsb_relase -cs) == "xenial" ]; then | ||||
|   wget -O - https://swupdate.openvpn.net/repos/repo-public.gpg|apt-key add - | ||||
|   echo "deb http://build.openvpn.net/debian/openvpn/release/2.4 xenial main" > /etc/apt/sources.list.d/openvpn-aptrepo.list | ||||
|   apt update | ||||
|   apt install openvpn | ||||
| fi | ||||
|  | ||||
| sudo apt install -y network-manager-openvpn-gnome | ||||
| sudo systemctl restart network-manager | ||||
| </code></pre> | ||||
|  | ||||
|         <p> | ||||
|           <a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a> | ||||
|           <button class="btn btn-secondary" type="button" data-toggle="collapse" data-target="#ubuntu-screenshots" aria-expanded="false" aria-controls="ubuntu-screenshots"> | ||||
|             Screenshots | ||||
|           </button> | ||||
|         </p> | ||||
|  | ||||
|         <div class="collapse" id="ubuntu-screenshots"> | ||||
|             <p>Open up network connections:</p> | ||||
|             <p><img src="/img/ubuntu-01-edit-connections.png"/></p> | ||||
|             <p>Hit <i>Add button</i>:</p> | ||||
|             <p><img src="/img/ubuntu-02-network-connections.png"/></p> | ||||
|             <p>Select <i>Import a saved VPN configuration...</i>:</p> | ||||
|             <p><img src="/img/ubuntu-03-import-saved-config.png"/></p> | ||||
|             <p>Select downloaded file:</p> | ||||
|             <p><img src="/img/ubuntu-04-select-file.png"/></p> | ||||
|             <p>Once profile is successfully imported following dialog appears:</p> | ||||
|             <p><img src="/img/ubuntu-05-profile-imported.png"/></p> | ||||
|             <p>By default all traffic is routed via VPN gateway, route only intranet subnets to the gateway select <i>Routes...</i> under <i>IPv4 Settings</i>:</p> | ||||
|             <p><img src="/img/ubuntu-06-ipv4-settings.png"/></p> | ||||
|             <p>Check <i>Use this connection only for resources on its network</i>:</p> | ||||
|             <p><img src="/img/ubuntu-07-disable-default-route.png"/></p> | ||||
|             <p>To activate the connection select it under <i>VPN Connections</i>:</p> | ||||
|             <p><img src="/img/ubuntu-08-activate-connection.png"/></p> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|   <div class="col-sm-12 mt-3 option ubuntu linux openvpn advanced"> | ||||
|     <div class="card"> | ||||
|       <div class="card-block"> | ||||
|         <h3 class="card-title">Ubuntu 18.04+ (advanced)</h3> | ||||
|         <p class="card-text">Copy-paste follownig to terminal as root user:</p> | ||||
|         <pre><code>{% include "snippets/request-client.sh" %} | ||||
| cat << EOF > '/etc/NetworkManager/system-connections/OpenVPN to {{ authority.namespace }}' | ||||
| {% include "snippets/networkmanager-openvpn.conf" %}EOF | ||||
|  | ||||
| nmcli con reload | ||||
| </code></pre> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|   <div class="col-sm-12 mt-3 option ubuntu linux ikev2 advanced"> | ||||
|     <div class="card"> | ||||
|       <div class="card-block"> | ||||
|         <h3 class="card-title">Ubuntu 18.04+ (advanced)</h3> | ||||
|         <p class="card-text">Copy-paste follownig to terminal as root user:</p> | ||||
|         <pre><code>{% include "snippets/request-client.sh" %} | ||||
| cat << EOF > '/etc/NetworkManager/system-connections/IPSec to {{ authority.namespace }}' | ||||
| {% include "snippets/networkmanager-strongswan.conf" %}EOF | ||||
|  | ||||
| nmcli con reload | ||||
| </code></pre> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|  | ||||
|   <div class="col-sm-12 mt-3 option fedora linux openvpn"> | ||||
|     <div class="card"> | ||||
|       <div class="card-block"> | ||||
|         <h3 class="card-title">Fedora</h3> | ||||
|         <p class="card-text">Install OpenVPN plugin for NetworkManager by running following two commands:</p> | ||||
|         <pre><code>dnf install NetworkManager-openvpn-gnome | ||||
| systemctl restart NetworkManager</code></pre> | ||||
|           Right click in the NetworkManager icon, select network settings. Hit the + button and select <i>Import from file...</i>, select the downloaded .ovpn file. | ||||
|           Remove the .ovpn file from the Downloads folder.</p> | ||||
|         <a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|   <div class="col-sm-12 mt-3 option windows ipsec"> | ||||
|     <div class="card"> | ||||
|       <div class="card-block"> | ||||
|         <h3 class="card-title">Windows</h3> | ||||
|         <p class="card-text"> | ||||
|           Import PKCS#12 container to your machine trust store. | ||||
|           Import VPN connection profile by moving the downloaded .pbk file to | ||||
|         <pre><code>%userprofile%\AppData\Roaming\Microsoft\Network\Connections\PBK</code></pre> | ||||
|         or | ||||
|         <pre><code>C:\ProgramData\Microsoft\Network\Connections\Pbk</code></pre></p> | ||||
|         <a href="javascript:onEnroll('p12');" class="btn btn-primary">Fetch PKCS#12 container</a> | ||||
|         <a href="#" class="btn btn-secondary">Fetch IPSec IKEv2 VPN profile</a> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|   <div class="col-sm-12 mt-3 option windows ikev2"> | ||||
|     <div class="card"> | ||||
|       <div class="card-block"> | ||||
|         <h3 class="card-title">Windows</h3> | ||||
|         <p>To configure IPSec IKEv2 tunnel on Windows, open PowerShell as administrator and copy-paste following:</p> | ||||
|         <div class="highlight"><pre class="code"><code>{% include "snippets/windows.ps1" %}</code></pre></div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|   <div class="col-sm-12 mt-3 option windows openvpn"> | ||||
|     <div class="card"> | ||||
|       <div class="card-block"> | ||||
|         <h3 class="card-title">Windows</h3> | ||||
|         <p class="card-text"> | ||||
|           Install OpenVPN community edition client. | ||||
|           Move the downloaded .ovpn file to C:\Program Files\OpenVPN\config and | ||||
|           right click in the system tray on OpenVPN icon and select Connect from the menu. | ||||
|           For finishing touch adjust the file permissions so only local | ||||
|           administrator can read that file, remove regular user access to the file. | ||||
|         </p> | ||||
|         <a href="https://openvpn.net/index.php/download/community-downloads.html" class="btn btn-secondary">Get OpenVPN community edition</a> | ||||
|         <a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a> | ||||
|         <button class="btn btn-secondary" type="button" data-toggle="collapse" data-target="#windows-screenshots" aria-expanded="false" aria-controls="windows-screenshots"> | ||||
|             Screenshots | ||||
|          </button> | ||||
|  | ||||
|         <div class="collapse" id="windows-screenshots"> | ||||
|           <p>Download OpenVPN from the link supplied above:</p> | ||||
|           <p><img src="/img/windows-01-download-openvpn.png"/></p> | ||||
|  | ||||
|           <p>Install OpenVPN:</p> | ||||
|           <p><img src="/img/windows-02-install-openvpn.png"/></p> | ||||
|  | ||||
|           <p>Move the configuraiton file downloaded from the second button above:</p> | ||||
|           <p><img src="/img/windows-03-move-config-file.png"/></p> | ||||
|  | ||||
|           <p>Connect from system tray:</p> | ||||
|           <p><img src="/img/windows-04-connect.png"/></p> | ||||
|  | ||||
|           <p>Connection is successfully configured:</p> | ||||
|           <p><img src="/img/windows-05-connected.png"/></p> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|   <div class="col-sm-12 mt-3 option mac openvpn"> | ||||
|     <div class="card"> | ||||
|       <div class="card-block"> | ||||
|         <h3 class="card-title">Mac OS X</h3> | ||||
|         <p class="card-text">Download Tunnelblick. Tap on the button above and import the profile.</p> | ||||
|         <a  href="https://tunnelblick.net/" target="_blank" class="btn btn-secondary">Get Tunnelblick</a> | ||||
|         <a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|   <div class="col-sm-12 mt-3 option iphone ipad openvpn"> | ||||
|     <div class="card"> | ||||
|       <div class="card-block"> | ||||
|         <h3 class="card-title">iPhone/iPad</h3> | ||||
|         <p class="card-text">Install OpenVPN Connect app, tap on the button below.</p> | ||||
|         <a href="https://itunes.apple.com/us/app/openvpn-connect/id590379981?mt=8" target="_blank" class="btn btn-secondary">Get OpenVPN Connect app</a> | ||||
|         <a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|   <div class="col-sm-12 mt-3 option iphone ipad ikev2"> | ||||
|     <div class="card"> | ||||
|       <div class="card-block"> | ||||
|         <h3 class="card-title">iPhone/iPad</h3> | ||||
|         <p class="card-text"> | ||||
|           Tap the button below, you'll be prompted about configuration profile, tap <i>Allow</i>. | ||||
|           Hit <i>Install</i> in the top-right corner. | ||||
|           Enter your passcode to unlock trust store. | ||||
|           Tap <i>Install</i> and confirm by hitting <i>Install</i>. | ||||
|           Where password for the certificate is prompted, enter 1234. | ||||
|           Hit <i>Done</i>. Go to <i>Settings</i>, open VPN submenu and tap on the VPN profile to connect. | ||||
|         </p> | ||||
|         <a href="javascript:onEnroll('mobileconfig');" class="btn btn-primary">Fetch IPSec IKEv2 VPN profile</a> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|   <div class="col-sm-12 mt-3 option mac ikev2"> | ||||
|     <div class="card"> | ||||
|       <div class="card-block"> | ||||
|         <h3 class="card-title">Mac OS X</h3> | ||||
|         <p class="card-text"> | ||||
|           Click on the button below, you'll be prompted about configuration profile, tap <i>Allow</i>. | ||||
|           Hit <i>Install</i> in the top-right corner. | ||||
|           Enter your passcode to unlock trust store. | ||||
|           Tap <i>Install</i> and confirm by hitting <i>Install</i>. | ||||
|           Where password for the certificate is prompted, enter 1234. | ||||
|           Hit <i>Done</i>. Go to <i>Settings</i>, open VPN submenu and tap on the VPN profile to connect. | ||||
|         </p> | ||||
|         <a href="javascript:onEnroll('mobileconfig');" class="btn btn-primary">Fetch VPN profile</a> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|   <div class="col-sm-12 mt-3 option android openvpn"> | ||||
|     <div class="card"> | ||||
|       <div class="card-block"> | ||||
|         <h3 class="card-title">Android</h3> | ||||
|         <p class="card-text">Intall OpenVPN Connect app on your device. | ||||
|           Tap on the downloaded .ovpn file, OpenVPN Connect should prompt for import. | ||||
|           Hit <i>Accept</i> and then <i>Connect</i>. | ||||
|           Remember to delete any remaining .ovpn files under the <i>Downloads</i>. | ||||
|         </p> | ||||
|         <a href="https://play.google.com/store/apps/details?id=net.openvpn.openvpn" target="_blank" class="btn btn-secondary">Get OpenVPN Connect app</a> | ||||
|         <a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|   <div class="col-sm-12 mt-3 option android ikev2"> | ||||
|     <div class="card"> | ||||
|       <div class="card-block"> | ||||
|         <h3 class="card-title">Android</h3> | ||||
|         <p class="card-text"> | ||||
|           Install strongSwan Client app on your device. | ||||
|           Tap on the downloaded .sswan file, StrongSwan Client should prompt for import. | ||||
|           Hit <i>Import certificate from VPN profile</i> and then <i>Import</i> in the top-right corner. | ||||
|           Remember to delete any remaining .sswan files under the <i>Downloads</i>. | ||||
|         </p> | ||||
|         <a href="https://play.google.com/store/apps/details?id=org.strongswan.android" class="btn btn-secondary">Get strongSwan VPN Client app</a> | ||||
|         <a href="javascript:onEnroll('sswan');" class="btn btn-primary">Fetch StrongSwan profile</a> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="col-sm-12 mt-3 option any"> | ||||
|     <a href="javascript:$('.option').show();">I did't find an appropriate option for me, show all options</a> | ||||
|   </div> | ||||
|  | ||||
| </div> | ||||
							
								
								
									
										2
									
								
								templates/views/error.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| <h1>{{ message.title }}</h1> | ||||
| <p>{{ message.description }}</p> | ||||
							
								
								
									
										14
									
								
								templates/views/insecure.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | ||||
| <p>You're viewing this page over insecure channel. | ||||
|   You can give it a try and <a href="https://{{ authority.hostname }}">connect over HTTPS</a>, | ||||
|   if that succeeds all subsequents accesses of this page will go over HTTPS. | ||||
| </p> | ||||
| <p> | ||||
|   Click <a href="/api/certificate">here</a> to fetch the certificate of this authority. | ||||
|   Alternatively install certificate on Fedora or Ubuntu with following copy-pastable snippet: | ||||
| </p> | ||||
|  | ||||
| <div class="highlight"> | ||||
| <pre class="code"><code>{% include "snippets/store-authority.sh" %} | ||||
| {% include "snippets/update-trust.sh" %}</code></pre> | ||||
| </div> | ||||
|  | ||||
							
								
								
									
										7
									
								
								templates/views/lease.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | ||||
| Last seen | ||||
| <time class="timeago" datetime="{{ certificate.lease.last_seen }}">{{ certificate.lease.last_seen }}</time> | ||||
| at | ||||
| <a target="_blank" href="http://{{ certificate.lease.inner_address }}">{{ certificate.lease.inner_address }}</a>{% if certificate.lease.outer_address %} | ||||
| from | ||||
| <a target="{{ certificate.lease.outer_address }}" href="https://geoiplookup.net/ip/{{ certificate.lease.outer_address }}">{{ certificate.lease.outer_address }}</a>{% endif %}. | ||||
| See some stats <a href="http://172.20.1.19:19999/host/{{ certificate.common_name }}/" target="_blank">here</a>. | ||||
							
								
								
									
										8
									
								
								templates/views/logentry.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | ||||
| <li id="log_entry_{{ entry.id }}" data-keywords="{{ entry.message }}" class="list-group-item justify-content-between filterable{% if entry.fresh %} fresh{% endif %}"> | ||||
| <span> | ||||
| <i class="fa fa-{{ entry.severity }}-circle"></i> | ||||
| {{ entry.message }} | ||||
| </span> | ||||
| <span class="badge badge-default badge-pill">{{ entry.created }}</span> | ||||
| </li> | ||||
|  | ||||
							
								
								
									
										64
									
								
								templates/views/request.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,64 @@ | ||||
| <div id="request-{{ request.id }}" class="card filterable mt-3" | ||||
|   data-keywords="{{ request.common_name }}|" data-id="{{request.id}}"> | ||||
|   <div class="card-header"> | ||||
|     {% if certificate.server %} | ||||
|       <i class="fa fa-server"></i> | ||||
|     {% else %} | ||||
|       <i class="fa fa-laptop"></i> | ||||
|     {% endif %} | ||||
|     {{ request.common_name }} | ||||
|   </div> | ||||
|   <div class="card-block"> | ||||
|     <p class="mb-1"> | ||||
|       Submitted | ||||
|       <time class="timeago" datetime="{{ request.submitted }}">Request was submitted {{ request.submitted }}</time> | ||||
|       from | ||||
|       {% if request.hostname %}{{request.hostname}} ({{request.address}}){% else %}{{request.address}}{% endif %} | ||||
|     </p> | ||||
|     <div class="btn-group"> | ||||
|       <button type="button" class="btn btn-secondary" data-toggle="collapse" data-target="#details-{{ request.sha256sum }}"><i class="fa fa-list"></i> Details</button> | ||||
|       <button type="button" class="btn btn-danger" | ||||
|         data-loading-text="<i class='fa fa-circle-o-notch fa-spin'></i> Rejecting..." | ||||
|         onclick="onRejectRequest(event, '{{ request.common_name }}', '{{ request.sha256sum }}');"> | ||||
|         <i class="fa fa-trash"></i> Reject</button> | ||||
|       <button type="button" class="btn btn-success" | ||||
|         data-loading-text="<i class='fa fa-circle-o-notch fa-spin'></i> Processing Order" | ||||
|         onclick="onSignRequest(event, '{{ request.common_name }}', '{{ request.sha256sum }}');"> | ||||
|         <i class="fa fa-thumbs-up"></i> Approve</button> | ||||
|       <button type="button" class="btn btn-success dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||||
|         <span class="sr-only">Toggle Dropdown</span> | ||||
|       </button> | ||||
|       <div class="dropdown-menu"> | ||||
|         {% for p in authority.signature.profiles %} | ||||
|           <a class="dropdown-item{% if not request.common_name.match(p.common_name) %} disabled{% endif %}" | ||||
|             {% if not request.common_name.match(p.common_name) %} title="Common name doesn't match expression {{ p.common_name }}"{% endif %} | ||||
|             href="#" onclick="javascript:$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}&profile={{ p.key }}',type:'post'});"> | ||||
|             {{ p.key }}, expires in {{ p.lifetime }} days</a> | ||||
|         {% endfor %} | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="collapse" id="details-{{ request.sha256sum }}"> | ||||
|       <p>Use following to fetch the signing request:</p> | ||||
|       <div class="bd-example"> | ||||
|         <pre><code class="language-sh" data-lang="sh">wget <a href="/api/request/{{ request.common_name }}/">http://{{ authority.namespace }}/api/request/{{ request.common_name }}/</a> | ||||
| curl -L http://{{ authority.namespace }}/api/request/{{ request.common_name }}/ \ | ||||
|   | openssl req -text -noout</code></pre> | ||||
|       </div> | ||||
|  | ||||
|       <div style="overflow: auto; max-width: 100%;"> | ||||
|         <table class="table" id="signed_certificates"> | ||||
|           <tbody> | ||||
|             <tr><th>Common name</th><td>{{ request.common_name }}</td></tr> | ||||
|             <tr><th>Submitted</th><td>{{ request.submitted | datetime }} | ||||
|             {% if request.address %}from {{ request.address }} | ||||
|             {% if request.hostname %} ({{ request.hostname }}){% endif %}{% endif %}</td></tr> | ||||
|             <tr><th>MD5</th><td>{{ request.md5sum }}</td></tr> | ||||
|             <tr><th>SHA1</th><td>{{ request.sha1sum }}</td></tr> | ||||
|             <tr><th>SHA256</th><td>{{ request.sha256sum }}</td></tr> | ||||
|           </tbody> | ||||
|         </table> | ||||
|       </div> | ||||
|  | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
							
								
								
									
										69
									
								
								templates/views/revoked.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,69 @@ | ||||
| <div id="certificate-{{ certificate.common_name | replace('@', '--') | replace('.', '-') }}" class="card filterable mt-3" | ||||
|   data-keywords="{{ certificate.common_name }}|"> | ||||
|   <div class="card-body"> | ||||
|     <div class="card-header"> | ||||
|       {% if certificate.server %} | ||||
|         <i class="fa fa-server"></i> | ||||
|       {% else %} | ||||
|         <i class="fa fa-laptop"></i> | ||||
|       {% endif %} | ||||
|       {{ certificate.common_name }} | ||||
|     </div> | ||||
|     <div class="card-block"> | ||||
|       <p> | ||||
|         Serial number {{ certificate.serial | serial }}. | ||||
|       </p> | ||||
|       <p> | ||||
|         Revoked | ||||
|         <time class="timeago" datetime="{{ certificate.revoked }}">Certificate revoked {{ certificate.revoked }}</time>. | ||||
|         Valid from {{ certificate.signed | datetime }} to {{ certificate.expired | datetime }}. | ||||
|       </p> | ||||
|  | ||||
|       <div class="btn-group"> | ||||
|         <button type="button" class="btn btn-secondary" data-toggle="collapse" data-target="#details-{{ certificate.sha256sum }}"><i class="fa fa-list"></i> Details</button> | ||||
|         <div class="btn-group"> | ||||
|           <a href="/api/signed/{{ certificate.common_name }}/" class="btn btn-secondary hidden-xs-down"><i class="fa fa-download"></i> Download</a> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="collapse" id="details-{{ certificate.sha256sum }}"> | ||||
|         <p>To fetch certificate:</p> | ||||
|  | ||||
|         <div class="bd-example"> | ||||
|           <pre><code class="language-sh" data-lang="sh">wget <a href="/api/revoked/{{ certificate.serial }}/">http://{{ authority.namespace }}/api/revoked/{{ certificate.serial }}/</a> | ||||
| curl http://{{ authority.namespace }}/api/revoked/{{ certificate.serial }}/ \ | ||||
|   | openssl x509 -text -noout</code></pre> | ||||
|         </div> | ||||
|  | ||||
|         <p>To perform online certificate status request</p> | ||||
|         <pre><code class="language-bash" data-lang="bash">curl http://{{ authority.namespace }}/api/certificate/ > session.pem | ||||
| openssl ocsp -issuer session.pem -CAfile session.pem \ | ||||
|   -url http://{{ authority.namespace }}/api/ocsp/ \ | ||||
|   -serial 0x{{ certificate.serial }}</span></code></pre> | ||||
|  | ||||
|         <p> | ||||
|           <table class="table" id="signed_certificates"> | ||||
|             <tbody> | ||||
|               <tr><th>Common name</th><td>{{ certificate.common_name }}</td></tr> | ||||
|               <tr><th>Organizational unit</th><td>{{ certificate.organizational_unit }}</td></tr> | ||||
|               <tr><th>Serial number</th><td>{{ certificate.serial }}</td></tr> | ||||
|               <tr><th>Signed</th><td>{{ certificate.signed | datetime  }} | ||||
|                 {% if certificate.signer %}, by {{ certificate.signer }}{% endif %}</td></tr> | ||||
|               <tr><th>Expired</th><td>{{ certificate.expired | datetime  }}</td></tr> | ||||
|               {% if certificate.lease %} | ||||
|               <tr><th>Lease</th><td><a href="http://{{ certificate.lease.inner_address }}">{{ certificate.lease.inner_address }}</a> at {{ certificate.lease.last_seen | datetime  }} | ||||
|                 from <a href="https://geoiptool.com/en/?ip={{ certificate.lease.outer_address }}" target="_blank">{{ certificate.lease.outer_address }}</a> | ||||
|               </td></tr> | ||||
|               {% endif %} | ||||
|  | ||||
|               <!-- | ||||
|               <tr><th>MD5</th><td>{{ certificate.md5sum }}</td></tr> | ||||
|               <tr><th>SHA1</th><td>{{ certificate.sha1sum }}</td></tr> | ||||
|               --> | ||||
|               <tr><th>SHA256</th><td>{{ certificate.sha256sum }}</td></tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </p> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
							
								
								
									
										134
									
								
								templates/views/signed.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,134 @@ | ||||
| <div id="certificate-{{ certificate.id }}" class="card filterable mt-3" | ||||
|   {% if certificate.lease %}data-last-seen={{ certificate.lease.last_seen }}{% endif %} | ||||
|   data-keywords="{{ certificate.common_name }}|{% if session.tagging %}{% for tag in certificate.tags %}{{ tag.id }}|{% endfor %}{% endif %}{% for key, value in certificate.attributes %}{{ key }}={{ value }}|{% endfor %}" data-id="{{certificate.id}}"> | ||||
|   <div class="card-header"> | ||||
|     {% if certificate.organizational_unit %} | ||||
|       <i class="fa fa-folder" aria-hidden="true"></i> | ||||
|     {{ certificate.organizational_unit }} / | ||||
|     {% endif %} | ||||
|     {% if certificate.extensions.extended_key_usage and "server_auth" in certificate.extensions.extended_key_usage %} | ||||
|       <i class="fa fa-server"></i> | ||||
|     {% else %} | ||||
|       <i class="fa fa-laptop"></i> | ||||
|     {% endif %} | ||||
|     {{ certificate.common_name }} | ||||
|   </div> | ||||
|   <div class="card-block"> | ||||
|     <p> | ||||
|       <i class="fa fa-circle"></i> | ||||
|       <span class="lease"> | ||||
|         {% if certificate.lease %} | ||||
|           {% include "views/lease.html" %} | ||||
|         {% endif %} | ||||
|       </span> | ||||
|  | ||||
|       Signed | ||||
|       <time class="timeago" datetime="{{ certificate.signed }}">Certificate was signed {{ certificate.signed }}</time>{% if certificate.signer %} by {{ certificate.signer }}{% endif %}, | ||||
|       expires | ||||
|       <time class="timeago" datetime="{{ certificate.expires }}">Certificate expires {{ certificate.expires }}</time>. | ||||
|     </p> | ||||
|     <p> | ||||
|     {% if session.tagging %} | ||||
|       <span class="tags" data-cn="{{ certificate.common_name }}"> | ||||
|         {% include "views/tags.html" %} | ||||
|       </span> | ||||
|     {% endif %} | ||||
|       <span class="attributes" data-cn="{{ certificate.common_name }}"> | ||||
|         {% include "views/attributes.html" %} | ||||
|       </span> | ||||
|     </p> | ||||
|  | ||||
|     <div class="btn-group"> | ||||
|       <button type="button" class="btn btn-secondary" data-toggle="collapse" data-target="#details-{{ certificate.sha256sum }}"><i class="fa fa-list"></i> Details</button> | ||||
|       <button type="button" class="btn btn-danger" | ||||
|        onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}',type:'delete'});"> | ||||
|        <i class="fa fa-ban"></i> Revoke</button> | ||||
|       <button type="button" class="btn btn-danger dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||||
|         <span class="sr-only">Toggle Dropdown</span> | ||||
|       </button> | ||||
|       <div class="dropdown-menu"> | ||||
|         <a class="dropdown-item" href="#" | ||||
|           onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=key_compromise',type:'delete'});">Revoke due to key compromise</a> | ||||
|         <a class="dropdown-item" href="#" | ||||
|           onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=cessation_of_operation',type:'delete'});">Revoke due to cessation of operation</a> | ||||
|         <a class="dropdown-item" href="#" | ||||
|           onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=privilege_withdrawn',type:'delete'});">Revoke due to withdrawn privilege</a> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="btn-group"> | ||||
|       {% if session.tagging %} | ||||
|         <button type="button" class="btn btn-default" onclick="onNewTagClicked(event);" data-key="other" data-cn="{{ certificate.common_name }}"> | ||||
|          <i class="fa fa-tag"></i> Tag</button> | ||||
|         <button type="button" class="btn btn-default dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||||
|           <span class="sr-only">Toggle Dropdown</span> | ||||
|         </button> | ||||
|         <div class="dropdown-menu"> | ||||
|           {% for tag_category in session.tagging %} | ||||
|           <a class="dropdown-item" href="#" data-key="{{ tag_category.name }}" data-cn="{{ certificate.common_name }}" | ||||
|             onclick="onNewTagClicked(event);">{{ tag_category.title }}</a> | ||||
|           {% endfor %} | ||||
|         </div> | ||||
|       {% endif %} | ||||
|     </div> | ||||
|  | ||||
|     <div class="collapse" id="details-{{ certificate.sha256sum }}"> | ||||
|       <p>To launch shell for this device, click <a href="#" onclick="onLaunchShell('{{ certificate.common_name }}')">here</a> | ||||
|       <p>To fetch certificate:</p> | ||||
|  | ||||
|       <div class="bd-example"> | ||||
|         <pre><code class="language-sh" data-lang="sh">wget <a href="/api/signed/{{ certificate.common_name }}/">http://{{ authority.namespace }}/api/signed/{{ certificate.common_name }}</a> | ||||
| curl -L http://{{ authority.namespace }}/api/signed/{{ certificate.common_name }}/ \ | ||||
|   | openssl x509 -text -noout</code></pre> | ||||
|       </div> | ||||
|  | ||||
|       {% if session.authorization.ocsp_subnets %} | ||||
|       {% if certificate.responder_url %} | ||||
|       <p>To perform online certificate status request{% if "0.0.0.0/0" not in session.authorization.ocsp_subnets %} | ||||
|       from whitelisted {{ session.authorization.ocsp_subnets }} subnets{% endif %}:</p> | ||||
|       <pre><code class="language-bash" data-lang="bash">curl http://{{ authority.namespace }}/api/certificate > session.pem | ||||
| openssl ocsp -issuer session.pem -CAfile session.pem \ | ||||
|   -url {{ certificate.responder_url }} \ | ||||
|   -serial 0x{{ certificate.serial }}</code></pre> | ||||
|       {% else %} | ||||
|       <p>Querying OCSP responder disabled for this certificate, see /etc/certidude/profile.conf how to enable if that's desired</p> | ||||
|       {% endif %} | ||||
|       {% endif %} | ||||
|  | ||||
|       <p>To fetch script:</p> | ||||
|       <pre><code class="language-bash" data-lang="bash">curl -L --cert-status https://{{ authority.namespace }}:8443/api/signed/{{ certificate.common_name }}/script/ \ | ||||
|     --cacert /etc/certidude/authority/{{ authority.namespace }}/ca_cert.pem \ | ||||
|     --key /etc/certidude/authority/{{ authority.namespace }}/host_key.pem \ | ||||
|     --cert /etc/certidude/authority/{{ authority.namespace }}/host_cert.pem</pre></code> | ||||
|  | ||||
|       <div style="overflow: auto; max-width: 100%;"> | ||||
|         <table class="table" id="signed_certificates"> | ||||
|           <tbody> | ||||
|             <tr><th>Common name</th><td>{{ certificate.common_name }}</td></tr> | ||||
|             <tr><th>Organizational unit</th><td>{% if certificate.organizational_unit %}{{ certificate.organizational_unit }}{% else %}-{% endif %}</td></tr> | ||||
|             <tr><th>Serial number</th><td style="word-wrap:break-word;">{{ certificate.serial | serial }}</td></tr> | ||||
|             <tr><th>Signed</th><td>{{ certificate.signed | datetime  }}{% if certificate.signer %} by {{ certificate.signer }}{% endif %}</td></tr> | ||||
|             <tr><th>Expires</th><td>{{ certificate.expires | datetime  }}</td></tr> | ||||
|             {% if certificate.lease %} | ||||
|             <tr><th>Lease</th><td><a href="http://{{ certificate.lease.inner_address }}">{{ certificate.lease.inner_address }}</a> at {{ certificate.lease.last_seen | datetime  }} | ||||
|               from <a href="https://geoiptool.com/en/?ip={{ certificate.lease.outer_address }}" target="_blank">{{ certificate.lease.outer_address }}</a> | ||||
|             </td></tr> | ||||
|             {% endif %} | ||||
|  | ||||
|             <!-- | ||||
|             <tr><th>MD5</th><td>{{ certificate.md5sum }}</td></tr> | ||||
|             <tr><th>SHA1</th><td>{{ certificate.sha1sum }}</td></tr> | ||||
|             --> | ||||
|             <tr><th>SHA256</th><td style="word-wrap:break-word;  overflow-wrap: break-word;  ">{{ certificate.sha256sum }}</td></tr> | ||||
|             {% if certificate.key_usage %} | ||||
|             <tr><th>Key usage</th><td>{{ certificate.key_usage | join(", ") | replace("_", " ") }}</td></tr> | ||||
|             {% endif %} | ||||
|             {% if certificate.extended_key_usage %} | ||||
|             <tr><th>Extended key usage</th><td>{{ certificate.extended_key_usage | join(", ") | replace("_", " ") }}</td></tr> | ||||
|             {% endif %} | ||||
|           </tbody> | ||||
|         </table> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
							
								
								
									
										6
									
								
								templates/views/tags.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| {% for tag in certificate.tags %} | ||||
|   <span data-cn="{{ certificate.common_name }}" | ||||
|     title="{{ tag }}" | ||||
|     class="badge badge-default" | ||||
|     onClick="onTagClicked(event);">{{ tag }}</span> | ||||
| {% endfor %} | ||||
							
								
								
									
										10
									
								
								templates/views/token.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,10 @@ | ||||
| <li id="token-{{ token.subject }}-{{ token.uuid }}" class="list-group-item filterable" data-keywords=""> | ||||
|   <span> | ||||
|   <i class="fas fa-ticket-alt"></i> | ||||
|   {{ token.uuid }}... | ||||
|   <a href="mailto:{{ token.mail }}">{{ token.subject }}</a> | ||||
|   {% if token.issuer %}{% if token.issuer != token.subject %}by {{ token.issuer }}{% else %}by himself{% endif %}{% else %}via shell{% endif %}, | ||||
|   expires | ||||
|   <time class="timeago" datetime="{{ token.expires }}">{{ token.expires }}</time> | ||||
|   </span> | ||||
| </li> | ||||