nodejs / nodejs/node

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

オープン
#63,313 コメント 4 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

主要言語
JavaScript
スター
122k
フォーク
37.3k
平均マージ
4日 2時間
マージ済み PR(30日)
283

説明

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.

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

src/crypto/crypto_x509.cc の node::crypto::ReadMacOSKeychainCertificates から始め、その信頼ポリシーの評価を追跡してください。macOS で、提供されている tls.getCACertificates("system") の時間計測再現コードを実行し、システム CA の収集によって失効に関連する trustd の遅延が回避される一方で、返される証明書と HTTPS の動作が維持されることを確認してください。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
javascript, macos, node.js
領域
networking, operating-systems, security
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
活発
明瞭さ
明確に書かれている
初心者へのやさしさ
58/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。