Skip to content

feat(express): add default domain-scoped CA trust for BitGo connections - #10073

Open
pranishnepal wants to merge 1 commit into
masterfrom
pranishnepal/wcn-3731-add-domain-scoped-ca-trust-and-optional-spki-pinning-to
Open

pranishnepal wants to merge 1 commit into
masterfrom
pranishnepal/wcn-3731-add-domain-scoped-ca-trust-and-optional-spki-pinning-to

Conversation

@pranishnepal

@pranishnepal pranishnepal commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

What

Restricts outbound HTTPS connections from BitGo Express (and @bitgo/sdk-api when tlsPolicy is configured) to official BitGo domains (*.bitgo.com, *.bitgo-test.com, *.bitgo-staging.com, *.bitgo-dev.com, and their apex domains) to a bundled multi-provider Root CA store (modules/express/bitgo-ca.pem) by default, while keeping the standard Node.js / system trust store for all non-BitGo destinations (external signer, block explorers, webhooks, KMS).

  • Curated 18-root multi-provider CA bundle (modules/express/bitgo-ca.pem): Pre-includes the self-signed Root CAs for the 5 tier-1 providers BitGo uses today or would fail over to (Google Trust Services, Amazon Trust Services, DigiCert, Let's Encrypt / ISRG, and Sectigo). This shrinks the trusted CA surface for BitGo domains from ~150 global CAs down to 5 providers (covering app.*, admin.*, and cdn.bitgo.com out of the box) without breaking across routine leaf renewals, intermediate rotations, or CA migrations.
  • Single enforcement funnel (BitGoAPI.getAgentRequest): Every BitGo-bound request (requestPatch, fetchConstants, executeAssetRequest) goes through getAgentRequest, which attaches the restricted https.Agent({ ca }), refuses redirects (.redirects(0)) on covered hosts so a redirect cannot escape the domain policy, and refuses plaintext http:// downgrades on covered hosts.
  • Single boolean opt-out + proxy safety: Enabled by default (disablePinCerts: false). Operators can opt out with --disablepincerts or BITGO_DISABLE_PIN_CERTS=true. If BITGO_USE_PROXY is set, Express logs a startup warning and skips the bundled CA policy so existing proxy deployments continue to work.

Why this shape

NODE_EXTRA_CA_CERTS can only add trust; nothing in Node.js lets you remove a compromised CA from the global trust store, and replacing the global store would break non-BitGo outbound calls. Passing ca on a dedicated https.Agent replaces the trust store only for connections routed through that agent. Pairing appliesTo(url) with agent inside BitGoTlsPolicy guarantees that covered BitGo domains use the restricted bundle while all other destinations keep the system store.

What's in here

  1. @bitgo/sdk-api (modules/sdk-api/src/tlsPolicy.ts, bitgoAPI.ts, types.ts):
    • createBitGoTlsPolicy({ ca, hosts }) splits and validates every PEM certificate in ca via crypto.X509Certificate, normalizes and validates host patterns via parsePolicyHost, and returns { agent, appliesTo(url) }.
    • appliesTo(url) matches official BitGo hostnames (including trailing-dot FQDNs) and throws if a covered host is contacted over plain http:// or a schemeless URL.
    • getCaExpiries(ca) inspects certificate expiry dates for startup monitoring.
    • Enforced in BitGoAPI.getAgentRequest (bitgoAPI.ts:497-501) and wired into fetchConstants (bitgoAPI.ts:780).
  2. @bitgo/express (modules/express/src/tlsPolicy.ts, bitgo-ca.pem, config.ts, args.ts, expressApp.ts, clientRoutes.ts, fetchEncryptedPrivKeys.ts):
    • Ships modules/express/bitgo-ca.pem (18 self-signed Root CAs across GTS, Amazon, DigiCert, ISRG, and Sectigo).
    • Enables the policy by default across BITGO_DEFAULT_TLS_HOSTS (bitgo.com, .bitgo.com, bitgo-test.com, .bitgo-test.com, bitgo-staging.com, .bitgo-staging.com, bitgo-dev.com, .bitgo-dev.com).
    • Adds --disablepincerts / BITGO_DISABLE_PIN_CERTS (with proper boolean parsing so BITGO_DISABLE_PIN_CERTS=false stays enabled).
    • Validates the policy at boot in checkPreconditions (expressApp.ts:273) and warns via console.warn if any bundled CA expires within 30 days.
  3. CI & Live Sync Check (scripts/verify-bitgo-policy-data.sh, modules/express/package.json):
    • Runs automatically before express unit-test (yarn test:data), verifying that all 18 certificates in bitgo-ca.pem parse, are self-signed CA roots (issuer === subject and cert.ca === true), are >30 days from expiry, and cover all 13 BitGo apex/subdomain hosts.
    • Supports bash scripts/verify-bitgo-policy-data.sh --live to verify real TLS handshakes against live app.*, admin.*, and cdn.bitgo.com endpoints using only bitgo-ca.pem.

How it was tested

  • Unit tests: modules/sdk-api/test/unit/tlsPolicy.ts (18/18), modules/express/test/unit/tlsPolicy.ts, config.ts, bitgoExpress.ts (55/55), and full express unit suite.
  • CI & live endpoint verification (scripts/verify-bitgo-policy-data.sh --live): Verified live TLS handshakes against app.bitgo.com, app.bitgo-test.com, app.bitgo-staging.com, app.bitgo-dev.com, admin.bitgo*.com, and cdn.bitgo.com using only bitgo-ca.pem.
  • Real staging E2E (app.bitgo-staging.com): Verified authenticated SDK/Express calls (fetchConstants, me(), wallet read, session unlock) with the default 18-root policy enabled, plus negative rejection when an unapproved CA bundle is used (Linear test evidence).

TICKET: WCN-3731

@linear-code

linear-code Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

WCN-3731

@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Unit tests are failing on Node 26.x (Current release line, non-blocking). This is not an LTS version yet, so it does not block merge, but it signals an incompatibility to fix before Node 26.x becomes LTS.

View run

@pranishnepal
pranishnepal marked this pull request as ready for review October 27, 2026 16:25
@pranishnepal
pranishnepal requested review from a team as code owners October 27, 2026 16:25

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@pranishnepal
pranishnepal marked this pull request as draft October 27, 2026 16:54
@pranishnepal
pranishnepal force-pushed the pranishnepal/wcn-3731-add-domain-scoped-ca-trust-and-optional-spki-pinning-to branch 2 times, most recently from 7fc7465 to c8d0fe9 Compare October 28, 2026 05:03
@pranishnepal pranishnepal changed the title Pranishnepal/wcn 3731 add domain scoped ca trust and optional spki pinning to feat(express): add default domain-scoped CA trust for BitGo connections Oct 28, 2026
@pranishnepal
pranishnepal marked this pull request as ready for review October 28, 2026 05:17
@pranishnepal
pranishnepal force-pushed the pranishnepal/wcn-3731-add-domain-scoped-ca-trust-and-optional-spki-pinning-to branch from c8d0fe9 to d2625bb Compare October 28, 2026 17:38
Restrict outbound HTTPS connections to official BitGo domains
(*.bitgo.com, *.bitgo-test.com, *.bitgo-staging.com, *.bitgo-dev.com,
and their apex domains) to a bundled multi-provider root CA store
(Google Trust Services, Amazon Trust Services, DigiCert, Let's
Encrypt/ISRG, and Sectigo) while keeping the system trust store for all
non-BitGo destinations:
- sdk-api: add createBitGoTlsPolicy({ ca, hosts }) and enforce it in
  BitGoAPI.getAgentRequest (refusing redirects and plain http on covered
  hosts)
- express: enable the bundled 18-root CA policy by default at startup,
  with a single opt-out flag (--disablepincerts / BITGO_DISABLE_PIN_CERTS)
  and automatic proxy fallback when BITGO_USE_PROXY is set
- scripts/verify-bitgo-policy-data.sh: CI check verifying all bundled
  roots are self-signed, valid (>30d), and cover all BitGo domains
  (plus --live handshake verification)

TICKET: WCN-3731
@pranishnepal
pranishnepal force-pushed the pranishnepal/wcn-3731-add-domain-scoped-ca-trust-and-optional-spki-pinning-to branch from d2625bb to 21198bd Compare October 28, 2026 17:48
"unprettied": "grep -R -L --include '*.ts' --include '*.js' --include '*.json' '@prettier' src test",
"fmt": "yarn prettier --write '{src,test}/**/*.{ts,js,json}'"
"fmt": "yarn prettier --write '{src,test}/**/*.{ts,js,json}'",
"test:data": "bash ../../scripts/verify-bitgo-policy-data.sh --live"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will run prior to the unit test and fail the CI if it needs updating ~30d prior to the actual expiration date

@bitgo/express: $ bash ../../scripts/verify-bitgo-policy-data.sh --live
@bitgo/express: ok: 18 self-signed CA certs valid (>30d), 13 BitGo hosts covered, http downgrades throw
@bitgo/express: live ok: https://app.bitgo.com/ (HTTP 302)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant