cert-store
Version:
🔐 Install, check and delete trusted root certificates.
279 lines (231 loc) • 8.35 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('node-forge'), require('path'), require('child_process'), require('os'), require('util'), require('fs')) :
typeof define === 'function' && define.amd ? define(['node-forge', 'path', 'child_process', 'os', 'util', 'fs'], factory) :
(global['cert-store'] = factory(global['node-forge'],global.path,global.child_process,global.os,global.util,global.fs));
}(this, (function (forge,path,cp,os,util,_fs) { 'use strict';
forge = forge && forge.hasOwnProperty('default') ? forge['default'] : forge;
path = path && path.hasOwnProperty('default') ? path['default'] : path;
cp = cp && cp.hasOwnProperty('default') ? cp['default'] : cp;
os = os && os.hasOwnProperty('default') ? os['default'] : os;
util = util && util.hasOwnProperty('default') ? util['default'] : util;
_fs = _fs && _fs.hasOwnProperty('default') ? _fs['default'] : _fs;
var exec = util.promisify(cp.exec);
// not using fs.promise because we're supporting Node 8.
var fs = {};
for (let [name, method] of Object.entries(_fs)) {
if (typeof method === 'function')
fs[name] = util.promisify(method);
}
async function ensureDirectory(directory) {
try {
await fs.stat(directory);
} catch(err) {
await fs.mkdir(directory);
}
}
// accepts PEM string, removes the --- header/footer, and calculates sha1 hash/thumbprint/fingerprint
function pemToHash(pem) {
return pem.toString()
.replace('-----BEGIN CERTIFICATE-----', '')
.replace('-----END CERTIFICATE-----', '')
.replace(/\r+/g, '')
.replace(/\n+/g, '')
.trim()
}
function isNodeForgeCert(arg) {
if (typeof arg !== 'object') return false
return arg.version !== undefined
&& arg.serialNumber !== undefined
&& arg.signature !== undefined
&& arg.publicKey !== undefined
}
const LINUX_CERT_DIR = '/usr/share/ca-certificates/extra/';
// Not tested. I don't have a mac. help needed.
var MAC_DIR;
if (os.platform() === 'darwin') {
let [major, minor] = os.release().split('.').map(Number);
if (major >= 10 && minor >= 14)
MAC_DIR = '/Library/Keychains/System.keychain';
else
MAC_DIR = '/System/Library/Keychains/SystemRootCertificates.keychain';
}
class CertStruct {
constructor(arg) {
if (Buffer.isBuffer(arg))
arg = arg.toString();
if (typeof arg === 'string') {
if (arg.includes('-----BEGIN CERTIFICATE-----'))
this.pem = arg;
else
this.path = arg;
} else if (typeof arg === 'object') {
this.path = arg.path || arg.path;
this.serialNumber = arg.serialNumber;
if (isNodeForgeCert(arg))
this.pem = forge.pki.certificateToPem(arg);
else
this.pem = arg.pem || arg.cert || arg.data;
}
}
async ensureCertReadFromFs() {
if (this.pem) return
if (!this.path) return
this.pem = (await fs.readFile(this.path)).toString();
}
get name() {
if (this._name) return this._name
if (this.path) {
this._name = path.basename(this.path);
if (this._name.endsWith('.crt') && this._name.endsWith('.cer'))
this._name = this._name.slice(0, -4);
else if (this._name.endsWith('.pem'))
this._name = this._name.slice(0, -5);
} else if (this.certificate) {
let attributes = certificate.subject.attributes;
if (attributes) {
var obj = attributes.find(obj => obj.shortName === 'CN')
|| attributes.find(obj => obj.shortName === 'O');
if (obj)
this._name = obj.value.toLowerCase().replace(/\W+/g, '-');
}
}
return this._name
}
get certificate() {
if (this._certificate) return this._certificate
if (this.pem) return this._certificate = forge.pki.certificateFromPem(this.pem)
}
set certificate(object) {
this._certificate = object;
}
get serialNumber() {
if (this._serialNumber) return this._serialNumber
if (this.certificate) return this._serialNumber = this.certificate.serialNumber
}
set serialNumber(string) {
this._serialNumber = string;
}
}
// https://manuals.gfi.com/en/kerio/connect/content/server-configuration/ssl-certificates/adding-trusted-root-certificates-to-the-server-1605.html
class CertStore {
// Only works on linux (ubuntu, debian).
// Finds certificate in /usr/share/ca-certificates/extra/ by its serial number.
// Returns path to the certificate if found, otherwise undefined.
static async _findLinuxCert(arg) {
await arg.ensureCertReadFromFs();
let filenames = await fs.readdir(LINUX_CERT_DIR);
for (let fileName of filenames) {
let filepath = LINUX_CERT_DIR + fileName;
let pem = await fs.readFile(filepath);
let certificate = forge.pki.certificateFromPem(pem);
if (arg.serialNumber === certificate.serialNumber)
return filepath
}
}
static async createTempFileIfNeeded(arg) {
if (arg.path) return
arg.tempPath = `temp-${Date.now()}-${Math.random()}.crt`;
await fs.writeFile(arg.tempPath, arg.pem);
}
static async deleteTempFileIfNeeded(arg) {
if (!arg.tempPath) return
await fs.unlink(arg.tempPath);
}
// SUGARY METHODS
static async install(arg) {
arg = new CertStruct(arg);
if (!arg.path && !arg.pem)
throw new Error('path to or contents of the certificate has to be defined.')
try {
return await this._install(arg)
} catch(err) {
throw new Error(`Couldn't install certificate.\n${err.stack}`)
} finally {
this.deleteTempFileIfNeeded(arg);
}
}
static async delete(arg) {
arg = new CertStruct(arg);
try {
return await this._delete(arg)
} catch(err) {
throw new Error(`Couldn't delete certificate.\n${err.stack}`)
}
}
static async isInstalled(arg) {
arg = new CertStruct(arg);
try {
return await this._isInstalled(arg)
} catch(err) {
throw new Error(`Couldn't find if certificate is installed.\n${err.stack}`)
}
}
}
class WindowsCertStore extends CertStore {
static async _install(arg) {
await this.createTempFileIfNeeded(arg);
await exec(`certutil -addstore -user -f root "${arg.path || arg.tempPath}"`);
}
static async _delete(arg) {
await arg.ensureCertReadFromFs();
await exec(`certutil -delstore -user root ${arg.serialNumber}`);
}
static async _isInstalled(arg) {
await arg.ensureCertReadFromFs();
try {
await exec(`certutil -verifystore -user root ${arg.serialNumber}`);
return true
} catch(err) {
// certutil always fails if the serial number is not found.
return false
}
}
}
class LinuxCertStore extends CertStore {
static async _install(arg) {
await ensureDirectory(LINUX_CERT_DIR);
var targetPath = LINUX_CERT_DIR + arg.name + '.crt';
if (!arg.pem && arg.path)
arg.pem = await fs.readFile(arg.path);
await fs.writeFile(targetPath, arg.pem);
await exec('update-ca-certificates');
}
static async _delete(arg) {
var targetPath = await this._findLinuxCert(arg);
if (targetPath) await fs.unlink(targetPath);
}
static async _isInstalled(arg) {
return !!(await this._findLinuxCert(arg))
}
}
// Not tested. I don't have a mac. help needed.
class MacCertStore extends CertStore {
static async _install(arg) {
await this.createTempFileIfNeeded(arg);
await exec(`security add-trusted-cert -d -r trustRoot -k "${MAC_DIR}" "${arg.path}"`);
}
static async _delete(arg) {
await arg.ensureCertReadFromFs();
var fingerPrint = pemToHash(arg.pem);
await exec(`security delete-certificate -Z ${fingerPrint} "${MAC_DIR}"`);
}
static async _isInstalled(arg) {
// TODO
throw new Error('isInstalled() not yet implemented on this platform')
}
}
var PlatformSpecificCertStore;
switch (process.platform) {
case 'win32':
PlatformSpecificCertStore = WindowsCertStore;
break
case 'darwin':
PlatformSpecificCertStore = MacCertStore;
break
default:
PlatformSpecificCertStore = LinuxCertStore;
break
}
var PlatformSpecificCertStore$1 = PlatformSpecificCertStore;
return PlatformSpecificCertStore$1;
})));