navy
Version:
Quick and powerful development environments using Docker and Docker Compose
295 lines (289 loc) • 9.79 kB
JavaScript
;
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);
}