UNPKG

@getflywheel/localcert

Version:

Generate and trust SSL certificates locally.

281 lines (232 loc) 7.47 kB
'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;