Add support for EC keys #3

Merged
lauri merged 6 commits from ec-key-support into master 2021-08-19 16:59:21 +00:00
4 changed files with 275 additions and 145 deletions

View File

@ -1,7 +1,7 @@
FROM alpine FROM alpine
MAINTAINER Pinecrypt Labs <info@pinecrypt.com> MAINTAINER Pinecrypt Labs <info@pinecrypt.com>
RUN apk add --update npm nginx rsync bash 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 rollup RUN npm install --prefix /usr/local --silent --no-optional -g nunjucks@2.5.2 nunjucks-date@1.2.0 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 RUN test -e /usr/local/lib/node_modules/jquery/dist/jquery.min.js
COPY nginx.conf /etc/nginx/nginx.conf COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80 443 8443 EXPOSE 80 443 8443
@ -13,7 +13,7 @@ COPY templates templates
COPY rollup.config.js . COPY rollup.config.js .
RUN rollup -c RUN rollup -c
RUN nunjucks-precompile --include snippets --include views templates >> js/bundle.js 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/{jquery/dist/jquery.min.js,tether/dist/js/tether.min.js,bootstrap/dist/js/bootstrap.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' 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'
RUN mkdir /frontend-secrets RUN mkdir /frontend-secrets
RUN ln -s ../server-secrets/self_cert.pem /frontend-secrets/fullchain.pem RUN ln -s ../server-secrets/self_cert.pem /frontend-secrets/fullchain.pem

View File

@ -4,27 +4,27 @@ import {
arrayBufferToString, arrayBufferToString,
stringToArrayBuffer, stringToArrayBuffer,
toBase64, toBase64,
fromBase64,
bufferToHexCodes bufferToHexCodes
} from "pvutils"; } from "pvutils";
import { import {
getCrypto, getCrypto,
getAlgorithmParameters, getAlgorithmParameters,
} from "../node_modules/pkijs/src/common.js"; } from "../node_modules/pkijs/src/common.js";
import { formatPEM } from "./formatPEM.js";
import CertificationRequest from "../node_modules/pkijs/src/CertificationRequest.js"; import CertificationRequest from "../node_modules/pkijs/src/CertificationRequest.js";
import AttributeTypeAndValue from "../node_modules/pkijs/src/AttributeTypeAndValue.js"; import AttributeTypeAndValue from "../node_modules/pkijs/src/AttributeTypeAndValue.js";
import Certificate from "../node_modules/pkijs/src/Certificate.js"; import { pkcs12chain } from "./pkcs12chain.js";
import {
pkijsToPem,
pkijsToBase64,
pemToBase64,
} from "./util.js"
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"]; const DEVICE_KEYWORDS = ["Android", "iPhone", "iPad", "Windows", "Ubuntu", "Fedora", "Mac", "Linux"];
jQuery.timeago.settings.allowFuture = true; jQuery.timeago.settings.allowFuture = true;
const crypto = getCrypto(); window.cryptoEngine = getCrypto();
if (typeof crypto === "undefined") if (typeof window.cryptoEngine === "undefined")
console.error("No WebCrypto extension found"); console.error("No WebCrypto extension found");
function onLaunchShell(common_name) { function onLaunchShell(common_name) {
@ -81,16 +81,14 @@ function onShowAll() {
} }
function onKeyGen() { function onKeyGen() {
return new Promise((resolve, reject) => { return new Promise(async (resolve, reject) => {
if (window.navigator.userAgent.indexOf(" Edge/") >= 0) { if (window.navigator.userAgent.indexOf(" Edge/") >= 0) {
$("#enroll .loader-container").hide(); $("#enroll .loader-container").hide();
$("#enroll .edge-broken").show(); $("#enroll .edge-broken").show();
return; return;
} }
let sequence = Promise.resolve(); let pkcs10 = new CertificationRequest();
const pkcs10 = new CertificationRequest();
let publicKey, privateKey;
// Commonname // Commonname
pkcs10.subject.typesAndValues.push( pkcs10.subject.typesAndValues.push(
@ -102,45 +100,29 @@ function onKeyGen() {
pkcs10.attributes = []; pkcs10.attributes = [];
sequence = sequence.then(() => { let algorithm;
const algorithm = getAlgorithmParameters(signAlg, "generatekey"); if (authority.certificate.algorithm == "rsa") {
algorithm = getAlgorithmParameters(
window.authority.certificate.key_type_specific, "generatekey");
}
if (authority.certificate.algorithm == "ec") {
algorithm = getAlgorithmParameters(
window.authority.certificate.curve, "generatekey");
}
if ("hash" in algorithm.algorithm) if ("hash" in algorithm.algorithm)
algorithm.algorithm.hash.name = hashAlg; algorithm.algorithm.hash.name = window.authority.certificate.hash_algorithm;
return crypto.generateKey(algorithm.algorithm, true, algorithm.usages); const keyPair = await window.cryptoEngine.generateKey(
}); algorithm.algorithm, true, algorithm.usages);
sequence = sequence.then(
(keyPair) => {
window.keys = keyPair; window.keys = keyPair;
publicKey = keyPair.publicKey; const publicKey = keyPair.publicKey;
privateKey = keyPair.privateKey; const privateKey = keyPair.privateKey;
},
(error) => Promise.reject(`Error during key generation: ${error}`)
);
sequence = sequence.then(() => await pkcs10.subjectPublicKeyInfo.importKey(publicKey);
pkcs10.subjectPublicKeyInfo.importKey(publicKey) await pkcs10.sign(privateKey, window.authority.certificate.hash_algorithm);
);
sequence = sequence.then(
async () => {
pkcs10.sign(privateKey, hashAlg);
window.csr = pkcs10; window.csr = pkcs10;
console.info("Certification request created"); 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(); $("#enroll .loader-container").hide();
var prefix = null; var prefix = null;
for (i in DEVICE_KEYWORDS) { for (i in DEVICE_KEYWORDS) {
@ -167,13 +149,17 @@ function onKeyGen() {
} }
} }
$(".option.any").show(); $(".option.any").show();
});
resolve();
}); });
} }
function blobToUuid(blob) { function blobToUuid(blob) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
crypto.digest({ name: "SHA-1" }, stringToArrayBuffer(blob)).then((res) => { window.cryptoEngine.digest(
{ name: "SHA-1" },
stringToArrayBuffer(blob))
.then((res) => {
res = bufferToHexCodes(res).toLowerCase(); res = bufferToHexCodes(res).toLowerCase();
res = res =
res.substring(0, 8) + res.substring(0, 8) +
@ -197,58 +183,38 @@ function onEnroll(encoding) {
console.info("User agent:", window.navigator.userAgent); console.info("User agent:", window.navigator.userAgent);
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open('GET', "/api/certificate/"); xhr.open('GET', "/api/certificate/");
xhr.onload = function() { xhr.onload = async function() {
if (xhr.status === 200) { if (xhr.status === 200) {
// const xhrPEM = xhr.responseText.replace( const caBase64 = pemToBase64(xhr.responseText);
// /(-----(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(); var xhr2 = new XMLHttpRequest();
xhr2.open("PUT", "/api/token/?token=" + query.token ); xhr2.open("PUT", "/api/token/?token=" + query.token );
xhr2.onload = async function() { xhr2.onload = async function() {
if (xhr2.status === 200) { if (xhr2.status === 200) {
var a = document.createElement("a"); var a = document.createElement("a");
const certBase64 = pemToBase64(xhr.responseText);
// const xhr2PEM = xhr.responseText.replace( // Private key to base64 (for pkcs12chain)
// /(-----(BEGIN|END) CERTIFICATE-----|\n)/g, let privKeyBase64 = await pkijsToBase64(keys.privateKey);
// ""
// );
// 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(
forgePrivKey, [cert, ca], "", {algorithm: '3des'})).getBytes();
switch(encoding) { switch(encoding) {
case 'p12': case 'p12':
var buf = forge.asn1.toDer(p12).getBytes(); var p12 = await pkcs12chain(privKeyBase64, [certBase64, caBase64], "", window.authority.certificate.hash_algorithm);
var buf = arrayBufferToString(p12.toSchema().toBER(false));
var mimetype = "application/x-pkcs12" var mimetype = "application/x-pkcs12"
a.download = query.title + ".p12"; a.download = query.title + ".p12";
break break
case 'sswan': case 'sswan':
var p12 = arrayBufferToString(
(await pkcs12chain(privKeyBase64, [certBase64, caBase64], "", window.authority.certificate.hash_algorithm)).toSchema().toBER(false));
var buf = JSON.stringify({ var buf = JSON.stringify({
uuid: blobToUuid(authority.namespace), uuid: await blobToUuid(authority.namespace),
name: authority.namespace, name: authority.namespace,
type: "ikev2-cert", type: "ikev2-cert",
'ike-proposal': 'aes256-sha384-prfsha384-modp2048', 'ike-proposal': window.authority.strongswan.ike,
'esp-proposal': 'aes128gcm16-modp2048', 'esp-proposal': window.authority.strongswan.esp,
remote: { remote: {
addr: authority.namespace, addr: authority.namespace,
revocation: { revocation: {
@ -265,13 +231,7 @@ function onEnroll(encoding) {
a.download = query.title + ".sswan"; a.download = query.title + ".sswan";
break break
case 'ovpn': case 'ovpn':
let privKey = await crypto.exportKey("pkcs8", keys.privateKey); let privKeyPem = await pkijsToPem(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', { var buf = nunjucks.render('snippets/openvpn-client.conf', {
authority: authority, authority: authority,
@ -283,23 +243,23 @@ function onEnroll(encoding) {
a.download = query.title + ".ovpn"; a.download = query.title + ".ovpn";
break break
case 'mobileconfig': case 'mobileconfig':
var p12 = forge.asn1.toDer(forge.pkcs12.toPkcs12Asn1( var p12 = arrayBufferToString(
keys.privateKey, [cert, ca], "1234", {algorithm: '3des'})).getBytes(); (await pkcs12chain(
privKeyBase64, [certBase64, caBase64],
"1234", window.authority.certificate.hash_algorithm))
.toSchema().toBER(false));
var buf = nunjucks.render('snippets/ios.mobileconfig', { var buf = nunjucks.render('snippets/ios.mobileconfig', {
authority: authority, authority: authority,
service_uuid: blobToUuid(query.title), service_uuid: await blobToUuid(query.title),
conf_uuid: blobToUuid(query.title + " conf1"), conf_uuid: await blobToUuid(query.title + " conf1"),
title: query.title, title: query.title,
common_name: common_name, common_name: common_name,
gateway: authority.namespace, gateway: authority.namespace,
p12_uuid: blobToUuid(p12), p12_uuid: await blobToUuid(p12),
p12: toBase64(p12), p12: toBase64(p12),
ca_uuid: blobToUuid( ca_uuid: await blobToUuid(caBase64),
forge.asn1.toDer(forge.pki.certificateToAsn1(ca)).getBytes() ca: caBase64,
),
ca: toBase64(
forge.asn1.toDer(forge.pki.certificateToAsn1(ca)).getBytes()
),
}); });
var mimetype = "application/x-apple-aspen-config"; var mimetype = "application/x-apple-aspen-config";
a.download = query.title + ".mobileconfig"; a.download = query.title + ".mobileconfig";
@ -321,19 +281,13 @@ function onEnroll(encoding) {
} }
} }
}; };
let resultString = "-----BEGIN CERTIFICATE REQUEST-----\r\n"; xhr2.send(await pkijsToPem(window.csr));
resultString = `${resultString}${formatPEM(
toBase64(arrayBufferToString(csr.toSchema().toBER(false)))
)}`;
resultString = `${resultString}\r\n-----END CERTIFICATE REQUEST-----\r\n`;
xhr2.send(resultString);
} }
} }
xhr.send(); xhr.send();
} }
function onHashChanged() { async function onHashChanged() {
window.query = {}; window.query = {};
var a = location.hash.substring(1).split('&'); var a = location.hash.substring(1).split('&');
@ -358,6 +312,12 @@ function onHashChanged() {
success: async function(authority) { success: async function(authority) {
window.authority = authority window.authority = authority
// convert "sha512" to "SHA-512"
window.authority.certificate.hash_algorithm =
(window.authority.certificate.hash_algorithm.slice(0,3) +
"-" + window.authority.certificate.hash_algorithm.slice(3))
.toUpperCase();
var prefix = "unknown"; var prefix = "unknown";
for (i in DEVICE_KEYWORDS) { for (i in DEVICE_KEYWORDS) {
var keyword = DEVICE_KEYWORDS[i]; var keyword = DEVICE_KEYWORDS[i];
@ -385,8 +345,8 @@ function onHashChanged() {
for (i = 0; i < options.length; i++) { for (i = 0; i < options.length; i++) {
options[i].style.display = "none"; options[i].style.display = "none";
} }
setTimeout(onKeyGen, 100);
console.info("Generating key pair..."); console.info("Generating key pair...");
await onKeyGen();
} else { } else {
loadAuthority(query); loadAuthority(query);
} }
@ -810,12 +770,12 @@ function loadAuthority(query) {
$("#enroll").click(async function() { $("#enroll").click(async function() {
var keys = await crypto.generateKey( var keys = await window.cryptoEngine.generateKey(
{ {
name: "RSASSA-PKCS1-v1_5", name: window.authority.certificate.key_type_specific,
modulusLength: 1024, modulusLength: window.authority.certificate.key_size,
publicExponent: new Uint8Array([1, 0, 1]), publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256", hash: window.authority.certificate.hash_algorithm,
}, },
true, true,
["encrypt", "decrypt"]); ["encrypt", "decrypt"]);
@ -836,11 +796,7 @@ function loadAuthority(query) {
var pkcs8 = await crypto.exportKey("pkcs8", keys.privateKey); var privateKeyBuffer = pkijsToPem(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`;
}); });
/** /**

120
static/js/pkcs12chain.js Normal file
View File

@ -0,0 +1,120 @@
import * as asn1js from "asn1js";
import {
stringToArrayBuffer,
fromBase64,
} from "pvutils";
import {
getRandomValues
} from "../node_modules/pkijs/src/common.js";
import Certificate from "../node_modules/pkijs/src/Certificate.js";
import PrivateKeyInfo from "../node_modules/pkijs/src/PrivateKeyInfo";
import Attribute from "../node_modules/pkijs/src/Attribute";
import SafeBag from "../node_modules/pkijs/src/SafeBag";
import PKCS8ShroudedKeyBag from "../node_modules/pkijs/src/PKCS8ShroudedKeyBag";
import PFX from "../node_modules/pkijs/src/PFX";
import AuthenticatedSafe from "../node_modules/pkijs/src/AuthenticatedSafe";
import SafeContents from "../node_modules/pkijs/src/SafeContents";
import CertBag from "../node_modules/pkijs/src/CertBag";
export async function pkcs12chain(priv, certs, password, hash_alg) {
const asn1 = asn1js.fromBER(stringToArrayBuffer(fromBase64(priv)));
const pkcs8Simpl = new PrivateKeyInfo({schema: asn1.result});
const keyLocalIDBuffer = new ArrayBuffer(4);
const keyLocalIDView = new Uint8Array(keyLocalIDBuffer);
getRandomValues(keyLocalIDView);
const bitArray = new ArrayBuffer(1);
const bitView = new Uint8Array(bitArray);
bitView[0] = bitView[0] | 0x80;
const keyUsage = new asn1js.BitString({
valueHex: bitArray,
unusedBits: 7
});
pkcs8Simpl.attributes = [
new Attribute({
type: "2.5.29.15",
values: [
keyUsage
]
})
];
const safeBags = [
new SafeBag({
bagId: "1.2.840.113549.1.12.10.1.2",
bagValue: new PKCS8ShroudedKeyBag({
parsedValue: pkcs8Simpl
}),
bagAttributes: [
new Attribute({
type: "1.2.840.113549.1.9.21", // localKeyID
values: [
new asn1js.OctetString({valueHex: keyLocalIDBuffer})
]
})
]
})
];
const numCerts = certs.length;
for (let i=0;i<numCerts;i++) {
const asn1 = asn1js.fromBER(stringToArrayBuffer(fromBase64(certs[i])));
const certSimpl = new Certificate({schema: asn1.result});
const certLocalIDBuffer = new ArrayBuffer(4);
const certLocalIDView = new Uint8Array(certLocalIDBuffer);
getRandomValues(certLocalIDView);
safeBags.push(
new SafeBag({
bagId: "1.2.840.113549.1.12.10.1.3",
bagValue: new CertBag({
parsedValue: certSimpl
}),
bagAttributes: [
new Attribute({
type: "1.2.840.113549.1.9.21", // localKeyID
values: [
new asn1js.OctetString({valueHex: certLocalIDBuffer})
]
})
]
})
);
}
let pkcs12 = new PFX({
parsedValue: {
integrityMode: 0, // Password-Based Integrity Mode
authenticatedSafe: new AuthenticatedSafe({
parsedValue: {
safeContents: [
{
privacyMode: 0, // "No-privacy" Protection Mode
value: new SafeContents({
safeBags: safeBags
})
}
]
}
})
}
});
await pkcs12.parsedValue.authenticatedSafe.makeInternalValues({
safeContents: [{}]
});
await pkcs12.makeInternalValues({
password: stringToArrayBuffer(password),
iterations: 10000,
pbkdf2HashAlgorithm: hash_alg,
hmacHashAlgorithm: hash_alg
})
return pkcs12;
}

54
static/js/util.js Normal file
View File

@ -0,0 +1,54 @@
import {
arrayBufferToString,
toBase64,
} from "pvutils";
import { formatPEM } from "./formatPEM.js";
export function pkijsToBase64(pkijsObj) {
return new Promise(async (resolve, reject) => {
switch(pkijsObj.__proto__.constructor.name) {
case "CryptoKey":
let arrayBuf = new ArrayBuffer(0);
if (pkijsObj.type == "private")
arrayBuf = await window.cryptoEngine.exportKey("pkcs8", pkijsObj);
else
arrayBuf = await window.cryptoEngine.exportKey("spki", pkijsObj);
resolve(toBase64(arrayBufferToString(arrayBuf)));
break;
case "CertificationRequest":
resolve(toBase64(arrayBufferToString(pkijsObj.toSchema().toBER(false))));
break;
}
});
}
export function pkijsToPem(pkijsObj) {
return new Promise(async (resolve, reject) => {
switch(pkijsObj.__proto__.constructor.name) {
case "CryptoKey":
let privKeyExported = await window.cryptoEngine.exportKey("pkcs8", pkijsObj);
let privKeyBody = formatPEM(
toBase64(
String.fromCharCode.apply(null, new Uint8Array(privKeyExported))
)
);
resolve(`-----BEGIN PRIVATE KEY-----\r\n${privKeyBody}\r\n-----END PRIVATE KEY-----\r\n`);
break;
case "CertificationRequest":
let resPem = "-----BEGIN CERTIFICATE REQUEST-----\r\n";
resPem = `${resPem}${formatPEM(
toBase64(arrayBufferToString(pkijsObj.toSchema().toBER(false)))
)}`;
resolve(`${resPem}\r\n-----END CERTIFICATE REQUEST-----\r\n`);
break;
}
});
}
export function pemToBase64(pem) {
return pem.replace(/(-----(BEGIN|END) CERTIFICATE-----|\n)/g, "");
}