pem.js

'use strict'

/**
 * pem module
 *
 * @module pem
 */
const {debug} = require('./debug.js')
const {promisify} = require('es6-promisify')
var net = require('net')
var helper = require('./helper.js')
var openssl = require('./openssl.js')
const hash_md5 = require("md5")

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']

  if (openssl.get('Vendor') === 'OPENSSL' && openssl.get('VendorVersionMajor') >= 3) {
    params.push('-traditional')
  }

  var delTempPWFiles = []

  if (options && options.cipher && (Number(helper.ciphers.indexOf(options.cipher)) !== -1) && options.password) {
    debug('helper.createPasswordFile', {
      cipher: options.cipher,
      password: options.password,
      passType: 'out'
    })
    helper.createPasswordFile({
      cipher: options.cipher,
      password: options.password,
      passType: 'out'
    }, params, delTempPWFiles)
  }

  params.push(keyBitsize)

  debug('version', openssl.get('openSslVersion'))

  openssl.exec(params, '(RSA |ENCRYPTED |)PRIVATE KEY', function (sslErr, key) {
    function done(err) {
      if (err) {
        return callback(err)
      }
      return callback(null, {
        key: key
      })
    }

    helper.deleteTempFiles(delTempPWFiles, function (fsErr) {
      debug('createPrivateKey', {
        sslErr: sslErr,
        fsErr: fsErr,
        key: key,
        keyLength: key && key.length
      })
      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
  }

  let delTempPWFiles = []

  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) {
    if (options && (options.password || options.clientKeyPassword)) {
      options.password = options.password || options.clientKeyPassword || ''
    }
    createPrivateKey(options.keyBitsize || 2048, options, 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 && (options.password || options.clientKeyPassword)) {
    helper.createPasswordFile({
      cipher: '',
      password: options.password || options.clientKeyPassword,
      passType: 'in'
    }, params, delTempPWFiles)
  }

  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
  }


  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, {
        cipher: options.cipher,
        password: options.clientKeyPassword || ''
      }, 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

  let 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'
  }
  let params = [
    type,
    '-noout',
    '-modulus',
    '-in',
    '--TMPFILE--'
  ]
  let 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) {
        if (hash === 'md5') {
          return callback(null, {
            modulus: hash_md5(match[1])
          })
        }

        return callback(null, {
          modulus: 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} certificate 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} key PEM encoded private key
 * @param {String} certificate PEM encoded certificate
 * @param {String} password 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 (openssl.get('Vendor') === "OPENSSL" && openssl.get('VendorVersionMajor') >= 3) {
    args.push('-legacy')
    args.push('-traditional')
  }

  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()

        debug("readPkcs12.execBinary - PRIVATE KEY - ?: ", keybundle.key)
        if (keybundle.key) {
          var args = ['rsa'];
          if (openssl.get('Vendor') === "OPENSSL" && openssl.get('VendorVersionMajor') >= 3) {
            args.push('-traditional')
          }
          args.push('-in');
          args.push('--TMPFILE--');

          // convert to RSA key
          return openssl.exec(args, '(RSA |)PRIVATE KEY', [keybundle.key], function (err, key) {
            if (err) {
              debug("readPkcs12.execBinary - PRIVATE KEY convert - error: ", err)
            }
            //debug("readPkcs12.execBinary - PRIVATE KEY", key)
            keybundle.key = key

            return callback(err, keybundle)
          })
        }

        if (options.clientKeyPassword) {
          keybundle.key = readFromString(stdout, ENCRYPTED_KEY_START, ENCRYPTED_KEY_END).pop()
          debug("readPkcs12.execBinary - ENCRYPTED PRIVATE KEY - ?: ", keybundle.key)
          /*return openssl.exec(['rsa', '-in', '--TMPFILE--'], 'RSA PRIVATE KEY', [keybundle.key], function (err, key) {
            if (err) {
              debug("readPkcs12.execBinary - ENCRYPTED PRIVATE KEY - error: ", err)
            }
            debug("readPkcs12.execBinary - ENCRYPTED PRIVATE KEY", key)
            keybundle.key = key

            return callback(err, keybundle)
          })*/
        } else {
          keybundle.key = readFromString(stdout, RSA_KEY_START, RSA_KEY_END).pop()
          debug("readPkcs12.execBinary - RSA PRIVATE KEY - ?: ", keybundle.key)
          /*return openssl.exec(['rsa', '-in', '--TMPFILE--'], 'RSA PRIVATE KEY', [keybundle.key], function (err, key) {
            if (err) {
              debug("readPkcs12.execBinary - RSA PRIVATE KEY - error: ", err)
            }
            debug("readPkcs12.execBinary - RSA PRIVATE KEY", key)
            keybundle.key = key

            return callback(err, keybundle)
          })*/
        }
      }

      return callback(err, keybundle)
    }

    helper.deleteTempFiles(delTempPWFiles, function (fsErr) {
      done(sslErr || fsErr)
    })
  })
}

/**
 * Check a certificate
 * @static
 * @param {String} certificate 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) {

      stdout = stdout && stdout.trim()
      var result
      switch (params[0]) {
        case 'rsa':
          result = /^Rsa key ok$/i.test(stdout)
          break
        default:
          result = /Signature Algorithm/im.test(stdout)
          break
      }
      if (!result) {
        if (openssl.get('Vendor') === "OPENSSL" && openssl.get('VendorVersionMajor') >= 3) {
          if (!(stderr && stderr.toString().trim().endsWith('verify OK'))) {
            return callback(new Error(stderr.toString()))
          }
        }
        if (err && err.toString().trim() !== 'verify OK') {
          return callback(err)
        }
      }
      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--'
  }

  if (openssl.get('Vendor') === "OPENSSL" && openssl.get('VendorVersionMajor') >= 3) {
    args.splice(2, 0, '-legacy');
  }

  openssl.spawnWrapper(args, tmpfiles, function (sslErr, code, stdout, stderr) {
    debug('checkPkcs12 error', {
      err: sslErr,
      code: code,
      stdout: stdout,
      stdoutResult: (/MAC verified OK/im.test(stderr) || (!(/MAC verified OK/im.test(stderr)) && !(/Mac verify error/im.test(stderr)))),
      stderr: 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) {
      debug('checkPkcs12 clean-up error', {
        sslErr: sslErr,
        fsErr: fsErr,
        code: code,
        stdout: stdout,
        stdoutResult: (/MAC verified OK/im.test(stderr) || (!(/MAC verified OK/im.test(stderr)) && !(/Mac verify error/im.test(stderr)))),
        stderr: stderr
      })
      done(sslErr || fsErr)
    })
  })
}

/**
 * Verifies the signing chain of the passed certificate
 * @static
 * @param {String|Array} certificate PEM encoded certificate include intermediate certificates
 * The correct order of trust chain must be preserved and should start with Leaf
 * certificate. Example array: [Leaf, Int CA 1, ... , Int CA N, Root CA].
 * @param {String|Array} ca [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 = readFromString(certificate, CERT_START, CERT_END)
  }
  if (!Array.isArray(ca) && ca !== undefined) {
    if (ca !== '') {
      ca = [ca]
    }
  }

  var params = ['verify']
  var files = []

  if (ca !== undefined) {
    // ca certificates
    params.push('-CAfile')
    params.push('--TMPFILE--')
    files.push(ca.join('\n'))
  }
  // extracting the very first - leaf - cert in chain
  var leaf = certificate.shift()

  if (certificate.length > 0) {
    params.push('-untrusted')
    params.push('--TMPFILE--')
    files.push(certificate.join('\n'))
  }

  params.push('--TMPFILE--')
  files.push(leaf)

  openssl.spawnWrapper(params, files, function (err, code, stdout, stderr) {
    // OPENSSL 3.x don't use stdout to print the error
    debug('Vendor', openssl.get('Vendor'))
    debug('VendorVersionMajor', openssl.get('VendorVersionMajor'))
    debug('openssl.get(\'VendorVersionMajor\') >= 3', openssl.get('VendorVersionMajor') >= 3)

    if (openssl.get('Vendor') === "OPENSSL" && openssl.get('VendorVersionMajor') >= 3) {
      let openssl30Check = !!(stdout && stdout.trim().includes(": OK"));

      if (err) {
        debug('verifySigningChain error', {
          err: err,
          code: code,
          stdout: stdout,
          stdoutResult: openssl30Check,
          stderr: stderr
        })
        return callback(err)
      }

      debug('verifySigningChain error - use stderr', {
        err: err,
        code: code,
        stdout: stdout.trim(),
        stdoutResult: openssl30Check,
        stderr: stderr.trim()
      })
      return callback(null, openssl30Check)
    }
    // END: OPENSSL 3.x don't use stdout to print the error
    if (err) {
      debug('verifySigningChain error', {
        err: err,
        code: code,
        stdout: stdout,
        stdoutResult: stdout && stdout.trim().slice(-4) === ': OK',
        stderr: stderr
      })
      return callback(err)
    }
    debug('verifySigningChain', {
      err: err,
      code: code,
      stdout: stdout,
      stdoutResult: stdout && stdout.trim().slice(-4) === ': OK',
      stderr: stderr
    })
    callback(null, stdout && stdout.trim().slice(-4) === ': OK')
  })
}

// HELPER FUNCTIONS
function fetchCertificateData(certData, callback) {
  // try catch : if something will fail in parsing it won't crash the calling code
  try {
    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)
      if (tmp) {
        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)
  } catch (err) {
    callback(err)
  }
}

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\s-!$%^&*()_+|~=`{}[\]:/;<>?,.@#]+/g, ' ').replace('/', '\\/').replace('+', '\\+').trim()
        })
        return tmpStr
      } else {
        return '/' + key + '=' + csrData[key].replace(/[^\w\s-!$%^&*()_+|~=`{}[\]:/;<>?,.@#]+/g, ' ').replace('/', '\\/').replace('+', '\\+').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)
}