nodejs / nodejs/node

macOS: `tls.getCACertificates('system')` uses revocation-enabled trust policy, causing 5-10s startup on machines with network filters

Ouverte
#63,313 4 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

Langage dominant
JavaScript
Étoiles
122k
Forks
37.3k
Merge moyen
4 j 2 h
PR mergées (30 j)
283

Description

Version

v24.15.0 (also v25 per anthropics/claude-code#53660 measurements)

Platform

Darwin (macOS 26.4.1, Apple Silicon Mac16,12). Reproduces on macOS Sonoma/Sequoia per the linked downstream reports.

Subsystem

tls, crypto

What steps will reproduce the bug?

On a Mac with any active network-flow filter (corporate NetworkExtension, content filter, or even a userland VPN-style daemon like ZeroTier), and a moderately-sized login keychain:

$ node -e 'const t0=process.hrtime.bigint();
           const c = require("tls").getCACertificates("system");
           console.log(`returned=${c.length} elapsed=${(Number(process.hrtime.bigint()-t0)/1e6).toFixed(0)}ms`)'
returned=34 elapsed=5287ms

The same call against "bundled" returns in ~0.1ms. HTTPS request times via --use-system-ca are ~100× slower than --use-bundled-ca (5093ms vs 103ms to api.github.com).

The bug is in node::crypto::ReadMacOSKeychainCertificates (src/crypto/crypto_x509.cc / wherever the macOS branch lives). It calls SecTrustEvaluateWithError with a revocation-enabled policy when collecting trust anchors, which causes trustd to attempt AIA/OCSP/CRL fetches for every cert. When a NetworkExtension content filter is present, each flow goes through per-flow cryptographic signing (~1-2ms × hundreds of certs = 5-10 seconds total) before being denied. Without a flow filter the cost is smaller but still present.

How often does it reproduce?

Every invocation. Stable across reboots and Node versions in the v24.x / v25 line.

What is the expected behavior? Why is that the expected behavior?

tls.getCACertificates("system") should be fast (single-digit milliseconds for a typical keychain) because it's collecting trust anchors, not validating a server chain. Revocation checks are pointless during anchor collection — a trust anchor's revocation status is determined by user/admin trust settings, not by an OCSP responder.

What do you see instead?

5-10+ seconds of latency, predominantly in synchronous XPC round-trips to trustd. Effectively blocks Node CLI startup any time the CLI's CA loader touches the system store. Several Node-based CLIs are affected in practice:

  • anthropics/claude-code#53660 — comprehensive root-cause analysis. Their measurement on a corporate Mac: 9.7s for 18 certs (530ms/cert), vs 0.3ms/cert for direct C calls using SecPolicyCreateBasicX509. They shipped a CLAUDE_CODE_CERT_STORE=bundled env var workaround.
  • github/copilot-cli#3330 (filing concurrently) — same call costs 5+ seconds on every copilot invocation. The CLI's code explicitly asks for "system" certs in addition to "bundled".
  • github/copilot-cli#1250 — Windows variant of the same call (different failure mode, same offending line).
Additional information
Proposed fix

Per the anthropics/claude-code#53660 analysis, the call should use SecPolicyCreateBasicX509() (no revocation) when enumerating system CAs to use as trust anchors. Alternatively, the function could skip trust evaluation entirely and call SecTrustSettingsCopyCertificates(kSecTrustSettingsDomainAdmin/System) to collect anchors without validation. Either approach removes revocation from the hot startup path.

Reproducer kit
# Standalone measurement
node -e 'const t0=process.hrtime.bigint();
         require("tls").getCACertificates("system");
         console.log(`${(Number(process.hrtime.bigint()-t0)/1e6).toFixed(0)}ms`)'

# HTTPS A/B
node --use-bundled-ca -e 'require("https").get("https://api.github.com/zen",r=>r.on("data",()=>{}).on("end",()=>{}))'   # ~100ms
node --use-system-ca  -e 'require("https").get("https://api.github.com/zen",r=>r.on("data",()=>{}).on("end",()=>{}))'   # ~5s

# Sample to confirm the syscall pattern
node --use-system-ca -e 'require("tls").getCACertificates("system")' &
PID=$!; sleep 0.1; sample $PID 3 -mayDie

Sample output during the wait deterministically shows:

node::crypto::ReadMacOSKeychainCertificates
  → node::crypto::IsCertificateTrustedForPolicy
    → SecTrustEvaluateWithError
      → securityd_send_sync_and_do (XPC to trustd)
Why this matters beyond corporate networks

Anthropic's analysis (and the original v24 design assumption) treats this as a "corporate Macs only" problem. In practice, any userland flow-handling daemon — ZeroTier, Tailscale, Cloudflare WARP, certain VPN clients — places similar code in the path. The fix doesn't depend on detecting the filter; it's simply correct to skip revocation when enumerating trust anchors.

Related Node-side tracking
  • #58990 — tracking issue for custom CA support; lists "Load system CA certificates off thread" as done, but off-thread loading doesn't fix the cost, only moves it. The root cause is the revocation policy.
  • #57163 — --use-system-ca intermediate-cert issues on Windows.
  • #58346 — request for env-var alternative to --use-system-ca.

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Piste de recherche

Commencez dans src/crypto/crypto_x509.cc, au niveau de node::crypto::ReadMacOSKeychainCertificates, et suivez son évaluation de la politique de confiance. Exécutez sur macOS le reproducteur de mesure du temps fourni tls.getCACertificates("system"), puis vérifiez que la collecte des CA système évite le délai de trustd lié à la révocation tout en préservant les certificats renvoyés et le comportement HTTPS.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
javascript, macos, node.js
Domaine
networking, operating-systems, security
Type d'issue
Bug
Difficulté
4/5
Temps estimé
3-5 jours
Activité
Active
Clarté
Clairement spécifiée
Accessibilité débutants
58/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.