Skip to content

Commit

Permalink
Merge pull request #59 from adetante/master
Browse files Browse the repository at this point in the history
Added functionality to export key and certificate to PKCS12 keystore
  • Loading branch information
Dexus committed Aug 17, 2015
2 parents 9e23b92 + 1e71e3f commit 5bc4716
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 9 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
94 changes: 85 additions & 9 deletions lib/pem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 = [];

Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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) {
if(err) {
return callback(err);
}
return callback(null, stdout);
});
}

/**
* Validates the pathBin for the openssl command.
*
Expand Down
38 changes: 38 additions & 0 deletions test/pem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
}
};

0 comments on commit 5bc4716

Please sign in to comment.