diff --git a/README.md b/README.md index eaeefa3..b7556ee 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,41 @@ browser-csr =========== +Create a CSR (Certificate Signing Request) in the browser that supports SAN altnames. + +Used for Let's Encrypt v2 with [greenlock-browser.js](https://git.coolaj86.com/coolaj86/greenlock-browser.js). + +Usage +----- + +```html + +``` + +```js +CSR.generate({ + + /* A WebCrypto-generated keypair */ + keypair: {} + + /* Subject & SANS altnames */ +, domains: [ 'example.com', 'www.example.com' ] + +, format: 'rfc7515' // unpadded urlsafe base64 + +}).then(function (csrweb64) { + console.log(csrweb64); +}); +``` + +Dependencies +------------ + +[`pkijs@v1.3.33`](https://github.com/PeculiarVentures/PKI.js/tree/41b63af760cacb565dd850fb3466ada4ca163eff) + +``` + + + + +``` diff --git a/csr.js b/csr.js new file mode 100644 index 0000000..8fd57bf --- /dev/null +++ b/csr.js @@ -0,0 +1,186 @@ +// Adapted from +// https://github.com/PeculiarVentures/PKI.js/blob/1bc0ed3/examples/PKCS%2310%20complex%20example/PKCS10_complex_example.html +// https://stackoverflow.com/questions/48419456/pki-js-v2-and-and-webcrypto-add-subject-alternative-name-with-csr-certificate/49896425#49896425 +// https://getwww.me/V1-csrhelp-master.zip +// unibabel.js + +// Dependencies +// +// +// +// +(function (exports) { +'use strict'; + +var CSR = exports.CSR = {}; + +function formatPEM(pem_string) { + var string_length = pem_string.length; + var result_string = ""; + + for (var i = 0, count = 0; i < string_length; i++, count++) { + if (count > 63) { + result_string = result_string + "\r\n"; + count = 0; + } + + result_string = result_string + pem_string[i]; + } + + return result_string; +} + +function arrayBufferToString(buffer) { + var result_string = ""; + var view = new Uint8Array(buffer); + + for (var i = 0; i < view.length; i++) { + result_string = result_string + String.fromCharCode(view[i]); + } + + return result_string; +} + +function createPkcs10Csr(domainKeypair, domains) { + var certf = {}; + certf.algorithm = 'ECC'; + certf.keysize = 'secp256r1'; + certf.hostname = domains.shift(); + + var context = {}; + + // #region Get a "crypto" extension + var crypto = org.pkijs.getCrypto(); + if (typeof crypto === "undefined") { + context.content = ''; + return Promise.reject('No WebCrypto extension found'); + } + // #endregion + + // #region Prepare P10 + context = context || {}; + var sequence = Promise.resolve(); + var pkcs10_simpl = new org.pkijs.simpl.PKCS10(); + var publicKey = domainKeypair.publicKey; + var privateKey = domainKeypair.privateKey; + var hash_algorithm; + hash_algorithm = "sha-256"; + + var signature_algorithm_name, keylength; + switch (certf.algorithm) { + case "RSA": + signature_algorithm_name = "RSASSA-PKCS1-V1_5"; + keylength = parseInt(certf.keysize); + break; + case "ECC": + signature_algorithm_name = "ECDSA"; + switch (certf.keysize) { + case 'secp256r1': + keylength = "P-256"; + break; + case 'secp384r1': + keylength = "P-384"; + break; + case 'secp521r1': + keylength = "P-521"; + break; + } + break; + default: + // do nothing + } + // #endregion + + // #region Put a static values + pkcs10_simpl.version = 0; + + pkcs10_simpl.subject.types_and_values.push(new org.pkijs.simpl.ATTR_TYPE_AND_VALUE({ + type: "2.5.4.3", + value: new org.pkijs.asn1.UTF8STRING({ + value: certf.hostname + }) + })); + + pkcs10_simpl.attributes = []; + // #endregion + + // #region Exporting public key into "subjectPublicKeyInfo" value of PKCS#10 + sequence = sequence.then(function() { + return pkcs10_simpl.subjectPublicKeyInfo.importKey(publicKey); + }); + // #endregion + + // #region SubjectKeyIdentifier + sequence = sequence.then(function() { + return crypto.digest({ + name: "SHA-256" + }, pkcs10_simpl.subjectPublicKeyInfo.subjectPublicKey.value_block.value_hex); + }).then(function(result) { + + var extensions = new org.pkijs.simpl.EXTENSIONS({ + extensions_array: [ + new org.pkijs.simpl.EXTENSION({ + extnID: "2.5.29.14", + critical: false, + extnValue: (new org.pkijs.asn1.OCTETSTRING({ value_hex: result })).toBER(false) + }) + ] + }); + + var altNames = new org.pkijs.simpl.GENERAL_NAMES({ + names: [ + new org.pkijs.simpl.GENERAL_NAME({ + NameType: 2, + Name: domains.join(', DNS:') //"domain1.com, DNS:domain2.com, DNS:domain3.com" + }) + ] + }); + + extensions.extensions_array.push(new org.pkijs.simpl.EXTENSION({ + extnID: "2.5.29.17", // subjectAltName + critical: false, + extnValue: altNames.toSchema().toBER(false) + })); + + var attribute = new org.pkijs.simpl.ATTRIBUTE({ + type: "1.2.840.113549.1.9.14", // pkcs-9-at-extensionRequest + values: [extensions.toSchema()] + }); + + pkcs10_simpl.attributes.push(attribute); + + }); + // #endregion + + // #region Signing final PKCS#10 request + sequence = sequence.then(function() { + context.privateKey = pkcs10_simpl.sign(privateKey, hash_algorithm); + return pkcs10_simpl.sign(privateKey, hash_algorithm); + }); + // #endregion + + sequence = sequence.then(function(/*result*/) { + var pkcs10_schema = pkcs10_simpl.toSchema(); + var pkcs10_encoded = pkcs10_schema.toBER(false); + + var pemder = window.btoa(arrayBufferToString(pkcs10_encoded)); + var result_string = "-----BEGIN CERTIFICATE REQUEST-----\r\n"; + result_string = result_string + formatPEM(pemder); + result_string = result_string + "\r\n-----END CERTIFICATE REQUEST-----\r\n"; + context.content = result_string; + + console.log('CSR as PEM:'); + console.log(context.content); + + return pemder.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); + }); + + return sequence; +} + +CSR.formatPem = formatPEM; +CSR.generate = function (options) { + return createPkcs10Csr(options.keypair, options.domains); +}; + +}(window)); diff --git a/package.json b/package.json new file mode 100644 index 0000000..38ff19c --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "browser-csr", + "version": "1.0.0-alpha", + "description": "Generate a CSR in your browser (i.e. for Let's Encrypt v2) with WebCrypto and PKIjs. Supports SAN altnames.", + "main": "csr.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "ssh://gitea@git.coolaj86.com:22042/coolaj86/browser-csr.js.git" + }, + "keywords": [ + "pki", + "pkijs", + "asn1", + "asn1js", + "Let's Encrypt", + "Let's Encrypt v2", + "letsencrypt", + "greenlock", + "browser", + "csr", + "certificate", + "signing", + "request", + "ACME" + ], + "author": "AJ ONeal (https://coolaj86.com/)", + "license": "(MIT OR Apache-2.0)" +}