From 1ea20850e90c5079bff5900b7cdc59fbacf19e4e Mon Sep 17 00:00:00 2001 From: pehmo1 Date: Mon, 2 Aug 2021 16:55:09 +0300 Subject: [PATCH] Switch from node-forge to pkijs --- Dockerfile | 5 +- rollup.config.js | 14 ++ static/index.html | 2 +- static/js/certidude.js | 290 +++++++++++++++++++++++++++++++---------- static/js/formatPEM.js | 32 +++++ 5 files changed, 272 insertions(+), 71 deletions(-) create mode 100644 rollup.config.js create mode 100644 static/js/formatPEM.js diff --git a/Dockerfile b/Dockerfile index bbe7b75..9850dff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,17 @@ FROM alpine MAINTAINER Pinecrypt Labs RUN apk add --update npm nginx rsync bash -RUN npm install --prefix /usr/local --silent --no-optional -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg xterm +RUN npm install --prefix /usr/local --silent --no-optional -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg xterm rollup RUN test -e /usr/local/lib/node_modules/jquery/dist/jquery.min.js COPY nginx.conf /etc/nginx/nginx.conf EXPOSE 80 443 8443 WORKDIR /var/lib/nginx/html/ +RUN npm init -y && npm i pkijs rollup-plugin-node-resolve RUN rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ fonts/ COPY static ./ COPY templates templates +COPY rollup.config.js . +RUN rollup -c RUN nunjucks-precompile --include snippets --include views templates >> js/bundle.js RUN bash -c 'cat /usr/local/lib/node_modules/{jquery/dist/jquery.min.js,tether/dist/js/tether.min.js,bootstrap/dist/js/bootstrap.min.js,node-forge/dist/forge.all.min.js,qrcode-svg/dist/qrcode.min.js,timeago/jquery.timeago.js,nunjucks/browser/nunjucks-slim.min.js,xterm/lib/xterm.js} >> js/bundle.js' RUN bash -c 'cat /usr/local/lib/node_modules/{tether/dist/css/tether.min.css,bootstrap/dist/css/bootstrap.min.css,font-awesome/css/font-awesome.min.css,xterm/css/xterm.css} >> css/bundle.css' diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..9a1783f --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,14 @@ +import rollupNodeResolve from "rollup-plugin-node-resolve"; + +export default { + input: "js/certidude.js", + plugins: [ + rollupNodeResolve({ jsnext: true, main: true }) + ], + output: [ + { + file: "js/certidude_bundle.js", + format: "iife" + } + ] +}; \ No newline at end of file diff --git a/static/index.html b/static/index.html index d7d7066..dded2a1 100644 --- a/static/index.html +++ b/static/index.html @@ -6,7 +6,7 @@ - + diff --git a/static/js/certidude.js b/static/js/certidude.js index 9293611..ab052ec 100644 --- a/static/js/certidude.js +++ b/static/js/certidude.js @@ -1,11 +1,32 @@ +"use strict"; +import * as asn1js from "asn1js"; +import { + arrayBufferToString, + stringToArrayBuffer, + toBase64, + fromBase64, + bufferToHexCodes +} from "pvutils"; +import { + getCrypto, + getAlgorithmParameters, +} from "../node_modules/pkijs/src/common.js"; +import { formatPEM } from "./formatPEM.js"; +import CertificationRequest from "../node_modules/pkijs/src/CertificationRequest.js"; +import AttributeTypeAndValue from "../node_modules/pkijs/src/AttributeTypeAndValue.js"; +import Certificate from "../node_modules/pkijs/src/Certificate.js"; -'use strict'; - +let hashAlg = "SHA-384"; +let signAlg = "RSASSA-PKCS1-V1_5"; const KEY_SIZE = 2048; const DEVICE_KEYWORDS = ["Android", "iPhone", "iPad", "Windows", "Ubuntu", "Fedora", "Mac", "Linux"]; jQuery.timeago.settings.allowFuture = true; +const crypto = getCrypto(); +if (typeof crypto === "undefined") + console.error("No WebCrypto extension found"); + function onLaunchShell(common_name) { $.post({ url: "/api/signed/" + common_name + "/shell/", @@ -60,61 +81,114 @@ function onShowAll() { } function onKeyGen() { - if (window.navigator.userAgent.indexOf(" Edge/") >= 0) { - $("#enroll .loader-container").hide(); - $("#enroll .edge-broken").show(); - return; - } - - window.keys = forge.pki.rsa.generateKeyPair(KEY_SIZE); - console.info('Key-pair created.'); - - window.csr = forge.pki.createCertificationRequest(); - csr.publicKey = keys.publicKey; - csr.setSubject([{ - name: 'commonName', value: common_name - }]); - - csr.sign(keys.privateKey, forge.md.sha384.create()); - console.info('Certification request created'); - - - $("#enroll .loader-container").hide(); - - var prefix = null; - for (i in DEVICE_KEYWORDS) { - var keyword = DEVICE_KEYWORDS[i]; - if (window.navigator.userAgent.indexOf(keyword) >= 0) { - prefix = keyword.toLowerCase(); - break; - } - } - - if (prefix == null) { - $(".option").show(); + return new Promise((resolve, reject) => { + if (window.navigator.userAgent.indexOf(" Edge/") >= 0) { + $("#enroll .loader-container").hide(); + $("#enroll .edge-broken").show(); return; - } + } - var protocols = query.protocols.split(","); - console.info("Showing snippets for:", protocols); - for (var j = 0; j < protocols.length; j++) { - var options = document.querySelectorAll(".option." + protocols[j] + "." + prefix); - for (i = 0; i < options.length; i++) { - options[i].style.display = "block"; + let sequence = Promise.resolve(); + const pkcs10 = new CertificationRequest(); + let publicKey, privateKey; + + // Commonname + pkcs10.subject.typesAndValues.push( + new AttributeTypeAndValue({ + type: "2.5.4.3", + value: new asn1js.Utf8String({ value: common_name }), + }) + ); + + pkcs10.attributes = []; + + sequence = sequence.then(() => { + const algorithm = getAlgorithmParameters(signAlg, "generatekey"); + if ("hash" in algorithm.algorithm) + algorithm.algorithm.hash.name = hashAlg; + + return crypto.generateKey(algorithm.algorithm, true, algorithm.usages); + }); + + sequence = sequence.then( + (keyPair) => { + window.keys = keyPair; + publicKey = keyPair.publicKey; + privateKey = keyPair.privateKey; + }, + (error) => Promise.reject(`Error during key generation: ${error}`) + ); + + sequence = sequence.then(() => + pkcs10.subjectPublicKeyInfo.importKey(publicKey) + ); + + sequence = sequence.then( + async () => { + pkcs10.sign(privateKey, hashAlg); + window.csr = pkcs10; + console.info("Certification request created"); + var pkcs8 = await crypto.exportKey("pkcs8", keys.privateKey); + var pem = formatPEM( + toBase64(String.fromCharCode.apply(null, new Uint8Array(pkcs8))) + ); + console.log( + `-----BEGIN RSA PRIVATE KEY-----\r\n${pem}\r\n-----END RSA PRIVATE KEY-----\r\n` + ); + resolve(); + }, + (error) => Promise.reject(`Error during exporting public key: ${error}`) + ); + + sequence = sequence.then(() => { + $("#enroll .loader-container").hide(); + var prefix = null; + for (i in DEVICE_KEYWORDS) { + var keyword = DEVICE_KEYWORDS[i]; + if (window.navigator.userAgent.indexOf(keyword) >= 0) { + prefix = keyword.toLowerCase(); + break; + } } - } - $(".option.any").show(); + + if (prefix == null) { + $(".option").show(); + return; + } + + var protocols = query.protocols.split(","); + console.info("Showing snippets for:", protocols); + for (var j = 0; j < protocols.length; j++) { + var options = document.querySelectorAll( + ".option." + protocols[j] + "." + prefix + ); + for (i = 0; i < options.length; i++) { + options[i].style.display = "block"; + } + } + $(".option.any").show(); + }); + }); } function blobToUuid(blob) { - var md = forge.md.md5.create(); - md.update(blob); - var digest = md.digest().toHex(); - return digest.substring(0, 8) + "-" + - digest.substring(8, 12) + "-" + - digest.substring(12, 16) + "-" + - digest.substring(16,20) + "-" + - digest.substring(20); + return new Promise((resolve, reject) => { + crypto.digest({ name: "SHA-1" }, stringToArrayBuffer(blob)).then((res) => { + res = bufferToHexCodes(res).toLowerCase(); + res = + res.substring(0, 8) + + "-" + + res.substring(8, 12) + + "-" + + res.substring(12, 16) + + "-" + + res.substring(16, 20) + + "-" + + res.substring(20); + + resolve(res); + }); + }); } function onEnroll(encoding) { @@ -125,17 +199,42 @@ function onEnroll(encoding) { xhr.open('GET', "/api/certificate/"); xhr.onload = function() { if (xhr.status === 200) { + // const xhrPEM = xhr.responseText.replace( + // /(-----(BEGIN|END) CERTIFICATE-----|\n)/g, + // "" + // ); + // const xhrAsn1 = asn1js.fromBER(stringToArrayBuffer(fromBase64(xhrPEM))); + // var ca = new Certificate({ schema: xhrAsn1.result }); var ca = forge.pki.certificateFromPem(xhr.responseText); console.info("Got CA certificate:"); var xhr2 = new XMLHttpRequest(); xhr2.open("PUT", "/api/token/?token=" + query.token ); - xhr2.onload = function() { + xhr2.onload = async function() { if (xhr2.status === 200) { var a = document.createElement("a"); + + // const xhr2PEM = xhr.responseText.replace( + // /(-----(BEGIN|END) CERTIFICATE-----|\n)/g, + // "" + // ); + // const xhr2asn1 = asn1js.fromBER( + // stringToArrayBuffer(fromBase64(xhr2PEM)) + // ); + // var cert = await new Certificate({ schema: xhr2asn1.result }); + var cert = forge.pki.certificateFromPem(xhr2.responseText); console.info("Got signed certificate:", xhr2.responseText); + + // Convert PKIJS key to forge key through PEM + let privateKeyArrayBuffer = new ArrayBuffer(0); + privateKeyArrayBuffer = await crypto.exportKey("pkcs8", keys.privateKey); + let tempPrivPem = `\r\n-----BEGIN PRIVATE KEY-----\r\n`; + tempPrivPem = `${tempPrivPem}${formatPEM(toBase64(arrayBufferToString(privateKeyArrayBuffer)))}`; + tempPrivPem = `${tempPrivPem}\r\n-----END PRIVATE KEY-----\r\n`; + let forgePrivKey = forge.pki.privateKeyFromPem(tempPrivPem); + var p12 = forge.asn1.toDer(forge.pkcs12.toPkcs12Asn1( - keys.privateKey, [cert, ca], "", {algorithm: '3des'})).getBytes(); + forgePrivKey, [cert, ca], "", {algorithm: '3des'})).getBytes(); switch(encoding) { case 'p12': @@ -158,7 +257,7 @@ function onEnroll(encoding) { } }, local: { - p12: forge.util.encode64(p12) + p12: toBase64(p12), } }); console.info("Buf is:", buf); @@ -166,9 +265,17 @@ function onEnroll(encoding) { a.download = query.title + ".sswan"; break case 'ovpn': + let privKey = await crypto.exportKey("pkcs8", keys.privateKey); + let privKeyBody = formatPEM( + toBase64( + String.fromCharCode.apply(null, new Uint8Array(privKey)) + ) + ); + let privKeyPem = `-----BEGIN RSA PRIVATE KEY-----\r\n${privKeyBody}\r\n-----END RSA PRIVATE KEY-----\r\n`; + var buf = nunjucks.render('snippets/openvpn-client.conf', { authority: authority, - key: forge.pki.privateKeyToPem(keys.privateKey), + key: privKeyPem, cert: xhr2.responseText, ca: xhr.responseText }); @@ -186,15 +293,19 @@ function onEnroll(encoding) { common_name: common_name, gateway: authority.namespace, p12_uuid: blobToUuid(p12), - p12: forge.util.encode64(p12), - ca_uuid: blobToUuid(forge.asn1.toDer(forge.pki.certificateToAsn1(ca)).getBytes()), - ca: forge.util.encode64(forge.asn1.toDer(forge.pki.certificateToAsn1(ca)).getBytes()) + p12: toBase64(p12), + ca_uuid: blobToUuid( + forge.asn1.toDer(forge.pki.certificateToAsn1(ca)).getBytes() + ), + ca: toBase64( + forge.asn1.toDer(forge.pki.certificateToAsn1(ca)).getBytes() + ), }); var mimetype = "application/x-apple-aspen-config"; a.download = query.title + ".mobileconfig"; break } - a.href = "data:" + mimetype + ";base64," + forge.util.encode64(buf); + a.href = "data:" + mimetype + ";base64," + toBase64(buf); console.info("Offering bundle for download"); document.body.appendChild(a); // Firefox needs this! a.click(); @@ -210,7 +321,13 @@ function onEnroll(encoding) { } } }; - xhr2.send(forge.pki.certificationRequestToPem(csr)); + let resultString = "-----BEGIN CERTIFICATE REQUEST-----\r\n"; + resultString = `${resultString}${formatPEM( + toBase64(arrayBufferToString(csr.toSchema().toBER(false))) + )}`; + resultString = `${resultString}\r\n-----END CERTIFICATE REQUEST-----\r\n`; + + xhr2.send(resultString); } } xhr.send(); @@ -238,13 +355,9 @@ function onHashChanged() { } $("#view-dashboard").html(env.render('views/error.html', { message: msg })); }, - success: function(authority) { + success: async function(authority) { window.authority = authority - // Device identifier - var dig = forge.md.sha384.create(); - dig.update(window.navigator.userAgent); - var prefix = "unknown"; for (i in DEVICE_KEYWORDS) { var keyword = DEVICE_KEYWORDS[i]; @@ -254,7 +367,9 @@ function onHashChanged() { } } - window.common_name = prefix + "-" + dig.digest().toHex().substring(0, 5); + // Device identifier + var dig = await blobToUuid(window.navigator.userAgent); + window.common_name = prefix + "-" + dig.substring(0, 5); console.info("Device identifier:", common_name); if (window.location.protocol != "https:") { @@ -694,8 +809,16 @@ function loadAuthority(query) { - $("#enroll").click(function() { - var keys = forge.pki.rsa.generateKeyPair(1024); + $("#enroll").click(async function() { + var keys = await crypto.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["encrypt", "decrypt"]); $.ajax({ method: "POST", @@ -713,7 +836,11 @@ function loadAuthority(query) { - var privateKeyBuffer = forge.pki.privateKeyToPem(keys.privateKey); + var pkcs8 = await crypto.exportKey("pkcs8", keys.privateKey); + var pem = formatPEM( + toBase64(String.fromCharCode.apply(null, new Uint8Array(pkcs8))) + ); + privateKeyBuffer = `-----BEGIN RSA PRIVATE KEY-----\r\n${pem}\r\n-----END RSA PRIVATE KEY-----\r\n`; }); /** @@ -838,3 +965,28 @@ $(document).ready(function() { env.addFilter("serial", serialFilter); onHashChanged(); }); + +window.onLaunchShell = onLaunchShell; +window.onRejectRequest = onRejectRequest; +window.onSignRequest = onSignRequest; +window.onShowAll = onShowAll; +window.onKeyGen = onKeyGen; +window.onEnroll = onEnroll; +window.onHashChanged = onHashChanged; +window.onToggleAccessButtonClicked = onToggleAccessButtonClicked; +window.onTagClicked = onTagClicked; +window.onNewTagClicked = onNewTagClicked; +window.onTagFilterChanged = onTagFilterChanged; +window.onLogEntry = onLogEntry; +window.onRequestSubmitted = onRequestSubmitted; +window.onRequestDeleted = onRequestDeleted; +window.onLeaseUpdate = onLeaseUpdate; +window.onRequestSigned = onRequestSigned; +window.onCertificateRevoked = onCertificateRevoked; +window.onTagUpdated = onTagUpdated; +window.onAttributeUpdated = onAttributeUpdated; +window.onSubmitRequest = onSubmitRequest; +window.onServerStarted = onServerStarted; +window.onServerStopped = onServerStopped; +window.onIssueToken = onIssueToken; +window.onInstanceAvailabilityUpdated = onInstanceAvailabilityUpdated; \ No newline at end of file diff --git a/static/js/formatPEM.js b/static/js/formatPEM.js new file mode 100644 index 0000000..7926712 --- /dev/null +++ b/static/js/formatPEM.js @@ -0,0 +1,32 @@ +//************************************************************************************** +//region Auxilliary functions +//************************************************************************************** +/** + * Format string in order to have each line with length equal to 64 + * @param {string} pemString String to format + * @returns {string} Formatted string + */ +export function formatPEM(pemString) +{ + const PEM_STRING_LENGTH = pemString.length, LINE_LENGTH = 64; + const wrapNeeded = PEM_STRING_LENGTH > LINE_LENGTH; + + if(wrapNeeded) + { + let formattedString = "", wrapIndex = 0; + + for(let i = LINE_LENGTH; i < PEM_STRING_LENGTH; i += LINE_LENGTH) + { + formattedString += pemString.substring(wrapIndex, i) + "\r\n"; + wrapIndex = i; + } + + formattedString += pemString.substring(wrapIndex, PEM_STRING_LENGTH); + return formattedString; + } + else + { + return pemString; + } +} +//**************************************************************************************