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:
Jan Dittberner 2020-11-22 11:28:28 +01:00
commit 564c1bd76b
7 changed files with 5121 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.*.swp
/.idea/
/node_modules/
/public/

40
README.md Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

31
package.json Normal file
View 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
View 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
View file

@ -0,0 +1 @@
@import "node_modules/bootstrap/scss/bootstrap";