From 0e110d92316e813f1fb0e85eba723617c5836b8b Mon Sep 17 00:00:00 2001 From: Wei Chen Date: Tue, 19 May 2026 15:11:16 -0700 Subject: [PATCH] Persist root CA only after trust-store install succeeds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit installCertificateAuthority used to write rootCACertPath and rootCAKeyPath to their canonical paths *before* invoking addToTrustStores. If the trust-store install failed or was declined (e.g. user clicks No on the Windows Security Warning), the on-disk state remained, and certificateFor's '!existsSync(rootCAKeyPath)' guard skipped re-installation forever — the browser stayed broken with NET::ERR_CERT_AUTHORITY_INVALID and devcert never re-prompted. Reorder so the cert+key are generated into temp paths, the trust-store install runs first, and canonical files are written only if it succeeded. Also propagate certutil failures from win32 addToTrustStores. Firefox open failures are still tolerated (most users do not have Firefox). --- src/certificate-authority.ts | 38 +++++++++++++++++++++++++++--------- src/platforms/win32.ts | 16 ++++++++++----- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/certificate-authority.ts b/src/certificate-authority.ts index 2587cf3..43d2a50 100644 --- a/src/certificate-authority.ts +++ b/src/certificate-authority.ts @@ -33,23 +33,43 @@ export default async function installCertificateAuthority(options: Options = {}) uninstall(); ensureConfigDirs(); - debug(`Making a temp working directory for files to copied in`); - let rootKeyPath = mktmp(); - debug(`Generating the OpenSSL configuration needed to setup the certificate authority`); seedConfigFiles(); + // Generate the root key + CA cert into TEMP paths first, so a failed/declined + // trust-store install does not leave behind canonical files that make + // certificateFor's `!existsSync(rootCAKeyPath)` guard skip re-installation + // on subsequent runs. + debug(`Making a temp working directory for files to be copied in`); + let tmpRootKeyPath = mktmp(); + let tmpRootCertPath = mktmp(); + debug(`Generating a private key`); - generateKey(rootKeyPath); + generateKey(tmpRootKeyPath); debug(`Generating a CA certificate`); - openssl(['req', '-new', '-x509', '-config', caSelfSignConfig, '-key', rootKeyPath, '-out', rootCACertPath, '-days', '825']); - - debug('Saving certificate authority credentials'); - await saveCertificateAuthorityCredentials(rootKeyPath); + openssl(['req', '-new', '-x509', '-config', caSelfSignConfig, '-key', tmpRootKeyPath, '-out', tmpRootCertPath, '-days', '825']); + // Install into the OS trust store BEFORE persisting to the canonical paths. + // If addToTrustStores throws (e.g. user declines the Windows Security Warning, + // or `sudo security add-trusted-cert` fails on macOS), the canonical files + // are never written and the next certificateFor() call will retry the whole + // install — including the user-facing prompt. debug(`Adding the root certificate authority to trust stores`); - await currentPlatform.addToTrustStores(rootCACertPath, options); + try { + await currentPlatform.addToTrustStores(tmpRootCertPath, options); + } catch (e) { + rm(tmpRootKeyPath); + rm(tmpRootCertPath); + throw e; + } + + debug(`Persisting root CA certificate and key to their canonical paths`); + writeFile(rootCACertPath, readFile(tmpRootCertPath)); + await saveCertificateAuthorityCredentials(tmpRootKeyPath); + + rm(tmpRootKeyPath); + rm(tmpRootCertPath); } /** diff --git a/src/platforms/win32.ts b/src/platforms/win32.ts index 94a78d7..9d02ba3 100644 --- a/src/platforms/win32.ts +++ b/src/platforms/win32.ts @@ -30,11 +30,17 @@ export default class WindowsPlatform implements Platform { try { run('certutil', ['-addstore', '-user', 'root', certificatePath]); } catch (e) { - e.output.map((buffer: Buffer) => { - if (buffer) { - console.log(buffer.toString()); - } - }); + if (e.output) { + e.output.map((buffer: Buffer) => { + if (buffer) { + console.log(buffer.toString()); + } + }); + } + // certutil non-zero exit means the user declined the Windows Security + // Warning, or the store is otherwise unwritable. Propagate so the caller + // can avoid persisting on-disk state that would block future retries. + throw e; } debug('adding devcert root to Firefox trust store') // Firefox (don't even try NSS certutil, no easy install for Windows)