186 lines
		
	
	
		
			No EOL
		
	
	
		
			9.1 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			186 lines
		
	
	
		
			No EOL
		
	
	
		
			9.1 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
| <!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");
 | |
|                         postData("/sign/", {"csr": csrPem, "commonName": subject}, csrfToken)
 | |
|                             .then(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.style.width = "100%";
 | |
|                                 progressBar.setAttribute("aria-valuenow", "4");
 | |
|                             });
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|             setTimeout(step);
 | |
|             return false;
 | |
|         };
 | |
|     });
 | |
| </script>
 | |
| </body>
 | |
| </html> |