Add initial implementation
This commit implements a basic static HTML page that uses Bootstrap 4 for layout and node-forge to generate a RSA key pair and a certificate signing request. The subject of the CSR and the key size can be chosen by the user. The implementation uses gulp to collect static assets and to allow bootstrap customization.
This commit is contained in:
		
						commit
						564c1bd76b
					
				
					 7 changed files with 5121 additions and 0 deletions
				
			
		
							
								
								
									
										4
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| .*.swp | ||||
| /.idea/ | ||||
| /node_modules/ | ||||
| /public/ | ||||
							
								
								
									
										40
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,40 @@ | |||
| # Browser PKCS#10 CSR generation PoC | ||||
| 
 | ||||
| This repository contains a small proof of concept implementation of browser | ||||
| based PKCS#10 certificate signing request and PKCS#12 key store generation | ||||
| using [node-forge](https://github.com/digitalbazaar/forge). | ||||
| 
 | ||||
| ## Running | ||||
| 
 | ||||
| 1. Clone the repository | ||||
| 
 | ||||
|     ``` | ||||
|     git clone https://git.dittberner.info/jan/browser_csr_generation.git | ||||
|     ``` | ||||
| 
 | ||||
| 2. Get dependencies and build assets | ||||
| 
 | ||||
|     ``` | ||||
|     cd browser_csr_generation | ||||
|     npm install --global gulp-cli | ||||
|     npm install | ||||
|     gulp | ||||
|     ``` | ||||
| 
 | ||||
| 3. Run a Python web server with the generated resources | ||||
| 
 | ||||
|     ``` | ||||
|     python3 -m http.server -d public | ||||
|     ``` | ||||
| 
 | ||||
|     Open http://localhost:8000/ in your browser. | ||||
| 
 | ||||
| 4. Run gulp watch | ||||
| 
 | ||||
|     You can run a [gulp watch](https://gulpjs.com/docs/en/getting-started/watching-files/) | ||||
|     in a second terminal window to automatically publish changes to the files | ||||
|     in the `src` directory: | ||||
| 
 | ||||
|     ``` | ||||
|     gulp watch | ||||
|     ``` | ||||
							
								
								
									
										65
									
								
								gulpfile.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								gulpfile.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,65 @@ | |||
| const {series, parallel, src, dest, watch} = require('gulp'); | ||||
| const csso = require('gulp-csso'); | ||||
| const del = require('delete'); | ||||
| const rename = require('gulp-rename'); | ||||
| const replace = require('gulp-replace'); | ||||
| const sass = require('gulp-sass'); | ||||
| const sourcemaps = require('gulp-sourcemaps'); | ||||
| const sriHash = require('gulp-sri-hash'); | ||||
| const uglify = require('gulp-uglify'); | ||||
| 
 | ||||
| sass.compiler = require('node-sass'); | ||||
| 
 | ||||
| function clean(cb) { | ||||
|     del(['./public/js/*.js', './public/css/*.css'], cb); | ||||
| } | ||||
| 
 | ||||
| function cssTranspile() { | ||||
|     return src('src/scss/**/*.scss') | ||||
|         .pipe(sourcemaps.init()) | ||||
|         .pipe(sass()) | ||||
|         .pipe(dest('public/css')) | ||||
|         .pipe(sourcemaps.write()); | ||||
| } | ||||
| 
 | ||||
| function cssMinify() { | ||||
|     return src('public/css/styles.css') | ||||
|         .pipe(csso()) | ||||
|         .pipe(rename({extname: '.min.css'})) | ||||
|         .pipe(dest('public/css')); | ||||
| } | ||||
| 
 | ||||
| function jsMinify() { | ||||
|     return src('src/js/*.js') | ||||
|         .pipe(uglify()) | ||||
|         .pipe(rename({extname: '.min.js'})) | ||||
|         .pipe(dest('public/js')); | ||||
| } | ||||
| 
 | ||||
| function publishAssets() { | ||||
|     return src([ | ||||
|         'node_modules/popper.js/dist/*.js', | ||||
|         'node_modules/popper.js/dist/*.map', | ||||
|         'node_modules/jquery/dist/*.*', | ||||
|         'node_modules/bootstrap/dist/js/*.*', | ||||
|         'node_modules/node-forge/dist/*.*' | ||||
|     ]).pipe(dest('public/js')); | ||||
| } | ||||
| 
 | ||||
| function publish() { | ||||
|     return src('src/*.html').pipe(sriHash()).pipe(replace('../public/', '')).pipe(dest('public')); | ||||
| } | ||||
| 
 | ||||
| exports.default = series( | ||||
|     clean, | ||||
|     cssTranspile, | ||||
|     parallel(cssMinify, jsMinify), | ||||
|     publishAssets, | ||||
|     publish | ||||
| ); | ||||
| 
 | ||||
| exports.watch = function () { | ||||
|     watch('src/js/*.js', series(jsMinify, publish)); | ||||
|     watch('src/scss/*.scss', series(cssTranspile, cssMinify, publish)); | ||||
|     watch('src/*.html', publish); | ||||
| } | ||||
							
								
								
									
										4869
									
								
								package-lock.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										4869
									
								
								package-lock.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										31
									
								
								package.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								package.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | |||
| { | ||||
|   "name": "browser-csr-generation", | ||||
|   "version": "0.1.0", | ||||
|   "description": "Browser based CSR and PKCS#12 generation in JavaScript", | ||||
|   "repository": { | ||||
|     "type": "git", | ||||
|     "url": "https://git.dittberner.info/jan/browser_csr_generation.git" | ||||
|   }, | ||||
|   "keywords": [ | ||||
|     "pkcs10", | ||||
|     "pkcs12", | ||||
|     "pki" | ||||
|   ], | ||||
|   "author": "Jan Dittberner", | ||||
|   "license": "GPL-2.0+", | ||||
|   "devDependencies": { | ||||
|     "bootstrap": "^4.5.3", | ||||
|     "delete": "^1.1.0", | ||||
|     "gulp": "^4.0.2", | ||||
|     "gulp-csso": "^4.0.1", | ||||
|     "gulp-rename": "^2.0.0", | ||||
|     "gulp-replace": "^1.0.0", | ||||
|     "gulp-sass": "^4.1.0", | ||||
|     "gulp-sourcemaps": "^3.0.0", | ||||
|     "gulp-sri-hash": "^2.2.1", | ||||
|     "gulp-uglify": "^3.0.2", | ||||
|     "jquery": "^3.5.1", | ||||
|     "node-forge": "^0.10.0", | ||||
|     "popper.js": "^1.16.1" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										111
									
								
								src/index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								src/index.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,111 @@ | |||
| <!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="../public/css/styles.min.css"> | ||||
|     <meta name="theme-color" content="#ffffff"> | ||||
| 
 | ||||
|     <title>CSR generation in browser</title> | ||||
| </head> | ||||
| <body> | ||||
| <div class="container"> | ||||
|     <h1>CSR generation in browser</h1> | ||||
|     <div class="row"> | ||||
|         <div class="col-12"> | ||||
|             <form id="csr-form"> | ||||
|                 <div class="form-group"> | ||||
|                     <label for="nameInput">Your name</label> | ||||
|                     <input type="text" class="form-control" id="nameInput" aria-describedby="nameHelp" required | ||||
|                            minlength="3"> | ||||
|                     <small id="nameHelp" class="form-text text-muted">Please input your name as it should be added to | ||||
|                         your certificate</small> | ||||
|                 </div> | ||||
|                 <fieldset class="form-group"> | ||||
|                     <legend>RSA Key Size</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">3072 Bit</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">2048 Bit (not recommended</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">4096 Bit</label> | ||||
|                     </div> | ||||
|                     <small id="keySizeHelp" class="form-text text-muted">An RSA key pair will be generated in your | ||||
|                         browser. Longer key sizes provide better security but take longer to generate.</small> | ||||
|                 </fieldset> | ||||
|                 <button type="submit" id="gen-csr-button" class="btn btn-primary">Generate Signing Request</button> | ||||
|             </form> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div id="status-block" class="d-none row"> | ||||
|         <div class="col-12"> | ||||
|             <div class="d-flex align-items-center"> | ||||
|                 <strong id="status-text">Loading ...</strong> | ||||
|                 <div class="spinner-border ml-auto" id="status-spinner" role="status" aria-hidden="true"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|     <pre id="key"></pre> | ||||
|     <pre id="csr"></pre> | ||||
| </div> | ||||
| <script src="../public/js/jquery.slim.min.js"></script> | ||||
| <script src="../public/js/forge.all.min.js"></script> | ||||
| <script src="../public/js/bootstrap.bundle.min.js"></script> | ||||
| <script> | ||||
|     const keyElement = document.getElementById('key'); | ||||
|     document.getElementById('csr-form').onsubmit = function (event) { | ||||
|         const subject = event.target["nameInput"].value; | ||||
|         const keySize = parseInt(event.target["keySize"].value); | ||||
|         if (isNaN(keySize)) { | ||||
|             return false; | ||||
|         } | ||||
|         const spinner = document.getElementById('status-spinner'); | ||||
|         const statusText = document.getElementById('status-text'); | ||||
|         const statusBlock = document.getElementById('status-block'); | ||||
|         statusBlock.classList.remove('d-none'); | ||||
|         spinner.classList.remove('d-none'); | ||||
| 
 | ||||
|         const state = forge.pki.rsa.createKeyPairGenerationState(keySize, 0x10001); | ||||
|         statusText.innerHTML = 'started key generation'; | ||||
|         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); | ||||
|                 statusText.innerHTML = `key generation running for ${seconds} seconds`; | ||||
|             } else { | ||||
|                 statusText.innerHTML = `key generated in ${seconds} seconds` | ||||
|                 spinner.classList.add('d-none'); | ||||
|                 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, | ||||
|                 }]); | ||||
|                 csr.sign(keys.privateKey, forge.md.sha256.create()); | ||||
| 
 | ||||
|                 const verified = csr.verify(); | ||||
|                 if (verified) { | ||||
|                     document.getElementById("csr").innerHTML = forge.pki.certificationRequestToPem(csr); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|         setTimeout(step); | ||||
|         return false; | ||||
|     }; | ||||
| </script> | ||||
| </body> | ||||
| </html> | ||||
| 
 | ||||
							
								
								
									
										1
									
								
								src/scss/styles.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/scss/styles.scss
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| @import "node_modules/bootstrap/scss/bootstrap"; | ||||
		Reference in a new issue