From 372dbd083f66df1463691d8d45c7b154c90e5d9e Mon Sep 17 00:00:00 2001 From: Antoine Date: Wed, 12 Aug 2015 23:00:59 +0200 Subject: [PATCH 1/2] Added functionality to export key and certificate to PKCS12 keystore --- README.md | 14 ++++++++ lib/pem.js | 94 ++++++++++++++++++++++++++++++++++++++++++++++++----- test/pem.js | 38 ++++++++++++++++++++++ 3 files changed, 137 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6a213dac..c5a59b49 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,20 @@ Where * **certificate** is a PEM encoded certificate, CSR or private key * **callback** is a callback function with an error object and `{modulus}` +## Export to PKCS12 keystore + +Use `createPkcs12` to export a certificate and the private key to a PKCS12 keystore. + + pem.createPkcs12(clientKey, certificate, p12Password, [options], callback) + +Where + +* **clientKey** is a PEM encoded private key +* **certificate** is a PEM encoded certificate +* **p12Password** is the password of the exported keystore +* **options** is an optional options object with `cipher` and `clientKeyPassword` (ciphers:["aes128", "aes192", "aes256", "camellia128", "camellia192", "camellia256", "des", "des3", "idea"]) +* **callback** is a callback function with an error object and `{pkcs12}` (binary) + ### Setting openssl location In some systems the `openssl` executable might not be available by the default name or it is not included in $PATH. In this case you can define the location of the executable yourself as a one time action after you have loaded the pem module: diff --git a/lib/pem.js b/lib/pem.js index f6038a38..1790c313 100644 --- a/lib/pem.js +++ b/lib/pem.js @@ -19,6 +19,7 @@ module.exports.getPublicKey = getPublicKey; module.exports.getFingerprint = getFingerprint; module.exports.getModulus = getModulus; module.exports.getModulusFromProtected = getModulusFromProtected; +module.exports.createPkcs12 = createPkcs12; module.exports.config = config; // PUBLIC API @@ -223,6 +224,7 @@ function createCSR(options, callback) { * @param {String} [options.hash] Hash function to use (either md5 sha1 or sha256, defaults to sha256) * @param {String} [options.csr] CSR for the certificate, if not defined a new one is generated * @param {Number} [options.days] Certificate expire time in days + * @param {String} [options.clientKeyPassword] Password of the client key * @param {Function} callback Callback function with an error object and {certificate, csr, clientKey, serviceKey} */ function createCertificate(options, callback) { @@ -298,6 +300,11 @@ function createCertificate(options, callback) { tmpfiles.push(options.config); } + if(options.clientKeyPassword){ + params.push('-passin'); + params.push('pass:' + options.clientKeyPassword); + } + execOpenSSL(params, 'CERTIFICATE', tmpfiles, function(error, data) { if (error) { return callback(error); @@ -513,6 +520,51 @@ function getFingerprint(certificate, hash, callback) { }); } +/** + * Export private key and certificate to a PKCS12 keystore + * + * @param {String} PEM encoded private key + * @param {String} PEM encoded certificate + * @param {String} Password of the result PKCS12 file + * @param {Object} [options] object of cipher and optional client key password {cipher:'aes128', clientKeyPassword: 'xxx'} + * @param {Function} callback Callback function with an error object and {pkcs12} + */ +function createPkcs12(key, certificate, password, options, callback) { + if (!callback && typeof options === 'function') { + callback = options; + options = {}; + } + + var params = ['pkcs12','-export']; + var cipher = ['aes128', 'aes192', 'aes256', 'camellia128', 'camellia192', 'camellia256', 'des', 'des3', 'idea']; + if (options && options.cipher && ( -1 !== Number(cipher.indexOf(options.cipher)) )){ + params.push( '-' + options.cipher ); + } + if(options && options.clientKeyPassword){ + params.push('-passin'); + params.push('pass:' + options.clientKeyPassword); + } + params.push( '-password' ); + params.push( 'pass:' + password ); + + params.push( '-in' ); + params.push('--TMPFILE--'); + params.push( '-inkey' ); + params.push('--TMPFILE--'); + + var tmpfiles = [certificate, key]; + + execBinaryOpenSSL(params, tmpfiles, function(error, pkcs12) { + if (error) { + return callback(error); + } + return callback(null, { + pkcs12: pkcs12 + }); + }); +} + + // HELPER FUNCTIONS function fetchCertificateData(certData, callback) { @@ -668,29 +720,32 @@ function generateCSRSubject(options) { * Generically spawn openSSL, without processing the result * * @param {Array} params The parameters to pass to openssl - * @param {String|Array} tmpfiles Stuff to pass to tmpfiles + * @param {Boolean} binary Output of openssl is binary or text * @param {Function} callback Called with (error, exitCode, stdout, stderr) */ -function spawnOpenSSL(params, callback) { +function spawnOpenSSL(params, binary, callback) { var pathBin = pathOpenSSL || process.env.OPENSSL_BIN || 'openssl'; testOpenSSLPath(pathBin, function(err) { if (err) { return callback(err); } - var openssl = spawn(pathBin, params), - stdout = '', stderr = ''; + var stdout = (binary ? new Buffer(0) : ''); openssl.stdout.on('data', function(data) { - stdout += (data || '').toString('binary'); + if(!binary){ + stdout += (data || '').toString('binary'); + } + else{ + stdout = Buffer.concat([stdout, data]); + } }); openssl.stderr.on('data', function(data) { stderr += (data || '').toString('binary'); }); - // We need both the return code and access to all of stdout. Stdout isn't // *really* available until the close event fires; the timing nuance was // making this fail periodically. @@ -725,14 +780,19 @@ function spawnOpenSSL(params, callback) { }); openssl.on('close', function() { - stdout = new Buffer(stdout, 'binary').toString('utf-8'); + stdout = (binary ? stdout : new Buffer(stdout, 'binary').toString('utf-8')); stderr = new Buffer(stderr, 'binary').toString('utf-8'); done(); }); }); } -function spawnWrapper(params, tmpfiles, callback) { +function spawnWrapper(params, tmpfiles, binary, callback) { + if (!callback && typeof binary === 'function') { + callback = binary; + binary = false; + } + var files = []; var toUnlink = []; @@ -765,7 +825,7 @@ function spawnWrapper(params, tmpfiles, callback) { }; var spawnSSL = function() { - spawnOpenSSL(params, function(err, code, stdout, stderr) { + spawnOpenSSL(params, binary, function(err, code, stdout, stderr) { toUnlink.forEach(function(filePath) { fs.unlink(filePath); }); @@ -813,6 +873,22 @@ function execOpenSSL(params, searchStr, tmpfiles, callback) { }); } +/** + * Spawn an openssl command and get binary output + **/ + function execBinaryOpenSSL(params, tmpfiles, callback){ + if (!callback && typeof tmpfiles === 'function') { + callback = tmpfiles; + tmpfiles = false; + } + spawnWrapper(params, tmpfiles, true, function(err, code, stdout, stderr) { + if(err) { + return callback(err); + } + return callback(null, stdout); + }); + } + /** * Validates the pathBin for the openssl command. * diff --git a/test/pem.js b/test/pem.js index 09c08f88..ee6c638a 100644 --- a/test/pem.js +++ b/test/pem.js @@ -459,5 +459,43 @@ exports['General Tests'] = { test.done(); }); }); + }, + 'Create PKCS12 without key password': function(test) { + pem.createPrivateKey(function(error, data) { + var key = (data && data.key || '').toString(); + + pem.createCertificate({ + clientKey: key, + selfSigned: true + }, function(error, csr) { + + pem.createPkcs12(csr.clientKey, csr.certificate, 'mypassword', function(err,pkcs12){ + test.ifError(err); + test.ok(pkcs12); + + test.ok(fs.readdirSync('./tmp').length === 0); + test.done(); + }); + }); + }); + }, + 'Create PKCS12 with key password': function(test) { + pem.createPrivateKey({cipher:'aes128',password:'xxx'}, function(error, data) { + var key = (data && data.key || '').toString(); + + pem.createCertificate({ + clientKey: key, + selfSigned: true + }, function(error, csr) { + + pem.createPkcs12(csr.clientKey, csr.certificate, 'mypassword', {cipher: 'aes256', clientKeyPassword: 'xxx'}, function(err,pkcs12){ + test.ifError(err); + test.ok(pkcs12); + + test.ok(fs.readdirSync('./tmp').length === 0); + test.done(); + }); + }); + }); } }; \ No newline at end of file From 1e71e3f32ec0af23468b8d3ff732e10c1c9506ca Mon Sep 17 00:00:00 2001 From: Antoine Date: Wed, 12 Aug 2015 23:07:11 +0200 Subject: [PATCH 2/2] Remove unused parameter --- lib/pem.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pem.js b/lib/pem.js index 1790c313..d0ce8963 100644 --- a/lib/pem.js +++ b/lib/pem.js @@ -881,7 +881,7 @@ function execOpenSSL(params, searchStr, tmpfiles, callback) { callback = tmpfiles; tmpfiles = false; } - spawnWrapper(params, tmpfiles, true, function(err, code, stdout, stderr) { + spawnWrapper(params, tmpfiles, true, function(err, code, stdout) { if(err) { return callback(err); }