@getflywheel/localcert
Version:
Generate and trust SSL certificates locally.
281 lines (232 loc) • 7.47 kB
JavaScript
'use strict';
const forge = require('node-forge');
const path = require('path');
const fs = require('fs-extra');
class X509Certificate {
/**
* Certificate getter
*
* @since 1.0.0
* @return {object} X509Certificate
*/
get certificate () {
return this._certificate;
}
/**
* Certificate setter
*
* @since 1.0.0
* @param {object} X509Certificate The certificate to set.
*/
set certificate (X509Certificate) {
this._certificate = X509Certificate;
}
/**
* Loads an existing certificate to trust.
*
* @since 1.0.1
* @param {string} certPath The path to the certificate.
* @param {string} privateKeyPath The path to the private key.
* @return {object} The paths to the certificate files.
*/
async setCertificate (certPath, privateKeyPath) {
this.setCommonName(path.basename(certPath).split('.').slice(0, -1).join('.'));
const pemCertRaw = await fs.readFile(certPath);
const pemCert = pemCertRaw.toString();
const pemPrivateKeyRaw = await fs.readFile(privateKeyPath);
const pemPrivateKey = pemPrivateKeyRaw.toString();
const cert = forge.pki.certificateFromPem(pemCert);
const der = forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes();
const m = forge.md.sha256.create();
m.update(der);
this.certificate = {
privateKey: pemPrivateKey,
certificate: pemCert,
fingerprint: m.digest().toHex().toUpperCase(),
serialNumber: cert.serialNumber,
};
this.certPath = certPath;
this.keyPAth = privateKeyPath;
return {
certPath: certPath,
keyPath: privateKeyPath,
};
}
/**
* Builds the attributes for setting up the certificate.
*
* @since 1.0.0
* @param {string|Array} domains Domain or domains to generate a certificate for.
* @param {string} countryName Two-letter country name
* @param {string} stateOrProvinceName Two-letter state
* @param {string} localityName Locality name
* @param {string} organizationName Organization name
* @param {string} organizationalUnitName Unit name
* @return {object} The various attrributes to save with the certificate
*/
getAttrs (domains = '', countryName = 'XX', stateOrProvinceName = 'XX', localityName = 'Fake Locality', organizationName = 'Super Fake Company, Fake.', organizationalUnitName = 'Fake Organizational Unit') {
return [{
name: 'commonName',
value: this.commonName,
}, {
name: 'countryName',
value: countryName,
}, {
name: 'stateOrProvinceName',
value: stateOrProvinceName,
}, {
name: 'localityName',
value: localityName,
}, {
name: 'organizationName',
value: organizationName,
}, {
name: 'organizationalUnitName',
value: organizationalUnitName,
}];
}
setCommonName (domains) {
let commonName = domains;
if (Array.isArray(domains)) {
commonName = domains[0];
}
this.commonName = commonName;
}
/**
* Builds the domain name structure for creating the certificate
*
* @since 1.0.0
* @param {string|Array} domains Domain or domains to generate a certificate for.
* @return {void}
*/
getAltNames (domains) {
if (!Array.isArray(domains)) {
domains = [domains];
}
const altNameWrapper = [{
name: 'subjectAltName',
altNames: [],
}];
domains.forEach((domain) => {
altNameWrapper[0].altNames.push({
type: 2, // DNSName
value: domain,
});
});
return altNameWrapper;
}
/**
* Ensures a positive hex string.
*
* @credit https://github.com/jfromaniello/selfsigned/blob/master/index.js#L7
* @since 1.0.0
* @param {string} hexString The hex string to convert
*
* @return {string} The positive hex string.
*/
toPositiveHex (hexString) {
let mostSiginficativeHexAsInt = parseInt(hexString[0], 16);
if (mostSiginficativeHexAsInt < 8) {
return hexString;
}
mostSiginficativeHexAsInt -= 8;
return mostSiginficativeHexAsInt.toString() + hexString.substring(1);
}
/**
* Generates the certificate.
*
* Generates the text of a certificate.
*
* @since 1.0.0
* @param {string|Array} domains Domain or domains to generate a certificate for.
* @param {string} countryName Two-letter country name
* @param {string} stateOrProvinceName Two-letter state
* @param {string} localityName Locality name
* @param {string} organizationName Organization name
* @param {string} organizationalUnitName Unit name
* @return {object} Certificate contents.
*/
async generateCertificate (domains = '', countryName = 'XX', stateOrProvinceName = 'XX', localityName = 'Fake Locality', organizationName = 'Super Fake Company, Fake.', organizationalUnitName = 'Fake Organizational Unit') {
this.setCommonName(domains);
const pki = forge.pki;
const keys = pki.rsa.generateKeyPair(2048);
const cert = pki.createCertificate();
cert.publicKey = keys.publicKey;
// NOTE: serialNumber is the hex encoded value of an ASN.1 INTEGER.
// Conforming CAs should ensure serialNumber is:
// - no more than 20 octets
// - non-negative (prefix a '00' if your value starts with a '1' bit)
cert.serialNumber = this.toPositiveHex(forge.util.bytesToHex(forge.random.getBytesSync(9))); // the serial number can be decimal or hex (if preceded by 0x);
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10);
cert.setSubject(this.getAttrs(domains, countryName, stateOrProvinceName, localityName, organizationName, organizationalUnitName));
cert.setIssuer(this.getAttrs(domains, countryName, stateOrProvinceName, localityName, organizationName, organizationalUnitName));
cert.setExtensions([{
name: 'basicConstraints',
cA: true,
}, {
name: 'keyUsage',
keyCertSign: true,
digitalSignature: true,
nonRepudiation: true,
keyEncipherment: true,
dataEncipherment: true,
}, {
name: 'extKeyUsage',
serverAuth: true,
clientAuth: true,
codeSigning: true,
emailProtection: true,
timeStamping: true,
}, {
name: 'nsCertType',
client: true,
server: true,
email: true,
objsign: true,
sslCA: true,
emailCA: true,
objCA: true,
}, {
name: 'subjectKeyIdentifier',
}].concat(this.getAltNames(domains)));
// self-sign certificate
cert.sign(keys.privateKey, forge.md.sha256.create());
const certificate = {
privateKey: pki.privateKeyToPem(keys.privateKey),
publicKey: pki.publicKeyToPem(keys.publicKey),
certificate: pki.certificateToPem(cert),
fingerprint: forge.md.sha256.create().update(forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes()).digest().toHex(),
};
this.certificate = certificate;
return certificate;
}
/**
* Save the certificate to disk.
*
* @since 1.0.0
* @param {string} certsPath The path to which to save the certificate.
* @return {object} JavaScript object containing the certificate paths.
*/
async saveCertificate (certsPath) {
fs.stat(certsPath, (err, stats) => {
if (err || !stats.isDirectory()) {
throw new Error('Invalid cert path specified');
}
});
const certPath = path.join(certsPath, `${this.commonName}.crt`);
const keyPath = path.join(certsPath, `${this.commonName}.key`);
await Promise.all([
fs.writeFile(certPath, this.certificate.certificate),
fs.writeFile(keyPath, this.certificate.privateKey),
]);
this.certPath = certPath;
this.keyPAth = keyPath;
return {
certPath: certPath,
keyPath: keyPath,
};
}
}
module.exports = X509Certificate;