UNPKG

navy

Version:

Quick and powerful development environments using Docker and Docker Compose

295 lines (289 loc) 9.79 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.createCert = createCert; exports.generateRootCa = generateRootCa; exports.getCertsPath = getCertsPath; exports.hostNameFromIssueUrl = hostNameFromIssueUrl; exports.issueLeafCertForUrl = issueLeafCertForUrl; exports.removeCert = removeCert; var _path = _interopRequireDefault(require("path")); var _crypto = _interopRequireDefault(require("crypto")); var _net = _interopRequireDefault(require("net")); var _url = require("url"); var _config = require("../config"); var _errors = require("../errors"); var _chalk = _interopRequireDefault(require("chalk")); var _fs = _interopRequireDefault(require("fs")); var _nodeForge = require("node-forge"); var _navy = require("../navy"); const debug = require('debug')('navy:https'); /** Random positive DER INTEGER serial (hex), for unique X.509 serialNumber fields. */ function randomSerialHex(numBytes = 16) { const buf = _crypto.default.randomBytes(numBytes); buf[0] &= 0x7f; return buf.toString('hex'); } /** Hostname (or IP string) from a user-supplied URL for TLS CN/SAN. */ function hostNameFromIssueUrl(urlString) { const trimmed = (urlString || '').trim(); if (!trimmed) { throw new _errors.NavyError('URL for --issue must not be empty.'); } const withScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed) ? trimmed : `https://${trimmed}`; let parsed; try { parsed = new _url.URL(withScheme); } catch (e) { throw new _errors.NavyError(`Invalid URL for --issue: ${urlString}`); } if (!parsed.hostname) { throw new _errors.NavyError(`Could not determine a hostname from URL: ${urlString}`); } return parsed.hostname; } function safeCertFileBase(host) { return host.replace(/[^a-zA-Z0-9._-]/g, '_'); } function subjectAltNamesForHost(host) { if (_net.default.isIP(host)) { return [{ type: 7, ip: host }]; } return [{ type: 2, value: host }]; } function buildSignedLeafPems(certName, tlsRootCaDir, sanAltNames) { const caCertString = _fs.default.readFileSync(`${tlsRootCaDir}/ca.crt`, 'utf8'); const caKeyString = _fs.default.readFileSync(`${tlsRootCaDir}/ca.key`, 'utf8'); const privateCAKey = _nodeForge.pki.privateKeyFromPem(caKeyString); const keys = _nodeForge.pki.rsa.generateKeyPair(2048); const cert = _nodeForge.pki.createCertificate(); const caCert = _nodeForge.pki.certificateFromPem(caCertString); cert.publicKey = keys.publicKey; cert.serialNumber = randomSerialHex(16); cert.validity.notBefore = new Date(); cert.validity.notBefore.setDate(cert.validity.notBefore.getDate() - 1); cert.validity.notAfter = new Date(); cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 2); const attrs = [{ name: 'commonName', value: certName }, { name: 'organizationName', value: 'navy-dev' }]; cert.setSubject(attrs); cert.setIssuer(caCert.subject.attributes); const caSubjectKeyId = caCert.generateSubjectKeyIdentifier().getBytes(); cert.setExtensions([{ name: 'basicConstraints', cA: false, critical: true }, { name: 'keyUsage', digitalSignature: true, keyEncipherment: true, critical: true }, { name: 'extKeyUsage', serverAuth: true }, { name: 'subjectAltName', altNames: sanAltNames }, { name: 'subjectKeyIdentifier' }, { name: 'authorityKeyIdentifier', keyIdentifier: caSubjectKeyId }]); cert.sign(privateCAKey, _nodeForge.md.sha256.create()); return { privateKey: _nodeForge.pki.privateKeyToPem(keys.privateKey), certificate: _nodeForge.pki.certificateToPem(cert) }; } /** * Issues a leaf TLS certificate and key for an arbitrary host, signed with the Navy root CA, * plus a copy of the root CA PEM for configuring trust / full chain on a non-Navy service. */ async function issueLeafCertForUrl(urlString, outputDir) { const certName = hostNameFromIssueUrl(urlString); const tlsRootCaDir = (0, _config.getConfig)().tlsRootCaDir || _config.DEFAULT_TLS_ROOT_CA_DIR; await generateRootCa(); const out = _path.default.resolve(outputDir); // $FlowIgnore _fs.default.mkdirSync(out, { recursive: true }); const base = safeCertFileBase(certName); const san = subjectAltNamesForHost(certName); let pem; try { pem = buildSignedLeafPems(certName, tlsRootCaDir, san); } catch (e) { throw new _errors.NavyError(e instanceof Error ? e.message : String(e)); } const certPath = _path.default.join(out, `${base}.crt`); const keyPath = _path.default.join(out, `${base}.key`); const caCertSrc = _path.default.join(tlsRootCaDir, 'ca.crt'); const caCopyPath = _path.default.join(out, 'navy-root-ca.crt'); _fs.default.writeFileSync(keyPath, pem.privateKey, { mode: 0o600 }); _fs.default.writeFileSync(certPath, pem.certificate, { mode: 0o644 }); _fs.default.copyFileSync(caCertSrc, caCopyPath); return { certPath, keyPath, caCopyPath }; } function getCertsPath(create = false) { const certsPath = _path.default.join((0, _config.getConfigDir)(), 'tls-certs'); if (!_fs.default.existsSync(certsPath)) { if (create) { debug(`Create ${certsPath} dir`); _fs.default.mkdirSync(certsPath, { recursive: true }); } else { return ''; } } return certsPath; } async function removeCert(opts) { const certsPath = getCertsPath(); const navy = await (0, _navy.getNavy)(opts.navy); const serviceUrl = await navy.url(opts.disable); const baseName = serviceUrl.split('//')[1]; const extensions = ['crt', 'key']; for (const ext of extensions) { const file = `${certsPath}/${baseName}.${ext}`; if (_fs.default.existsSync(file)) { try { await _fs.default.unlinkSync(file); debug(`File ${file} removed.`); } catch (err) { throw new _errors.NavyError(err); } } } } async function generateRootCa() { const tlsRootCaDir = (0, _config.getConfig)().tlsRootCaDir || _config.DEFAULT_TLS_ROOT_CA_DIR; if (!_fs.default.existsSync(tlsRootCaDir)) { debug(`Creating ${tlsRootCaDir} Root CA dir`); try { _fs.default.mkdirSync(tlsRootCaDir, { recursive: true }); } catch (err) { throw new _errors.NavyError(err); } } if (_fs.default.existsSync(`${tlsRootCaDir}/ca.crt`) && _fs.default.existsSync(`${tlsRootCaDir}/ca.key`)) { debug('Root CA already exists, skipping generation'); return; } debug('Generating Root CA'); debug('Generating 2048-bit key-pair...'); const keys = _nodeForge.pki.rsa.generateKeyPair(2048); debug('Creating self-signed certificate...'); const cert = _nodeForge.pki.createCertificate(); cert.publicKey = keys.publicKey; cert.serialNumber = randomSerialHex(8); cert.validity.notBefore = new Date(); cert.validity.notAfter = new Date(); cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 5); const attrs = [{ name: 'commonName', value: 'navy-dev-ca.local' }, { name: 'organizationName', value: 'navy-dev' }]; cert.setSubject(attrs); cert.setIssuer(attrs); // Trust stores (macOS keychain, NSS, Windows) expect a CA to assert keyCertSign (and // usually cRLSign) on the issuer; leaf certs need SAN + keyUsage for modern TLS clients. cert.setExtensions([{ name: 'basicConstraints', cA: true, critical: true, pathLenConstraint: 0 }, { name: 'keyUsage', keyCertSign: true, cRLSign: true, critical: true }, { name: 'subjectKeyIdentifier' }, { name: 'authorityKeyIdentifier', keyIdentifier: true }]); try { // self-sign certificate cert.sign(keys.privateKey, _nodeForge.md.sha256.create()); // PEM-format keys and cert const pem = { privateKey: _nodeForge.pki.privateKeyToPem(keys.privateKey), publicKey: _nodeForge.pki.publicKeyToPem(keys.publicKey), certificate: _nodeForge.pki.certificateToPem(cert) }; _fs.default.writeFileSync(tlsRootCaDir + '/ca.key', pem.privateKey, { mode: 0o400 }); _fs.default.writeFileSync(tlsRootCaDir + '/ca.pub.key', pem.publicKey, { mode: 0o640 }); _fs.default.writeFileSync(tlsRootCaDir + '/ca.crt', pem.certificate, { mode: 0o640 }); console.log(_chalk.default.green(`✅ CA Certificate created at ${tlsRootCaDir}/ca.crt`)); console.log(_chalk.default.yellow('⚠️ Importing a self-signed CA into a browser/truststore/keychain is not advisable ⚠️')); } catch (e) { throw new _errors.NavyError(e); } } async function createCert(opts) { const tlsRootCaDir = (0, _config.getConfig)().tlsRootCaDir || _config.DEFAULT_TLS_ROOT_CA_DIR; const certName = opts.hostName || opts.serviceUrl.split('//')[1]; const certsPath = getCertsPath(true); if (_fs.default.existsSync(`${certsPath}/${certName}.crt`)) { debug(`Certificate for ${certName} already exists, skipping generation`); return; } await generateRootCa(); debug(`Generating cert for ${certName} in ${certsPath}`); let commonName = certName; if (opts.serviceUrl && !opts.hostName) { try { const hostname = new _url.URL(opts.serviceUrl).hostname; if (hostname) { commonName = hostname; } } catch (e) { // keep commonName as certName } } const san = subjectAltNamesForHost(commonName); let pem; try { pem = buildSignedLeafPems(commonName, tlsRootCaDir, san); } catch (e) { throw new _errors.NavyError(e instanceof Error ? e.message : String(e)); } _fs.default.writeFileSync(`${certsPath}/${certName}.key`, pem.privateKey); _fs.default.writeFileSync(`${certsPath}/${certName}.crt`, pem.certificate); }