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";
|
Loading…
Reference in a new issue