'use strict' /** * pem module * * @module pem */ const { promisify } = require('es6-promisify') var net = require('net') var helper = require('./helper.js') var openssl = require('./openssl.js') module.exports.createPrivateKey = createPrivateKey module.exports.createDhparam = createDhparam module.exports.createEcparam = createEcparam module.exports.createCSR = createCSR module.exports.createCertificate = createCertificate module.exports.readCertificateInfo = readCertificateInfo module.exports.getPublicKey = getPublicKey module.exports.getFingerprint = getFingerprint module.exports.getModulus = getModulus module.exports.getDhparamInfo = getDhparamInfo module.exports.createPkcs12 = createPkcs12 module.exports.readPkcs12 = readPkcs12 module.exports.verifySigningChain = verifySigningChain module.exports.checkCertificate = checkCertificate module.exports.checkPkcs12 = checkPkcs12 module.exports.config = config /** * quick access the convert module * @type {module:convert} */ module.exports.convert = require('./convert.js') var KEY_START = '-----BEGIN PRIVATE KEY-----' var KEY_END = '-----END PRIVATE KEY-----' var RSA_KEY_START = '-----BEGIN RSA PRIVATE KEY-----' var RSA_KEY_END = '-----END RSA PRIVATE KEY-----' var ENCRYPTED_KEY_START = '-----BEGIN ENCRYPTED PRIVATE KEY-----' var ENCRYPTED_KEY_END = '-----END ENCRYPTED PRIVATE KEY-----' var CERT_START = '-----BEGIN CERTIFICATE-----' var CERT_END = '-----END CERTIFICATE-----' /** * Creates a private key * * @static * @param {Number} [keyBitsize=2048] Size of the key, defaults to 2048bit * @param {Object} [options] object of cipher and password {cipher:'aes128',password:'xxx'}, defaults empty object * @param {String} [options.cipher] string of the cipher for the encryption - needed with password * @param {String} [options.password] string of the cipher password for the encryption needed with cipher * @param {Function} callback Callback function with an error object and {key} */ function createPrivateKey (keyBitsize, options, callback) { if (!callback && !options && typeof keyBitsize === 'function') { callback = keyBitsize keyBitsize = undefined options = {} } else if (!callback && keyBitsize && typeof options === 'function') { callback = options options = {} } keyBitsize = Number(keyBitsize) || 2048 var params = ['genrsa'] var delTempPWFiles = [] if (options && options.cipher && (Number(helper.ciphers.indexOf(options.cipher)) !== -1) && options.password) { helper.createPasswordFile({ 'cipher': options.cipher, 'password': options.password, 'passType': 'out' }, params, delTempPWFiles) } params.push(keyBitsize) openssl.exec(params, 'RSA PRIVATE KEY', function (sslErr, key) { function done (err) { if (err) { return callback(err) } callback(null, { key: key }) } helper.deleteTempFiles(delTempPWFiles, function (fsErr) { done(sslErr || fsErr) }) }) } /** * Creates a dhparam key * * @static * @param {Number} [keyBitsize=512] Size of the key, defaults to 512bit * @param {Function} callback Callback function with an error object and {dhparam} */ function createDhparam (keyBitsize, callback) { if (!callback && typeof keyBitsize === 'function') { callback = keyBitsize keyBitsize = undefined } keyBitsize = Number(keyBitsize) || 512 var params = ['dhparam', '-outform', 'PEM', keyBitsize ] openssl.exec(params, 'DH PARAMETERS', function (error, dhparam) { if (error) { return callback(error) } return callback(null, { dhparam: dhparam }) }) } /** * Creates a ecparam key * @static * @param {String} [keyName=secp256k1] Name of the key, defaults to secp256k1 * @param {String} [paramEnc=explicit] Encoding of the elliptic curve parameters, defaults to explicit * @param {Boolean} [noOut=false] This option inhibits the output of the encoded version of the parameters. * @param {Function} callback Callback function with an error object and {ecparam} */ function createEcparam (keyName, paramEnc, noOut, callback) { if (!callback && typeof noOut === 'undefined' && !paramEnc && typeof keyName === 'function') { callback = keyName keyName = undefined } else if (!callback && typeof noOut === 'undefined' && keyName && typeof paramEnc === 'function') { callback = paramEnc paramEnc = undefined } else if (!callback && typeof noOut === 'function' && keyName && paramEnc) { callback = noOut noOut = undefined } keyName = keyName || 'secp256k1' paramEnc = paramEnc || 'explicit' noOut = noOut || false var params = ['ecparam', '-name', keyName, '-genkey', '-param_enc', paramEnc ] var searchString = 'EC PARAMETERS' if (noOut) { params.push('-noout') searchString = 'EC PRIVATE KEY' } openssl.exec(params, searchString, function (error, ecparam) { if (error) { return callback(error) } return callback(null, { ecparam: ecparam }) }) } /** * Creates a Certificate Signing Request * If client key is undefined, a new key is created automatically. The used key is included * in the callback return as clientKey * @static * @param {Object} [options] Optional options object * @param {String} [options.clientKey] Optional client key to use * @param {Number} [options.keyBitsize] If clientKey is undefined, bit size to use for generating a new key (defaults to 2048) * @param {String} [options.hash] Hash function to use (either md5 sha1 or sha256, defaults to sha256) * @param {String} [options.country] CSR country field * @param {String} [options.state] CSR state field * @param {String} [options.locality] CSR locality field * @param {String} [options.organization] CSR organization field * @param {String} [options.organizationUnit] CSR organizational unit field * @param {String} [options.commonName='localhost'] CSR common name field * @param {String} [options.emailAddress] CSR email address field * @param {String} [options.csrConfigFile] CSR config file * @param {Array} [options.altNames] is a list of subjectAltNames in the subjectAltName field * @param {Function} callback Callback function with an error object and {csr, clientKey} */ function createCSR (options, callback) { if (!callback && typeof options === 'function') { callback = options options = undefined } options = options || {} // http://stackoverflow.com/questions/14089872/why-does-node-js-accept-ip-addresses-in-certificates-only-for-san-not-for-cn if (options.commonName && (net.isIPv4(options.commonName) || net.isIPv6(options.commonName))) { if (!options.altNames) { options.altNames = [options.commonName] } else if (options.altNames.indexOf(options.commonName) === -1) { options.altNames = options.altNames.concat([options.commonName]) } } if (!options.clientKey) { createPrivateKey(options.keyBitsize || 2048, function (error, keyData) { if (error) { return callback(error) } options.clientKey = keyData.key createCSR(options, callback) }) return } var params = ['req', '-new', '-' + (options.hash || 'sha256') ] if (options.csrConfigFile) { params.push('-config') params.push(options.csrConfigFile) } else { params.push('-subj') params.push(generateCSRSubject(options)) } params.push('-key') params.push('--TMPFILE--') var tmpfiles = [options.clientKey] var config = null if (options.altNames && Array.isArray(options.altNames) && options.altNames.length) { params.push('-extensions') params.push('v3_req') params.push('-config') params.push('--TMPFILE--') var altNamesRep = [] for (var i = 0; i < options.altNames.length; i++) { altNamesRep.push((net.isIP(options.altNames[i]) ? 'IP' : 'DNS') + '.' + (i + 1) + ' = ' + options.altNames[i]) } tmpfiles.push(config = [ '[req]', 'req_extensions = v3_req', 'distinguished_name = req_distinguished_name', '[v3_req]', 'subjectAltName = @alt_names', '[alt_names]', altNamesRep.join('\n'), '[req_distinguished_name]', 'commonName = Common Name', 'commonName_max = 64' ].join('\n')) } else if (options.config) { config = options.config } var delTempPWFiles = [] if (options.clientKeyPassword) { helper.createPasswordFile({ 'cipher': '', 'password': options.clientKeyPassword, 'passType': 'in' }, params, delTempPWFiles) } openssl.exec(params, 'CERTIFICATE REQUEST', tmpfiles, function (sslErr, data) { function done (err) { if (err) { return callback(err) } callback(null, { csr: data, config: config, clientKey: options.clientKey }) } helper.deleteTempFiles(delTempPWFiles, function (fsErr) { done(sslErr || fsErr) }) }) } /** * Creates a certificate based on a CSR. If CSR is not defined, a new one * will be generated automatically. For CSR generation all the options values * can be used as with createCSR. * @static * @param {Object} [options] Optional options object * @param {String} [options.serviceCertificate] PEM encoded certificate * @param {String} [options.serviceKey] Private key for signing the certificate, if not defined a new one is generated * @param {String} [options.serviceKeyPassword] Password of the service key * @param {Boolean} [options.selfSigned] If set to true and serviceKey is not defined, use clientKey for signing * @param {String|Number} [options.serial] Set a serial max. 20 octets - only together with options.serviceCertificate * @param {String} [options.serialFile] Set the name of the serial file, without extension. - only together with options.serviceCertificate and never in tandem with options.serial * @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 {String} [options.extFile] extension config file - without '-extensions v3_req' * @param {String} [options.config] extension config file - with '-extensions v3_req' * @param {String} [options.csrConfigFile] CSR config file - only used if no options.csr is provided * @param {Array} [options.altNames] is a list of subjectAltNames in the subjectAltName field - only used if no options.csr is provided * @param {Function} callback Callback function with an error object and {certificate, csr, clientKey, serviceKey} */ function createCertificate (options, callback) { if (!callback && typeof options === 'function') { callback = options options = undefined } options = options || {} if (!options.csr) { createCSR(options, function (error, keyData) { if (error) { return callback(error) } options.csr = keyData.csr options.config = keyData.config options.clientKey = keyData.clientKey createCertificate(options, callback) }) return } if (!options.clientKey) { options.clientKey = '' } if (!options.serviceKey) { if (options.selfSigned) { options.serviceKey = options.clientKey } else { createPrivateKey(options.keyBitsize || 2048, function (error, keyData) { if (error) { return callback(error) } options.serviceKey = keyData.key createCertificate(options, callback) }) return } } readCertificateInfo(options.csr, function (error2, data2) { if (error2) { return callback(error2) } var params = ['x509', '-req', '-' + (options.hash || 'sha256'), '-days', Number(options.days) || '365', '-in', '--TMPFILE--' ] var tmpfiles = [options.csr] var delTempPWFiles = [] if (options.serviceCertificate) { params.push('-CA') params.push('--TMPFILE--') params.push('-CAkey') params.push('--TMPFILE--') if (options.serial) { params.push('-set_serial') if (helper.isNumber(options.serial)) { // set the serial to the max lenth of 20 octets () // A certificate serial number is not decimal conforming. That is the // bytes in a serial number do not necessarily map to a printable ASCII // character. // eg: 0x00 is a valid serial number and can not be represented in a // human readable format (atleast one that can be directly mapped to // the ACSII table). params.push('0x' + ('0000000000000000000000000000000000000000' + options.serial.toString(16)).slice(-40)) } else { if (helper.isHex(options.serial)) { if (options.serial.startsWith('0x')) { options.serial = options.serial.substring(2, options.serial.length) } params.push('0x' + ('0000000000000000000000000000000000000000' + options.serial).slice(-40)) } else { params.push('0x' + ('0000000000000000000000000000000000000000' + helper.toHex(options.serial)).slice(-40)) } } } else { params.push('-CAcreateserial') if (options.serialFile) { params.push('-CAserial') params.push(options.serialFile + '.srl') } } if (options.serviceKeyPassword) { helper.createPasswordFile({ 'cipher': '', 'password': options.serviceKeyPassword, 'passType': 'in' }, params, delTempPWFiles) } tmpfiles.push(options.serviceCertificate) tmpfiles.push(options.serviceKey) } else { params.push('-signkey') params.push('--TMPFILE--') if (options.serviceKeyPassword) { helper.createPasswordFile({ 'cipher': '', 'password': options.serviceKeyPassword, 'passType': 'in' }, params, delTempPWFiles) } tmpfiles.push(options.serviceKey) } if (options.config) { params.push('-extensions') params.push('v3_req') params.push('-extfile') params.push('--TMPFILE--') tmpfiles.push(options.config) } else if (options.extFile) { params.push('-extfile') params.push(options.extFile) } else { var altNamesRep = [] if (data2 && data2.san) { for (var i = 0; i < data2.san.dns.length; i++) { altNamesRep.push('DNS' + '.' + (i + 1) + ' = ' + data2.san.dns[i]) } for (var i2 = 0; i2 < data2.san.ip.length; i2++) { altNamesRep.push('IP' + '.' + (i2 + 1) + ' = ' + data2.san.ip[i2]) } for (var i3 = 0; i3 < data2.san.email.length; i3++) { altNamesRep.push('email' + '.' + (i3 + 1) + ' = ' + data2.san.email[i3]) } params.push('-extensions') params.push('v3_req') params.push('-extfile') params.push('--TMPFILE--') tmpfiles.push([ '[v3_req]', 'subjectAltName = @alt_names', '[alt_names]', altNamesRep.join('\n') ].join('\n')) } } if (options.clientKeyPassword) { helper.createPasswordFile({ 'cipher': '', 'password': options.clientKeyPassword, 'passType': 'in' }, params, delTempPWFiles) } openssl.exec(params, 'CERTIFICATE', tmpfiles, function (sslErr, data) { function done (err) { if (err) { return callback(err) } var response = { csr: options.csr, clientKey: options.clientKey, certificate: data, serviceKey: options.serviceKey } return callback(null, response) } helper.deleteTempFiles(delTempPWFiles, function (fsErr) { done(sslErr || fsErr) }) }) }) } /** * Exports a public key from a private key, CSR or certificate * @static * @param {String} certificate PEM encoded private key, CSR or certificate * @param {Function} callback Callback function with an error object and {publicKey} */ function getPublicKey (certificate, callback) { if (!callback && typeof certificate === 'function') { callback = certificate certificate = undefined } certificate = (certificate || '').toString() var params if (certificate.match(/BEGIN(\sNEW)? CERTIFICATE REQUEST/)) { params = ['req', '-in', '--TMPFILE--', '-pubkey', '-noout' ] } else if (certificate.match(/BEGIN RSA PRIVATE KEY/) || certificate.match(/BEGIN PRIVATE KEY/)) { params = ['rsa', '-in', '--TMPFILE--', '-pubout' ] } else { params = ['x509', '-in', '--TMPFILE--', '-pubkey', '-noout' ] } openssl.exec(params, 'PUBLIC KEY', certificate, function (error, key) { if (error) { return callback(error) } return callback(null, { publicKey: key }) }) } /** * Reads subject data from a certificate or a CSR * @static * @param {String} certificate PEM encoded CSR or certificate * @param {Function} callback Callback function with an error object and {country, state, locality, organization, organizationUnit, commonName, emailAddress} */ function readCertificateInfo (certificate, callback) { if (!callback && typeof certificate === 'function') { callback = certificate certificate = undefined } certificate = (certificate || '').toString() var isMatch = certificate.match(/BEGIN(\sNEW)? CERTIFICATE REQUEST/) var type = isMatch ? 'req' : 'x509' var params = [type, '-noout', '-nameopt', 'RFC2253,sep_multiline,space_eq,-esc_msb,utf8', '-text', '-in', '--TMPFILE--' ] openssl.spawnWrapper(params, certificate, function (err, code, stdout, stderr) { if (err) { return callback(err) } else if (stderr) { return callback(stderr) } return fetchCertificateData(stdout, callback) }) } /** * get the modulus from a certificate, a CSR or a private key * @static * @param {String} certificate PEM encoded, CSR PEM encoded, or private key * @param {String} [password] password for the certificate * @param {String} [hash] hash function to use (up to now `md5` supported) (default: none) * @param {Function} callback Callback function with an error object and {modulus} */ function getModulus (certificate, password, hash, callback) { if (!callback && !hash && typeof password === 'function') { callback = password password = undefined hash = false } else if (!callback && hash && typeof hash === 'function') { callback = hash hash = false // password will be falsy if not provided } // adding hash function to params, is not supported by openssl. // process piping would be the right way (... | openssl md5) // No idea how this can be achieved in easy with the current build in methods // of pem. if (hash && hash !== 'md5') { hash = false } certificate = (Buffer.isBuffer(certificate) && certificate.toString()) || certificate var type = '' if (certificate.match(/BEGIN(\sNEW)? CERTIFICATE REQUEST/)) { type = 'req' } else if (certificate.match(/BEGIN RSA PRIVATE KEY/) || certificate.match(/BEGIN PRIVATE KEY/)) { type = 'rsa' } else { type = 'x509' } var params = [ type, '-noout', '-modulus', '-in', '--TMPFILE--' ] var delTempPWFiles = [] if (password) { helper.createPasswordFile({ 'cipher': '', 'password': password, 'passType': 'in' }, params, delTempPWFiles) } openssl.spawnWrapper(params, certificate, function (sslErr, code, stdout, stderr) { function done (err) { if (err) { return callback(err) } var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m) if (match) { return callback(null, { modulus: hash ? require(hash)(match[1]) : match[1] }) } else { return callback(new Error('No modulus')) } } helper.deleteTempFiles(delTempPWFiles, function (fsErr) { done(sslErr || fsErr || stderr) }) }) } /** * get the size and prime of DH parameters * @static * @param {String} DH parameters PEM encoded * @param {Function} callback Callback function with an error object and {size, prime} */ function getDhparamInfo (dh, callback) { dh = (Buffer.isBuffer(dh) && dh.toString()) || dh var params = [ 'dhparam', '-text', '-in', '--TMPFILE--' ] openssl.spawnWrapper(params, dh, function (err, code, stdout, stderr) { if (err) { return callback(err) } else if (stderr) { return callback(stderr) } var result = {} var match = stdout.match(/Parameters: \((\d+) bit\)/) if (match) { result.size = Number(match[1]) } var prime = '' stdout.split('\n').forEach(function (line) { if (/\s+([0-9a-f][0-9a-f]:)+[0-9a-f]?[0-9a-f]?/g.test(line)) { prime += line.trim() } }) if (prime) { result.prime = prime } if (!match && !prime) { return callback(new Error('No DH info found')) } return callback(null, result) }) } /** * config the pem module * @static * @param {Object} options */ function config (options) { Object.keys(options).forEach(function (k) { openssl.set(k, options[k]) }) } /** * Gets the fingerprint for a certificate * @static * @param {String} PEM encoded certificate * @param {String} [hash] hash function to use (either `md5`, `sha1` or `sha256`, defaults to `sha1`) * @param {Function} callback Callback function with an error object and {fingerprint} */ function getFingerprint (certificate, hash, callback) { if (!callback && typeof hash === 'function') { callback = hash hash = undefined } hash = hash || 'sha1' var params = ['x509', '-in', '--TMPFILE--', '-fingerprint', '-noout', '-' + hash ] openssl.spawnWrapper(params, certificate, function (err, code, stdout, stderr) { if (err) { return callback(err) } else if (stderr) { return callback(stderr) } var match = stdout.match(/Fingerprint=([0-9a-fA-F:]+)$/m) if (match) { return callback(null, { fingerprint: match[1] }) } else { return callback(new Error('No fingerprint')) } }) } /** * Export private key and certificate to a PKCS12 keystore * @static * @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: 'xxxx', certFiles: ['file1','file2']} * @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 delTempPWFiles = [] if (options.cipher && options.clientKeyPassword) { // NOTICE: The password field is needed! self if it is empty. // create password file for the import "-passin" helper.createPasswordFile({ 'cipher': options.cipher, 'password': options.clientKeyPassword, 'passType': 'in' }, params, delTempPWFiles) } // NOTICE: The password field is needed! self if it is empty. // create password file for the password "-password" helper.createPasswordFile({ 'cipher': '', 'password': password, 'passType': 'word' }, params, delTempPWFiles) params.push('-in') params.push('--TMPFILE--') params.push('-inkey') params.push('--TMPFILE--') var tmpfiles = [certificate, key] if (options.certFiles) { tmpfiles.push(options.certFiles.join('')) params.push('-certfile') params.push('--TMPFILE--') } openssl.execBinary(params, tmpfiles, function (sslErr, pkcs12) { function done (err) { if (err) { return callback(err) } return callback(null, { pkcs12: pkcs12 }) } helper.deleteTempFiles(delTempPWFiles, function (fsErr) { done(sslErr || fsErr) }) }) } /** * read sslcert data from Pkcs12 file. Results are provided in callback response in object notation ({cert: .., ca:..., key:...}) * @static * @param {Buffer|String} bufferOrPath Buffer or path to file * @param {Object} [options] openssl options * @param {Function} callback Called with error object and sslcert bundle object */ function readPkcs12 (bufferOrPath, options, callback) { if (!callback && typeof options === 'function') { callback = options options = {} } options.p12Password = options.p12Password || '' var tmpfiles = [] var delTempPWFiles = [] var args = ['pkcs12', '-in', bufferOrPath] helper.createPasswordFile({ 'cipher': '', 'password': options.p12Password, 'passType': 'in' }, args, delTempPWFiles) if (Buffer.isBuffer(bufferOrPath)) { tmpfiles = [bufferOrPath] args[2] = '--TMPFILE--' } if (options.clientKeyPassword) { helper.createPasswordFile({ 'cipher': '', 'password': options.clientKeyPassword, 'passType': 'out' }, args, delTempPWFiles) } else { args.push('-nodes') } openssl.execBinary(args, tmpfiles, function (sslErr, stdout) { function done (err) { var keybundle = {} if (err && err.message.indexOf('No such file or directory') !== -1) { err.code = 'ENOENT' } if (!err) { var certs = readFromString(stdout, CERT_START, CERT_END) keybundle.cert = certs.shift() keybundle.ca = certs keybundle.key = readFromString(stdout, KEY_START, KEY_END).pop() if (keybundle.key) { // convert to RSA key return openssl.exec(['rsa', '-in', '--TMPFILE--'], 'RSA PRIVATE KEY', [keybundle.key], function (err, key) { keybundle.key = key return callback(err, keybundle) }) } if (options.clientKeyPassword) { keybundle.key = readFromString(stdout, ENCRYPTED_KEY_START, ENCRYPTED_KEY_END).pop() } else { keybundle.key = readFromString(stdout, RSA_KEY_START, RSA_KEY_END).pop() } } return callback(err, keybundle) } helper.deleteTempFiles(delTempPWFiles, function (fsErr) { done(sslErr || fsErr) }) }) } /** * Check a certificate * @static * @param {String} PEM encoded certificate * @param {String} [passphrase] password for the certificate * @param {Function} callback Callback function with an error object and a boolean valid */ function checkCertificate (certificate, passphrase, callback) { var params var delTempPWFiles = [] if (!callback && typeof passphrase === 'function') { callback = passphrase passphrase = undefined } certificate = (certificate || '').toString() if (certificate.match(/BEGIN(\sNEW)? CERTIFICATE REQUEST/)) { params = ['req', '-text', '-noout', '-verify', '-in', '--TMPFILE--'] } else if (certificate.match(/BEGIN RSA PRIVATE KEY/) || certificate.match(/BEGIN PRIVATE KEY/)) { params = ['rsa', '-noout', '-check', '-in', '--TMPFILE--'] } else { params = ['x509', '-text', '-noout', '-in', '--TMPFILE--'] } if (passphrase) { helper.createPasswordFile({ 'cipher': '', 'password': passphrase, 'passType': 'in' }, params, delTempPWFiles) } openssl.spawnWrapper(params, certificate, function (sslErr, code, stdout, stderr) { function done (err) { if (err && err.toString().trim() !== 'verify OK') { return callback(err) } var result switch (params[0]) { case 'rsa': result = /^Rsa key ok$/i.test(stdout.trim()) break default: result = /Signature Algorithm/im.test(stdout) break } callback(null, result) } helper.deleteTempFiles(delTempPWFiles, function (fsErr) { done(sslErr || fsErr || stderr) }) }) } /** * check a PKCS#12 file (.pfx or.p12) * @static * @param {Buffer|String} bufferOrPath PKCS#12 certificate * @param {String} [passphrase] optional passphrase which will be used to open the keystore * @param {Function} callback Callback function with an error object and a boolean valid */ function checkPkcs12 (bufferOrPath, passphrase, callback) { if (!callback && typeof passphrase === 'function') { callback = passphrase passphrase = '' } var tmpfiles = [] var delTempPWFiles = [] var args = ['pkcs12', '-info', '-in', bufferOrPath, '-noout', '-maciter', '-nodes'] helper.createPasswordFile({ 'cipher': '', 'password': passphrase, 'passType': 'in' }, args, delTempPWFiles) if (Buffer.isBuffer(bufferOrPath)) { tmpfiles = [bufferOrPath] args[3] = '--TMPFILE--' } openssl.spawnWrapper(args, tmpfiles, function (sslErr, code, stdout, stderr) { function done (err) { if (err) { return callback(err) } callback(null, (/MAC verified OK/im.test(stderr) || (!(/MAC verified OK/im.test(stderr)) && !(/Mac verify error/im.test(stderr))))) } helper.deleteTempFiles(delTempPWFiles, function (fsErr) { done(sslErr || fsErr) }) }) } /** * Verifies the signing chain of the passed certificate * @static * @param {String|Array} PEM encoded certificate include intermediate certificates * @param {String|Array} [List] of CA certificates * @param {Function} callback Callback function with an error object and a boolean valid */ function verifySigningChain (certificate, ca, callback) { if (!callback && typeof ca === 'function') { callback = ca ca = undefined } if (!Array.isArray(certificate)) { certificate = [certificate] } if (!Array.isArray(ca) && ca !== undefined) { if (ca !== '') { ca = [ca] } } var files = [] if (ca !== undefined) { // ca certificates files.push(ca.join('\n')) } // certificate incl. intermediate certificates files.push(certificate.join('\n')) var params = ['verify'] if (ca !== undefined) { // ca certificates params.push('-CAfile') params.push('--TMPFILE--') } // certificate incl. intermediate certificates params.push('--TMPFILE--') openssl.spawnWrapper(params, files, function (err, code, stdout, stderr) { if (err) { return callback(err) } callback(null, stdout.trim().slice(-4) === ': OK') }) } // HELPER FUNCTIONS function fetchCertificateData (certData, callback) { certData = (certData || '').toString() var serial, subject, tmp, issuer var certValues = { issuer: {} } var validity = {} var san var ky, i // serial if ((serial = certData.match(/\s*Serial Number:\r?\n?\s*([^\r\n]*)\r?\n\s*\b/)) && serial.length > 1) { certValues.serial = serial[1] } if ((subject = certData.match(/\s*Subject:\r?\n(\s*(([a-zA-Z0-9.]+)\s=\s[^\r\n]+\r?\n))*\s*\b/)) && subject.length > 1) { subject = subject[0] tmp = matchAll(subject, /\s([a-zA-Z0-9.]+)\s=\s([^\r\n].*)/g) for (i = 0; i < tmp.length; i++) { ky = tmp[i][1].trim() if (ky.match('(C|ST|L|O|OU|CN|emailAddress|DC)') || ky === '') { continue } certValues[ky] = tmp[i][2].trim() } // country tmp = subject.match(/\sC\s=\s([^\r\n].*?)[\r\n]/) certValues.country = (tmp && tmp[1]) || '' // state tmp = subject.match(/\sST\s=\s([^\r\n].*?)[\r\n]/) certValues.state = (tmp && tmp[1]) || '' // locality tmp = subject.match(/\sL\s=\s([^\r\n].*?)[\r\n]/) certValues.locality = (tmp && tmp[1]) || '' // organization tmp = matchAll(subject, /\sO\s=\s([^\r\n].*)/g) certValues.organization = tmp ? (tmp.length > 1 ? tmp.sort(function (t, n) { var e = t[1].toUpperCase() var r = n[1].toUpperCase() return r > e ? -1 : e > r ? 1 : 0 }).sort(function (t, n) { return t[1].length - n[1].length }).map(function (t) { return t[1] }) : tmp[0][1]) : '' // unit tmp = matchAll(subject, /\sOU\s=\s([^\r\n].*)/g) certValues.organizationUnit = tmp ? (tmp.length > 1 ? tmp.sort(function (t, n) { var e = t[1].toUpperCase() var r = n[1].toUpperCase() return r > e ? -1 : e > r ? 1 : 0 }).sort(function (t, n) { return t[1].length - n[1].length }).map(function (t) { return t[1] }) : tmp[0][1]) : '' // common name tmp = matchAll(subject, /\sCN\s=\s([^\r\n].*)/g) certValues.commonName = tmp ? (tmp.length > 1 ? tmp.sort(function (t, n) { var e = t[1].toUpperCase() var r = n[1].toUpperCase() return r > e ? -1 : e > r ? 1 : 0 }).sort(function (t, n) { return t[1].length - n[1].length }).map(function (t) { return t[1] }) : tmp[0][1]) : '' // email tmp = matchAll(subject, /emailAddress\s=\s([^\r\n].*)/g) certValues.emailAddress = tmp ? (tmp.length > 1 ? tmp.sort(function (t, n) { var e = t[1].toUpperCase() var r = n[1].toUpperCase() return r > e ? -1 : e > r ? 1 : 0 }).sort(function (t, n) { return t[1].length - n[1].length }).map(function (t) { return t[1] }) : tmp[0][1]) : '' // DC name tmp = matchAll(subject, /\sDC\s=\s([^\r\n].*)/g) certValues.dc = tmp ? (tmp.length > 1 ? tmp.sort(function (t, n) { var e = t[1].toUpperCase() var r = n[1].toUpperCase() return r > e ? -1 : e > r ? 1 : 0 }).sort(function (t, n) { return t[1].length - n[1].length }).map(function (t) { return t[1] }) : tmp[0][1]) : '' } if ((issuer = certData.match(/\s*Issuer:\r?\n(\s*([a-zA-Z0-9.]+)\s=\s[^\r\n].*\r?\n)*\s*\b/)) && issuer.length > 1) { issuer = issuer[0] tmp = matchAll(issuer, /\s([a-zA-Z0-9.]+)\s=\s([^\r\n].*)/g) for (i = 0; i < tmp.length; i++) { ky = tmp[i][1].toString() if (ky.match('(C|ST|L|O|OU|CN|emailAddress|DC)')) { continue } certValues.issuer[ky] = tmp[i][2].toString() } // country tmp = issuer.match(/\sC\s=\s([^\r\n].*?)[\r\n]/) certValues.issuer.country = (tmp && tmp[1]) || '' // state tmp = issuer.match(/\sST\s=\s([^\r\n].*?)[\r\n]/) certValues.issuer.state = (tmp && tmp[1]) || '' // locality tmp = issuer.match(/\sL\s=\s([^\r\n].*?)[\r\n]/) certValues.issuer.locality = (tmp && tmp[1]) || '' // organization tmp = matchAll(issuer, /\sO\s=\s([^\r\n].*)/g) certValues.issuer.organization = tmp ? (tmp.length > 1 ? tmp.sort(function (t, n) { var e = t[1].toUpperCase() var r = n[1].toUpperCase() return r > e ? -1 : e > r ? 1 : 0 }).sort(function (t, n) { return t[1].length - n[1].length }).map(function (t) { return t[1] }) : tmp[0][1]) : '' // unit tmp = matchAll(issuer, /\sOU\s=\s([^\r\n].*)/g) certValues.issuer.organizationUnit = tmp ? (tmp.length > 1 ? tmp.sort(function (t, n) { var e = t[1].toUpperCase() var r = n[1].toUpperCase() return r > e ? -1 : e > r ? 1 : 0 }).sort(function (t, n) { return t[1].length - n[1].length }).map(function (t) { return t[1] }) : tmp[0][1]) : '' // common name tmp = matchAll(issuer, /\sCN\s=\s([^\r\n].*)/g) certValues.issuer.commonName = tmp ? (tmp.length > 1 ? tmp.sort(function (t, n) { var e = t[1].toUpperCase() var r = n[1].toUpperCase() return r > e ? -1 : e > r ? 1 : 0 }).sort(function (t, n) { return t[1].length - n[1].length }).map(function (t) { return t[1] }) : tmp[0][1]) : '' // DC name tmp = matchAll(issuer, /\sDC\s=\s([^\r\n].*)/g) certValues.issuer.dc = tmp ? (tmp.length > 1 ? tmp.sort(function (t, n) { var e = t[1].toUpperCase() var r = n[1].toUpperCase() return r > e ? -1 : e > r ? 1 : 0 }).sort(function (t, n) { return t[1].length - n[1].length }).map(function (t) { return t[1] }) : tmp[0][1]) : '' } // SAN if ((san = certData.match(/X509v3 Subject Alternative Name: \r?\n([^\r\n]*)\r?\n/)) && san.length > 1) { san = san[1].trim() + '\n' certValues.san = {} // hostnames tmp = pregMatchAll('DNS:([^,\\r\\n].*?)[,\\r\\n\\s]', san) certValues.san.dns = tmp || '' // IP-Addresses IPv4 & IPv6 tmp = pregMatchAll('IP Address:([^,\\r\\n].*?)[,\\r\\n\\s]', san) certValues.san.ip = tmp || '' // Email Addresses tmp = pregMatchAll('email:([^,\\r\\n].*?)[,\\r\\n\\s]', san) certValues.san.email = tmp || '' } // Validity if ((tmp = certData.match(/Not Before\s?:\s?([^\r\n]*)\r?\n/)) && tmp.length > 1) { validity.start = Date.parse((tmp && tmp[1]) || '') } if ((tmp = certData.match(/Not After\s?:\s?([^\r\n]*)\r?\n/)) && tmp.length > 1) { validity.end = Date.parse((tmp && tmp[1]) || '') } if (validity.start && validity.end) { certValues.validity = validity } // Validity end // Signature Algorithm if ((tmp = certData.match(/Signature Algorithm: ([^\r\n]*)\r?\n/)) && tmp.length > 1) { certValues.signatureAlgorithm = (tmp && tmp[1]) || '' } // Public Key if ((tmp = certData.match(/Public[ -]Key: ([^\r\n]*)\r?\n/)) && tmp.length > 1) { certValues.publicKeySize = ((tmp && tmp[1]) || '').replace(/[()]/g, '') } // Public Key Algorithm if ((tmp = certData.match(/Public Key Algorithm: ([^\r\n]*)\r?\n/)) && tmp.length > 1) { certValues.publicKeyAlgorithm = (tmp && tmp[1]) || '' } callback(null, certValues) } function matchAll (str, regexp) { var matches = [] str.replace(regexp, function () { var arr = ([]).slice.call(arguments, 0) var extras = arr.splice(-2) arr.index = extras[0] arr.input = extras[1] matches.push(arr) }) return matches.length ? matches : null } function pregMatchAll (regex, haystack) { var globalRegex = new RegExp(regex, 'g') var globalMatch = haystack.match(globalRegex) || [] var matchArray = [] var nonGlobalRegex, nonGlobalMatch for (var i = 0; i < globalMatch.length; i++) { nonGlobalRegex = new RegExp(regex) nonGlobalMatch = globalMatch[i].match(nonGlobalRegex) matchArray.push(nonGlobalMatch[1]) } return matchArray } function generateCSRSubject (options) { options = options || {} var csrData = { C: options.country || options.C, ST: options.state || options.ST, L: options.locality || options.L, O: options.organization || options.O, OU: options.organizationUnit || options.OU, CN: options.commonName || options.CN || 'localhost', DC: options.dc || options.DC || '', emailAddress: options.emailAddress } var csrBuilder = Object.keys(csrData).map(function (key) { if (csrData[key]) { if (typeof csrData[key] === 'object' && csrData[key].length >= 1) { var tmpStr = '' csrData[key].map(function (o) { tmpStr += '/' + key + '=' + o.replace(/[^\w .*\-,@']+/g, ' ').trim() }) return tmpStr } else { return '/' + key + '=' + csrData[key].replace(/[^\w .*\-,@']+/g, ' ').trim() } } }) return csrBuilder.join('') } function readFromString (string, start, end) { if (Buffer.isBuffer(string)) { string = string.toString('utf8') } var output = [] if (!string) { return output } var offset = string.indexOf(start) while (offset !== -1) { string = string.substring(offset) var endOffset = string.indexOf(end) if (endOffset === -1) { break } endOffset += end.length output.push(string.substring(0, endOffset)) offset = string.indexOf(start, endOffset) } return output } // promisify not tested yet /** * Verifies the signing chain of the passed certificate * @namespace * @name promisified * @property {function} createPrivateKey @see createPrivateKey * @property {function} createDhparam - The default number of players. * @property {function} createEcparam - The default level for the party. * @property {function} createCSR - The default treasure. * @property {function} createCertificate - How much gold the party starts with. */ module.exports.promisified = { createPrivateKey: promisify(createPrivateKey), createDhparam: promisify(createDhparam), createEcparam: promisify(createEcparam), createCSR: promisify(createCSR), createCertificate: promisify(createCertificate), readCertificateInfo: promisify(readCertificateInfo), getPublicKey: promisify(getPublicKey), getFingerprint: promisify(getFingerprint), getModulus: promisify(getModulus), getDhparamInfo: promisify(getDhparamInfo), createPkcs12: promisify(createPkcs12), readPkcs12: promisify(readPkcs12), verifySigningChain: promisify(verifySigningChain), checkCertificate: promisify(checkCertificate), checkPkcs12: promisify(checkPkcs12) }