<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="css/styles.min.css"> <meta name="theme-color" content="#ffffff"> <title>{{ .Title }}</title> </head> <body> <div class="container"> <h1>{{ .Title }}</h1> <div class="row"> <div class="col-12"> <form id="csr-form"> {{ .csrfField }} <div class="form-group"> <label for="nameInput">{{ .NameLabel }}</label> <input type="text" class="form-control" id="nameInput" aria-describedby="nameHelp" required minlength="3"> <small id="nameHelp" class="form-text text-muted">{{ .NameHelpText }}</small> </div> <div class="form-group"> <label for="passwordInput">{{ .PasswordLabel }}</label> <input type="password" class="form-control" id="passwordInput" aria-describedby="nameHelp" required minlength="8"> </div> <fieldset class="form-group"> <legend>{{ .RSAKeySizeLegend }}</legend> <div class="form-check"> <input class="form-check-input" type="radio" name="keySize" id="size3072" value="3072" checked> <label class="form-check-label" for="size3072">{{ .RSA3072Label }}</label> </div> <div class="form-check"> <input class="form-check-input" type="radio" name="keySize" id="size2048" value="2048"> <label class="form-check-label" for="size2048">{{ .RSA2048Label }}</label> </div> <div class="form-check"> <input class="form-check-input" type="radio" name="keySize" id="size4096" value="4096"> <label class="form-check-label" for="size4096">{{ .RSA4096Label }}</label> </div> <small id="keySizeHelp" class="form-text text-muted">{{ .RSAHelpText }}</small> </fieldset> <button type="submit" id="action-button" class="btn btn-primary">{{ .CSRButtonLabel }}</button> </form> </div> </div> <div class="row d-none" id="status-block"> <div class="col-12 py-3"> <div class="progress" style="height: 2rem"> <div id="progress-bar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="4">{{ .StatusLoading }} </div> </div> </div> <div class="col-12 d-none" id="download-wrapper"> <p class="text-info">{{ .DownloadDescription }}</p> <a href="#" class="btn btn-success" id="download-link"> <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/> <path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/> </svg> {{ .DownloadLabel }}</a> </div> </div> <pre id="key" class="d-none"></pre> <pre id="csr" class="d-none"></pre> <pre id="crt" class="d-none"></pre> </div> <script src="js/jquery.min.js"></script> <script src="js/forge.all.min.js"></script> <script src="js/bootstrap.bundle.min.js"></script> <script src="js/i18next.min.js"></script> <script> async function postData(url = '', data = {}, csrfToken) { const response = await fetch(url, { method: 'POST', mode: 'cors', cache: 'no-cache', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken, }, redirect: "error", referrerPolicy: "no-referrer", body: JSON.stringify(data), }); return response.json() } document.addEventListener("DOMContentLoaded", function () { i18n.init({fallbackLng: 'en', debug: true, useCookie: false}, (err) => { if (err) return console.log('something went wrong loading', err); }); const keyElement = document.getElementById('key'); document.getElementById('csr-form').onsubmit = function (event) { const subject = event.target["nameInput"].value; const password = event.target["passwordInput"].value; const csrfToken = event.target["csrfToken"].value; const keySize = parseInt(event.target["keySize"].value); if (isNaN(keySize)) { return false; } const statusBlock = document.getElementById('status-block'); const progressBar = document.getElementById('progress-bar'); statusBlock.classList.remove('d-none'); progressBar.classList.add('progress-bar-striped', 'progress-bar-animated'); progressBar.style.width = "25%"; progressBar.setAttribute("aria-valuenow", "1"); const state = forge.pki.rsa.createKeyPairGenerationState(keySize, 0x10001); progressBar.innerHTML = i18n.t('keygen.started'); const startDate = new Date(); const step = function () { let duration = (new Date()).getTime() - startDate.getTime(); let seconds = Math.floor(duration / 100) / 10; if (!forge.pki.rsa.stepKeyPairGenerationState(state, 100)) { setTimeout(step, 1); progressBar.innerHTML = i18n.t('keygen.running', {seconds: seconds}); } else { progressBar.classList.remove("progress-bar-animated", 'progress-bar-striped'); progressBar.style.width = "50%"; progressBar.setAttribute("aria-valuenow", "2"); progressBar.innerHTML = i18n.t('keygen.generated', {seconds: seconds}); const keys = state.keys; keyElement.innerHTML = forge.pki.privateKeyToPem(keys.privateKey); const csr = forge.pki.createCertificationRequest(); csr.publicKey = keys.publicKey; csr.setSubject([{ name: 'commonName', value: subject, valueTagClass: forge.asn1.Type.UTF8, }]); csr.sign(keys.privateKey, forge.md.sha256.create()); const verified = csr.verify(); if (verified) { let csrPem = forge.pki.certificationRequestToPem(csr); document.getElementById("csr").innerHTML = csrPem; progressBar.style.width = "75%"; progressBar.setAttribute("aria-valuenow", "3"); progressBar.classList.add('progress-bar-striped', 'progress-bar-animated'); progressBar.innerHTML = i18n.t('keygen.generated', {seconds: seconds}) + ', ' + i18n.t('certificate.waiting'); postData("/sign/", {"csr": csrPem, "common_name": subject}, csrfToken) .then(data => { const request_id = data["request_id"] const webSocket = new WebSocket( "wss://" + window.location.toString().substring( "https://".length ).split("/")[0] + "/ws/") webSocket.onopen = function () { webSocket.send(JSON.stringify({"request_id": request_id})) } webSocket.onmessage = function (event) { handleCertificateResponse(JSON.parse(event.data)); } webSocket.onclose = function (event) { if (event.wasClean) { console.debug("websocket closed cleanly"); } else { console.error("websocket connection died"); } } webSocket.onerror = function (error) { console.error(error.message); } }); function handleCertificateResponse(data) { document.getElementById("crt").innerHTML = data["certificate"]; let certificates = [] certificates.push(forge.pki.certificateFromPem(data["certificate"])); for (let certificatePemData of data["ca_chain"]) { certificates.push(forge.pki.certificateFromPem(certificatePemData)); } // browsers have trouble importing anything but 3des encrypted PKCS#12 const p12asn1 = forge.pkcs12.toPkcs12Asn1( keys.privateKey, certificates, password, {algorithm: '3des'} ); const p12Der = forge.asn1.toDer(p12asn1).getBytes(); const p12B64 = forge.util.encode64(p12Der); const downloadLink = document.getElementById('download-link'); downloadLink.download = 'client_certificate.p12'; downloadLink.setAttribute('href', 'data:application/x-pkcs12;base64,' + p12B64); document.getElementById('download-wrapper').classList.remove("d-none"); progressBar.classList.remove("progress-bar-animated", 'progress-bar-striped'); progressBar.style.width = "100%"; progressBar.innerHTML = i18n.t('keygen.generated', {seconds: seconds}) + ', ' + i18n.t('certificate.received'); progressBar.setAttribute("aria-valuenow", "4"); } } } } setTimeout(step); return false; }; }); </script> </body> </html>