CI / Build & Test (push) Successful in 3m37s
- Replace sudo with pkexec so PolicyKit shows a graphical auth dialog when the native host is launched by the browser (no terminal). - Pass DISPLAY and XAUTHORITY into the pkexec env so the polkit agent can display the dialog in the current session. - Keep copy + update-ca-certificates in a single pkexec sh -c for one password prompt. - Update SECURITY.md and JSDoc to describe pkexec / PolicyKit.
498 lines
20 KiB
JavaScript
498 lines
20 KiB
JavaScript
/**
|
|
* Certificate authority for Holesail Browser native host.
|
|
* Generates a root CA and per-domain certs for virtual hosts (e.g. *.hs).
|
|
* Modeled on P2NS certificate_authority.js; uses node-forge for key/cert generation.
|
|
*/
|
|
|
|
// node-forge expects Node (crypto) or a browser (window/self). Bare has neither.
|
|
// Provide window so util.globalScope works; do NOT set process.versions.node so forge
|
|
// stays in "browser" mode and uses its pure JS crypto instead of require('crypto').
|
|
if (typeof global !== 'undefined' && typeof global.window === 'undefined') {
|
|
global.window = global;
|
|
}
|
|
|
|
const path = require('bare-path');
|
|
const fs = require('bare-fs');
|
|
const forge = require('node-forge');
|
|
const platform = typeof process !== 'undefined' && process.platform ? process.platform : '';
|
|
|
|
let spawn = null;
|
|
try {
|
|
const cp = require('child_process');
|
|
if (cp && typeof cp.spawn === 'function') spawn = cp.spawn;
|
|
} catch (_) {}
|
|
|
|
function runCommand(command, callback) {
|
|
if (typeof callback !== 'function') callback = () => {};
|
|
if (!spawn) {
|
|
callback(new Error('child_process.spawn (bare-subprocess) not available'));
|
|
return;
|
|
}
|
|
const proc = spawn(command, [], { shell: true });
|
|
let stdout = '';
|
|
let stderr = '';
|
|
if (proc.stdout) proc.stdout.on('data', (chunk) => { stdout += (chunk && chunk.toString) ? chunk.toString() : String(chunk); });
|
|
if (proc.stderr) proc.stderr.on('data', (chunk) => { stderr += (chunk && chunk.toString) ? chunk.toString() : String(chunk); });
|
|
proc.once('close', (code, signal) => {
|
|
if (code !== 0 && code != null) {
|
|
callback(new Error(stderr || 'Command failed with code ' + code), stdout, stderr);
|
|
} else {
|
|
callback(null, stdout, stderr);
|
|
}
|
|
});
|
|
proc.once('error', (err) => callback(err, '', ''));
|
|
}
|
|
|
|
/** Run a shell command with macOS GUI sudo prompt (osascript "with administrator privileges"). */
|
|
function runWithSudoMacOS(shellCommand, callback) {
|
|
if (typeof callback !== 'function') callback = () => {};
|
|
const escaped = shellCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
const appleScript = 'do shell script "' + escaped + '" with administrator privileges';
|
|
const arg = appleScript.replace(/'/g, "'\"'\"'");
|
|
runCommand("osascript -e '" + arg + "'", callback);
|
|
}
|
|
|
|
// Resolve certs dir relative to the executable when running as a standalone
|
|
// binary (__dirname is bare:/app.bundle/ which is not a real filesystem path).
|
|
function resolveCertsBase() {
|
|
try {
|
|
const os = require('bare-os');
|
|
const execPath = os.execPath();
|
|
if (execPath && !execPath.startsWith('bare:')) {
|
|
return path.dirname(execPath);
|
|
}
|
|
} catch (_) {}
|
|
return path.dirname(__dirname);
|
|
}
|
|
const certsDir = process.env.CERTS_DIR || process.env.HOLESAIL_BROWSER_CERTS || path.join(resolveCertsBase(), 'holesail-browser-certs');
|
|
const caKeyPath = path.join(certsDir, 'ca.key.pem');
|
|
const caCertPath = path.join(certsDir, 'ca.cert.pem');
|
|
|
|
const CA_COMMON_NAME = 'Holesail Browser CA';
|
|
|
|
function logDebug(tag, msg) {
|
|
if (process.stderr) process.stderr.write(`[CA ${tag}] ${msg}\n`);
|
|
}
|
|
function logWarn(tag, msg) {
|
|
if (process.stderr) process.stderr.write(`[CA ${tag}] WARN: ${msg}\n`);
|
|
}
|
|
function logError(tag, msg) {
|
|
if (process.stderr) process.stderr.write(`[CA ${tag}] ERROR: ${msg}\n`);
|
|
}
|
|
function logInfo(tag, msg) {
|
|
if (process.stderr) process.stderr.write(`[CA ${tag}] ${msg}\n`);
|
|
}
|
|
|
|
if (!fs.existsSync(certsDir)) {
|
|
try {
|
|
fs.mkdirSync(certsDir, { recursive: true });
|
|
} catch (e) {
|
|
logError('CA', 'Failed to create certs dir: ' + e.message);
|
|
}
|
|
}
|
|
|
|
// Determine whether CA generation is needed without doing the expensive RSA work yet.
|
|
let _caReadyResolve = null;
|
|
let _caReadyReject = null;
|
|
const caReady = new Promise((resolve, reject) => {
|
|
_caReadyResolve = resolve;
|
|
_caReadyReject = reject;
|
|
});
|
|
|
|
function _generateCA() {
|
|
try {
|
|
logInfo('CA', 'Generating Root CA (2048-bit RSA)...');
|
|
const keys = forge.pki.rsa.generateKeyPair(2048);
|
|
const cert = forge.pki.createCertificate();
|
|
cert.publicKey = keys.publicKey;
|
|
cert.serialNumber = '01';
|
|
cert.validity.notBefore = new Date();
|
|
cert.validity.notAfter = new Date();
|
|
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10);
|
|
const attrs = [
|
|
{ name: 'commonName', value: CA_COMMON_NAME },
|
|
{ name: 'countryName', value: 'US' },
|
|
{ shortName: 'ST', value: 'CA' },
|
|
{ name: 'localityName', value: 'San Francisco' },
|
|
{ name: 'organizationName', value: 'Holesail Browser' },
|
|
{ shortName: 'OU', value: 'Holesail Root CA' }
|
|
];
|
|
cert.setSubject(attrs);
|
|
cert.setIssuer(attrs);
|
|
const skid = forge.pki.getPublicKeyFingerprint(cert.publicKey, { md: forge.md.sha1.create() });
|
|
cert.setExtensions([
|
|
{ name: 'basicConstraints', cA: true, pathLenConstraint: 0 },
|
|
{ name: 'keyUsage', keyCertSign: true, cRLSign: true },
|
|
{ name: 'subjectKeyIdentifier' },
|
|
{ name: 'authorityKeyIdentifier', keyIdentifier: skid.getBytes() }
|
|
]);
|
|
cert.sign(keys.privateKey, forge.md.sha256.create(), { padding: forge.pki.rsa.PKCS1_v1_5 });
|
|
fs.writeFileSync(caKeyPath, forge.pki.privateKeyToPem(keys.privateKey));
|
|
fs.writeFileSync(caCertPath, forge.pki.certificateToPem(cert));
|
|
logInfo('CA', 'Root CA generated.');
|
|
_caReadyResolve();
|
|
} catch (e) {
|
|
logError('CA', 'Failed to generate Root CA: ' + e.message);
|
|
_caReadyReject(e);
|
|
}
|
|
}
|
|
|
|
// Defer CA generation to the next event-loop tick so it does not block
|
|
// the native host startup (RSA key generation can take several seconds).
|
|
let _needsGeneration = false;
|
|
if (fs.existsSync(caKeyPath) && fs.existsSync(caCertPath)) {
|
|
try {
|
|
const caCertPem = fs.readFileSync(caCertPath, 'utf8');
|
|
const cert = forge.pki.certificateFromPem(caCertPem);
|
|
if (cert.validity.notAfter > new Date()) {
|
|
logDebug('CA', 'Existing Root CA is valid.');
|
|
_caReadyResolve();
|
|
} else {
|
|
logWarn('CA', 'Existing Root CA expired, regenerating.');
|
|
_needsGeneration = true;
|
|
}
|
|
} catch (e) {
|
|
logWarn('CA', 'Could not read existing CA: ' + e.message + '; regenerating.');
|
|
_needsGeneration = true;
|
|
}
|
|
} else {
|
|
_needsGeneration = true;
|
|
}
|
|
|
|
if (_needsGeneration) {
|
|
// Use setImmediate so the event loop can process the first native message
|
|
// before the blocking RSA generation starts.
|
|
if (typeof setImmediate === 'function') {
|
|
setImmediate(_generateCA);
|
|
} else {
|
|
setTimeout(_generateCA, 0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install the root CA certificate into the OS trust store.
|
|
* Uses `security add-trusted-cert` on macOS (with GUI sudo prompt),
|
|
* `pkexec` + cp + `update-ca-certificates` on Linux (PolicyKit GUI password prompt),
|
|
* and `certutil` on Windows.
|
|
* @param {Function} callback - Called as `callback(err)` on completion.
|
|
*/
|
|
function installRootCA(callback) {
|
|
if (typeof callback !== 'function') callback = () => {};
|
|
if (!spawn) {
|
|
callback(new Error('child_process.spawn (bare-subprocess) not available'));
|
|
return;
|
|
}
|
|
const caPath = path.resolve(caCertPath);
|
|
|
|
if (platform === 'darwin') {
|
|
const systemKeychain = '/Library/Keychains/System.keychain';
|
|
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
const loginKc = home ? path.join(home, 'Library', 'Keychains', 'login.keychain-db') : '';
|
|
|
|
// Check if the *current* CA cert on disk is trusted for SSL (fingerprint match + policy).
|
|
// Uses verify-cert -p ssl so we catch stale entries with the same CN but a different key.
|
|
function isTrustedCurrent(done) {
|
|
runCommand(`security verify-cert -p ssl -c "${caPath}" 2>/dev/null`, (err) => {
|
|
if (err) { done(false); return; }
|
|
// Also confirm the fingerprint of the installed cert matches the one on disk.
|
|
runCommand(`security find-certificate -a -c "${CA_COMMON_NAME}" -Z "${loginKc}" 2>/dev/null`, (errFind, findOut) => {
|
|
if (errFind || !findOut) { done(false); return; }
|
|
const m = findOut.match(/SHA-1 hash:\s*([0-9A-Fa-f]+)/);
|
|
if (!m) { done(false); return; }
|
|
const installedSha1 = m[1].toUpperCase();
|
|
runCommand(`openssl x509 -in "${caPath}" -noout -fingerprint -sha1 2>/dev/null`, (errFp, fpOut) => {
|
|
if (errFp) { done(false); return; }
|
|
const currentSha1 = (fpOut.split('=')[1] || '').trim().replace(/:/g, '').toUpperCase();
|
|
done(installedSha1 === currentSha1);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function removeOldEntries(done) {
|
|
runCommand(`security delete-certificate -c "${CA_COMMON_NAME}" "${systemKeychain}" 2>/dev/null; true`, () => {
|
|
if (loginKc) {
|
|
runCommand(`security delete-certificate -c "${CA_COMMON_NAME}" "${loginKc}" 2>/dev/null; true`, () => done());
|
|
} else {
|
|
done();
|
|
}
|
|
});
|
|
}
|
|
|
|
function addToLoginKeychain(done) {
|
|
// Install to the user's login keychain with explicit SSL trust policy.
|
|
// This requires no sudo/admin password and Chrome respects it because
|
|
// it evaluates TLS trust using the SSL policy against all user keychains.
|
|
// The -p ssl flag sets the correct policy OID that Chrome checks.
|
|
if (!loginKc) {
|
|
done(new Error('HOME not set; cannot determine login keychain path'));
|
|
return;
|
|
}
|
|
const cmd = `security add-trusted-cert -r trustRoot -p ssl -k "${loginKc}" "${caPath}"`;
|
|
runCommand(cmd, (err, _stdout, stderr) => {
|
|
if (!err) {
|
|
logInfo('CA', 'Root CA installed to login keychain with SSL trust (trusted by Chrome).');
|
|
done(null);
|
|
return;
|
|
}
|
|
const msg = (stderr || err.message || '').trim();
|
|
logError('CA', 'add-trusted-cert failed: ' + msg);
|
|
done(new Error('Could not install CA: ' + msg));
|
|
});
|
|
}
|
|
|
|
isTrustedCurrent((trusted) => {
|
|
if (trusted) {
|
|
logInfo('CA', 'Root CA already trusted on macOS (fingerprint verified).');
|
|
callback(null);
|
|
return;
|
|
}
|
|
// Remove any stale entry (same CN, different key) then install fresh.
|
|
removeOldEntries(() => addToLoginKeychain(callback));
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (platform === 'linux') {
|
|
const caTargetPath = '/usr/local/share/ca-certificates/holesail-browser-ca.crt';
|
|
// Escape caPath for safe use inside a single-quoted sh -c string (single quotes in path become '"'"').
|
|
const escapedPath = caPath.replace(/'/g, "'\"'\"'");
|
|
// Pass DISPLAY/XAUTHORITY so pkexec's polkit auth agent can show a GUI password dialog when
|
|
// the native host is launched by the browser (no terminal).
|
|
const display = (process.env.DISPLAY || '').replace(/'/g, "'\"'\"'");
|
|
const xauth = (process.env.XAUTHORITY || '').replace(/'/g, "'\"'\"'");
|
|
const pkexecCmd =
|
|
`pkexec env DISPLAY='${display}' XAUTHORITY='${xauth}' sh -c 'cp "${escapedPath}" ${caTargetPath} && update-ca-certificates'`;
|
|
runCommand(pkexecCmd, (err) => {
|
|
if (err) {
|
|
logError('CA', 'Error installing CA on Linux (pkexec copy + update-ca-certificates): ' + err.message);
|
|
callback(err);
|
|
return;
|
|
}
|
|
logInfo('CA', 'Root CA installed on Linux.');
|
|
callback(null);
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (platform === 'win32') {
|
|
// regenerated is always false here — the CA on disk is the authoritative version.
|
|
// If the cert was just regenerated the caller should delete the old store entry first.
|
|
const regenerated = false;
|
|
runCommand(`certutil -store ROOT | findstr "${CA_COMMON_NAME}"`, (err, stdout) => {
|
|
const installed = !err && (stdout && stdout.trim() !== '');
|
|
if (installed && !regenerated) {
|
|
logInfo('CA', 'Root CA already installed on Windows.');
|
|
callback(null);
|
|
return;
|
|
}
|
|
function addCA() {
|
|
const command = `certutil -addstore -f "ROOT" "${caPath}"`;
|
|
runCommand(command, (errAdd, _o, stderr) => {
|
|
if (errAdd) {
|
|
logError('CA', 'Error installing root CA on Windows: ' + (stderr || errAdd.message));
|
|
callback(errAdd);
|
|
} else {
|
|
logInfo('CA', 'Root CA installed successfully on Windows.');
|
|
callback(null);
|
|
}
|
|
});
|
|
}
|
|
if (installed) {
|
|
runCommand(`certutil -delstore ROOT "${CA_COMMON_NAME}"`, (errDel) => {
|
|
if (errDel) logError('CA', 'Error deleting old CA on Windows');
|
|
addCA();
|
|
});
|
|
} else {
|
|
addCA();
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
logWarn('CA', 'Unsupported platform for auto-install CA: ' + platform);
|
|
callback(null);
|
|
}
|
|
|
|
/**
|
|
* Get or create a TLS certificate for the given domain, signed by the root CA.
|
|
* Certificates are cached on disk in `<certsDir>/<domain>/`. Returns `{ cert, key }`
|
|
* buffers, or null if the CA is not yet ready.
|
|
* @param {string} domain - Certificate common name / directory key.
|
|
* @param {Array<{type: number, value: string}>} altNames - Subject Alternative Names.
|
|
* @param {boolean} [forceRegenerate=false] - If true, regenerate even if a cached cert exists.
|
|
* @returns {{cert: Buffer, key: Buffer}|null}
|
|
*/
|
|
function getOrCreateDomainCert(domain, altNames, forceRegenerate) {
|
|
if (!domain) return null;
|
|
const domainDir = path.join(certsDir, domain.replace(/\*/g, 'wildcard'));
|
|
if (!fs.existsSync(domainDir)) {
|
|
try {
|
|
fs.mkdirSync(domainDir, { recursive: true });
|
|
} catch (e) {
|
|
logError('CERT', 'Failed to create domain dir: ' + e.message);
|
|
return null;
|
|
}
|
|
}
|
|
const keyPath = path.join(domainDir, 'key.pem');
|
|
const certPath = path.join(domainDir, 'cert.pem');
|
|
|
|
if (!forceRegenerate && fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
try {
|
|
const certPem = fs.readFileSync(certPath, 'utf8');
|
|
const certMatch = certPem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/);
|
|
if (certMatch) {
|
|
const cert = forge.pki.certificateFromPem(certMatch[0]);
|
|
if (cert.validity.notAfter > new Date()) {
|
|
return {
|
|
key: fs.readFileSync(keyPath, 'utf8'),
|
|
cert: fs.readFileSync(certPath, 'utf8')
|
|
};
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
|
|
try {
|
|
const caKeyPem = fs.readFileSync(caKeyPath, 'utf8');
|
|
const caCertPem = fs.readFileSync(caCertPath, 'utf8');
|
|
const caKey = forge.pki.privateKeyFromPem(caKeyPem);
|
|
const caCert = forge.pki.certificateFromPem(caCertPem);
|
|
const keys = forge.pki.rsa.generateKeyPair(2048);
|
|
const cert = forge.pki.createCertificate();
|
|
cert.publicKey = keys.publicKey;
|
|
cert.serialNumber = String(Date.now());
|
|
cert.validity.notBefore = new Date();
|
|
cert.validity.notAfter = new Date();
|
|
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
|
|
const attrs = [{ name: 'commonName', value: domain }];
|
|
cert.setSubject(attrs);
|
|
cert.setIssuer(caCert.subject.attributes);
|
|
const akid = forge.pki.getPublicKeyFingerprint(caCert.publicKey, { md: forge.md.sha1.create() });
|
|
const skid = forge.pki.getPublicKeyFingerprint(cert.publicKey, { md: forge.md.sha1.create() });
|
|
const finalAltNames = Array.isArray(altNames) && altNames.length ? altNames : [{ type: 2, value: domain }];
|
|
cert.setExtensions([
|
|
{ name: 'basicConstraints', cA: false },
|
|
{ name: 'keyUsage', digitalSignature: true, keyEncipherment: true },
|
|
{ name: 'extKeyUsage', serverAuth: true },
|
|
{ name: 'subjectAltName', altNames: finalAltNames },
|
|
{ name: 'authorityKeyIdentifier', keyIdentifier: akid.getBytes() },
|
|
{ name: 'subjectKeyIdentifier' }
|
|
]);
|
|
cert.sign(caKey, forge.md.sha256.create(), { padding: forge.pki.rsa.PKCS1_v1_5 });
|
|
const pemKey = forge.pki.privateKeyToPem(keys.privateKey);
|
|
const pemCert = forge.pki.certificateToPem(cert);
|
|
const pemChain = pemCert + caCertPem;
|
|
fs.writeFileSync(keyPath, pemKey);
|
|
fs.writeFileSync(certPath, pemChain);
|
|
return { key: pemKey, cert: pemChain };
|
|
} catch (e) {
|
|
logError('CERT', 'Error signing certificate for ' + domain + ': ' + e.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the absolute path to the root CA certificate PEM file.
|
|
* @returns {string}
|
|
*/
|
|
function getCaCertPath() {
|
|
return caCertPath;
|
|
}
|
|
|
|
/**
|
|
* Return the absolute path to the directory where all certificates are stored.
|
|
* @returns {string}
|
|
*/
|
|
function getCertsDir() {
|
|
return certsDir;
|
|
}
|
|
|
|
/**
|
|
* Check whether the root CA is currently trusted by the OS.
|
|
* The check is platform-specific and compares SHA-256 fingerprints.
|
|
* @param {Function} callback - Called as `callback(isInstalled: boolean)`.
|
|
*/
|
|
function isRootCAInstalled(callback) {
|
|
if (platform === 'darwin') {
|
|
// Check SSL trust policy AND that the fingerprint matches the current CA on disk.
|
|
// This catches the case where an old CA with the same CN is trusted but the
|
|
// current CA (regenerated) is different — which would cause cert errors.
|
|
runCommand(`security verify-cert -p ssl -c "${caCertPath}" 2>/dev/null`, (err) => {
|
|
if (err) { callback(false); return; }
|
|
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
const loginKc = home ? path.join(home, 'Library', 'Keychains', 'login.keychain-db') : '';
|
|
runCommand(`security find-certificate -a -c "${CA_COMMON_NAME}" -Z "${loginKc}" 2>/dev/null`, (errFind, findOut) => {
|
|
if (errFind || !findOut) { callback(false); return; }
|
|
const m = findOut.match(/SHA-1 hash:\s*([0-9A-Fa-f]+)/);
|
|
if (!m) { callback(false); return; }
|
|
const installedSha1 = m[1].toUpperCase();
|
|
runCommand(`openssl x509 -in "${caCertPath}" -noout -fingerprint -sha1 2>/dev/null`, (errFp, fpOut) => {
|
|
if (errFp) { callback(false); return; }
|
|
const currentSha1 = (fpOut.split('=')[1] || '').trim().replace(/:/g, '').toUpperCase();
|
|
callback(installedSha1 === currentSha1);
|
|
});
|
|
});
|
|
});
|
|
} else if (platform === 'win32') {
|
|
// On Windows, match by the SHA-1 fingerprint of the current CA on disk
|
|
// so we detect stale entries with the same CN but a different key.
|
|
try {
|
|
const caCertPem = fs.readFileSync(caCertPath, 'utf8');
|
|
const caCert = forge.pki.certificateFromPem(caCertPem);
|
|
const der = forge.asn1.toDer(forge.pki.certificateToAsn1(caCert)).getBytes();
|
|
const md = forge.md.sha1.create();
|
|
md.update(der);
|
|
const thumbprint = md.digest().toHex().toUpperCase().match(/.{2}/g).join(' ');
|
|
runCommand(`certutil -store ROOT | findstr /i "${thumbprint.slice(0, 29)}"`, (err, stdout) => {
|
|
callback(!err && stdout && stdout.trim() !== '');
|
|
});
|
|
} catch (_) {
|
|
runCommand(`certutil -store ROOT | findstr "${CA_COMMON_NAME}"`, (err, stdout) => {
|
|
callback(!err && stdout && stdout.trim() !== '');
|
|
});
|
|
}
|
|
} else if (platform === 'linux') {
|
|
const caTargetPath = '/usr/local/share/ca-certificates/holesail-browser-ca.crt';
|
|
// Compare fingerprints: installed file vs current CA on disk
|
|
runCommand(`openssl x509 -in "${caTargetPath}" -noout -fingerprint -sha256 2>/dev/null`, (errInstalled, installedOut) => {
|
|
if (errInstalled) { callback(false); return; }
|
|
runCommand(`openssl x509 -in "${caCertPath}" -noout -fingerprint -sha256 2>/dev/null`, (errDisk, diskOut) => {
|
|
if (errDisk) { callback(false); return; }
|
|
callback(installedOut.trim() === diskOut.trim());
|
|
});
|
|
});
|
|
} else {
|
|
callback(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get or create a wildcard certificate for a parent domain of any depth.
|
|
* e.g. getOrCreateWildcardCert('hole.sail') -> cert covering *.hole.sail
|
|
* getOrCreateWildcardCert('love.hole.sail') -> cert covering *.love.hole.sail
|
|
*
|
|
* This is the preferred API for the SNI-aware HTTPS proxy — one cert per
|
|
* wildcard parent, selected at handshake time based on the SNI hostname.
|
|
*/
|
|
function getOrCreateWildcardCert (parentDomain) {
|
|
if (!parentDomain || parentDomain.split('.').length < 2) return null;
|
|
const certKey = 'wildcard.' + parentDomain;
|
|
const altNames = [
|
|
{ type: 2, value: '*.' + parentDomain },
|
|
{ type: 2, value: parentDomain }
|
|
];
|
|
return getOrCreateDomainCert(certKey, altNames);
|
|
}
|
|
|
|
module.exports = {
|
|
installRootCA,
|
|
isRootCAInstalled,
|
|
getOrCreateDomainCert,
|
|
getOrCreateWildcardCert,
|
|
getCaCertPath,
|
|
getCertsDir,
|
|
/** Resolves when the CA is ready (either already existed or was generated). */
|
|
caReady
|
|
};
|