Files
holesail-browser/native-host/certificate-authority.js
T
Raven Scott f1e98a7edd
CI / Build & Test (push) Successful in 2m54s
docs: add CONTRIBUTING.md, CHANGELOG.md, and JSDoc to entire codebase
Add docs/CONTRIBUTING.md covering the build system, dev workflow, all
npm scripts, how to add new native host message types, code style, and
debugging guidance.

Add CHANGELOG.md at the project root documenting all features and fixes
across the 1.0.0 release.

Add JSDoc (@param, @returns) to all previously undocumented exported
functions across 35 JS files:
- native-host/holesail-manager/ (index, virtual-hosts, service-tunnels,
  servers, port-allocator)
- native-host top-level managers (startup, connect-proxy, https-proxy,
  certificate-authority, ssh-manager, rdp-manager)
- extension/background/ (logs, native-messaging, proxy, message-router)
- extension/dashboard/core/ (utils, navigation, init)
- extension/dashboard/ui/ (modal, toast, state-tag)
- extension/dashboard/pages/ (all 10 page files)
- extension/dashboard/refresh.js, events.js
- extension/dashboard/data/hostname-validator.js
- scripts/ (build-host, run-install)
2026-03-01 00:40:53 -05:00

497 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),
* `update-ca-certificates` on Linux, 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';
const copyCmd = `cp "${caPath}" ${caTargetPath}`;
runCommand(copyCmd, (errCopy) => {
if (errCopy) {
logError('CA', 'Error copying CA on Linux: ' + errCopy.message);
callback(errCopy);
return;
}
runCommand('update-ca-certificates', (errUpdate) => {
if (errUpdate) {
logError('CA', 'Error updating CA on Linux: ' + errUpdate.message);
callback(errUpdate);
} else {
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
};