fix(desktop): trust macOS keychain CAs for remote gateways like Windows
installWindowsSystemCaTrust only handled win32, so on macOS a remote
gateway fronted by a keychain-trusted private CA rendered fine but
every main-process Node https call failed with 'unable to get local
issuer certificate'. Generalize it to installSystemCaTrust covering
both stores tls.getCACertificates('system') can enumerate: the Windows
cert store and the macOS keychain (user + System roots, honoring the
SSL 'Always Trust' policy; Node >= 22.15). Linux stays excluded since
its 'system' store is the OpenSSL scan the default trust already
covers.
Fixes https://github.com/NousResearch/hermes-agent/issues/57241
This commit is contained in:
@@ -598,7 +598,7 @@ import {
|
||||
shouldSurfaceErrorForRendererStackCookieCrashLoop,
|
||||
writeGpuStackCookieMarker
|
||||
} from './windows-stack-cookie-fallback'
|
||||
import { installWindowsSystemCaTrust } from './windows-system-ca'
|
||||
import { installSystemCaTrust } from './system-ca'
|
||||
import { readWindowsUserEnvVar } from './windows-user-env'
|
||||
import { isPackagedInstallPath as isPackagedInstallPathUnderRoots } from './workspace-cwd'
|
||||
import { readWslWindowsClipboardImage } from './wsl-clipboard-image'
|
||||
@@ -18495,14 +18495,14 @@ app.whenReady().then(() => {
|
||||
startChromiumLogWatcher(CHROMIUM_LOG_PATH)
|
||||
}
|
||||
|
||||
const systemCa = installWindowsSystemCaTrust(tls)
|
||||
const systemCa = installSystemCaTrust(tls)
|
||||
|
||||
if (systemCa.applied) {
|
||||
rememberLog(
|
||||
`[tls] trusting ${systemCa.systemCertificateCount} Windows system CA certificate(s) for backend connections`
|
||||
`[tls] trusting ${systemCa.systemCertificateCount} OS CA certificate(s) for backend connections`
|
||||
)
|
||||
} else if (systemCa.error) {
|
||||
rememberLog(`[tls] could not load Windows system CA certificates: ${systemCa.error}`)
|
||||
rememberLog(`[tls] could not load OS system CA certificates: ${systemCa.error}`)
|
||||
}
|
||||
|
||||
// Keyring-less Linux `--password-store=basic` support. This must run before
|
||||
|
||||
@@ -3,8 +3,8 @@ import { X509Certificate } from 'node:crypto'
|
||||
|
||||
import { afterEach, test, vi } from 'vitest'
|
||||
|
||||
import { bundledRoot, expiredRoot, privateRoot } from './fixtures/windows-system-ca'
|
||||
import { installWindowsSystemCaTrust, type NodeTlsCaApi } from './windows-system-ca'
|
||||
import { bundledRoot, expiredRoot, privateRoot } from './fixtures/system-ca'
|
||||
import { installSystemCaTrust, type NodeTlsCaApi } from './system-ca'
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
@@ -16,7 +16,7 @@ test('excludes expired roots and deduplicates real certificates with defaults fi
|
||||
[expiredRoot, bundledRoot.replaceAll('\n', '\r\n'), privateRoot, privateRoot, 'unparseable-system']
|
||||
)
|
||||
|
||||
const result = installWindowsSystemCaTrust(tlsApi, 'win32')
|
||||
const result = installSystemCaTrust(tlsApi, 'win32')
|
||||
|
||||
assert.deepEqual(tlsApi.installed, [[bundledRoot, 'unparseable-default', privateRoot, 'unparseable-system']])
|
||||
assert.deepEqual(result, { applied: true, systemCertificateCount: 2, totalCertificateCount: 4 })
|
||||
@@ -26,7 +26,7 @@ test('excludes a root at its exact expiry while retaining valid defaults', () =>
|
||||
vi.spyOn(Date, 'now').mockReturnValue(new X509Certificate(expiredRoot).validToDate.getTime())
|
||||
const tlsApi = fakeTlsApi([bundledRoot], [expiredRoot])
|
||||
|
||||
const result = installWindowsSystemCaTrust(tlsApi, 'win32')
|
||||
const result = installSystemCaTrust(tlsApi, 'win32')
|
||||
|
||||
assert.deepEqual(tlsApi.installed, [[bundledRoot]])
|
||||
assert.deepEqual(result, { applied: true, systemCertificateCount: 0, totalCertificateCount: 1 })
|
||||
@@ -52,7 +52,7 @@ function fakeTlsApi(
|
||||
test('installs Windows system CAs without dropping existing defaults', () => {
|
||||
const tlsApi = fakeTlsApi(['mozilla-root', 'extra-ca'], ['machine-root', 'user-root'])
|
||||
|
||||
const result = installWindowsSystemCaTrust(tlsApi, 'win32')
|
||||
const result = installSystemCaTrust(tlsApi, 'win32')
|
||||
|
||||
assert.deepEqual(tlsApi.installed, [['mozilla-root', 'extra-ca', 'machine-root', 'user-root']])
|
||||
assert.deepEqual(result, {
|
||||
@@ -62,7 +62,39 @@ test('installs Windows system CAs without dropping existing defaults', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.each(['darwin', 'linux'] as const)('does not inspect or replace CAs on %s', platform => {
|
||||
test('installs macOS keychain CAs (the renderer already trusts them; Node https did not)', () => {
|
||||
// #57241: a remote gateway fronted by a private/homelab CA trusted in Keychain Access
|
||||
// loads in the Chromium renderer but fails every main-process Node https call with
|
||||
// `unable to get local issuer certificate`. On darwin the same installer must fold the
|
||||
// keychain's user + System roots into the default trust store.
|
||||
const tlsApi = fakeTlsApi(['mozilla-root'], ['homelab-root', 'corp-root'])
|
||||
|
||||
const result = installSystemCaTrust(tlsApi, 'darwin')
|
||||
|
||||
assert.deepEqual(tlsApi.installed, [['mozilla-root', 'homelab-root', 'corp-root']])
|
||||
assert.deepEqual(result, {
|
||||
applied: true,
|
||||
systemCertificateCount: 2,
|
||||
totalCertificateCount: 3
|
||||
})
|
||||
})
|
||||
|
||||
test('darwin without keychain trust additions leaves the defaults untouched', () => {
|
||||
// A machine with no user-installed anchors enumerates zero 'system' certs; the default
|
||||
// trust store must stay untouched (also the pre-keychain-reader Node behavior).
|
||||
const tlsApi = fakeTlsApi(['mozilla-root'], [])
|
||||
|
||||
const result = installSystemCaTrust(tlsApi, 'darwin')
|
||||
|
||||
assert.deepEqual(tlsApi.installed, [])
|
||||
assert.deepEqual(result, {
|
||||
applied: false,
|
||||
systemCertificateCount: 0,
|
||||
totalCertificateCount: 1
|
||||
})
|
||||
})
|
||||
|
||||
test('does not inspect or replace CAs on linux', () => {
|
||||
let reads = 0
|
||||
|
||||
const tlsApi: NodeTlsCaApi = {
|
||||
@@ -76,7 +108,7 @@ test.each(['darwin', 'linux'] as const)('does not inspect or replace CAs on %s',
|
||||
}
|
||||
}
|
||||
|
||||
const result = installWindowsSystemCaTrust(tlsApi, platform)
|
||||
const result = installSystemCaTrust(tlsApi, 'linux')
|
||||
|
||||
assert.equal(reads, 0)
|
||||
assert.deepEqual(result, {
|
||||
@@ -86,10 +118,10 @@ test.each(['darwin', 'linux'] as const)('does not inspect or replace CAs on %s',
|
||||
})
|
||||
})
|
||||
|
||||
test('leaves the existing defaults untouched when Windows has no system CAs', () => {
|
||||
test('leaves the existing defaults untouched when the OS store has no system CAs', () => {
|
||||
const tlsApi = fakeTlsApi(['mozilla-root'], [])
|
||||
|
||||
const result = installWindowsSystemCaTrust(tlsApi, 'win32')
|
||||
const result = installSystemCaTrust(tlsApi, 'win32')
|
||||
|
||||
assert.deepEqual(tlsApi.installed, [])
|
||||
assert.deepEqual(result, {
|
||||
@@ -99,7 +131,7 @@ test('leaves the existing defaults untouched when Windows has no system CAs', ()
|
||||
})
|
||||
})
|
||||
|
||||
test('fails open when the runtime cannot load the Windows certificate store', () => {
|
||||
test('fails open when the runtime cannot load the OS certificate store', () => {
|
||||
const tlsApi: NodeTlsCaApi = {
|
||||
getCACertificates(type = 'default') {
|
||||
if (type === 'system') {
|
||||
@@ -113,7 +145,7 @@ test('fails open when the runtime cannot load the Windows certificate store', ()
|
||||
}
|
||||
}
|
||||
|
||||
const result = installWindowsSystemCaTrust(tlsApi, 'win32')
|
||||
const result = installSystemCaTrust(tlsApi, 'win32')
|
||||
|
||||
assert.deepEqual(result, {
|
||||
applied: false,
|
||||
@@ -5,15 +5,21 @@ interface NodeTlsCaApi {
|
||||
setDefaultCACertificates(certificates: string[]): void
|
||||
}
|
||||
|
||||
interface WindowsSystemCaResult {
|
||||
interface SystemCaResult {
|
||||
applied: boolean
|
||||
systemCertificateCount: number
|
||||
totalCertificateCount: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
function installWindowsSystemCaTrust(tlsApi: NodeTlsCaApi, platform = process.platform): WindowsSystemCaResult {
|
||||
if (platform !== 'win32') {
|
||||
// Platforms whose OS trust store tls.getCACertificates('system') can enumerate: the Windows
|
||||
// cert store and the macOS keychain (Node reads user + System keychains there, honoring the
|
||||
// "Always Trust" SSL policy — Node ≥ 22.15). Linux is deliberately absent: its 'system' store
|
||||
// is the OpenSSL directory scan, which the default trust already covers.
|
||||
const SYSTEM_CA_PLATFORMS = new Set(['win32', 'darwin'])
|
||||
|
||||
function installSystemCaTrust(tlsApi: NodeTlsCaApi, platform = process.platform): SystemCaResult {
|
||||
if (!SYSTEM_CA_PLATFORMS.has(platform)) {
|
||||
return {
|
||||
applied: false,
|
||||
systemCertificateCount: 0,
|
||||
@@ -75,5 +81,5 @@ function installWindowsSystemCaTrust(tlsApi: NodeTlsCaApi, platform = process.pl
|
||||
}
|
||||
}
|
||||
|
||||
export { installWindowsSystemCaTrust }
|
||||
export type { NodeTlsCaApi, WindowsSystemCaResult }
|
||||
export { installSystemCaTrust }
|
||||
export type { NodeTlsCaApi, SystemCaResult }
|
||||
Reference in New Issue
Block a user