Repository navigation
[SDK-8000] Reject TLS chains carrying the compromised eMudhra intermediate keys - #1203
harshit-browserstack wants to merge 5 commits into
Conversation
… keys (SDK-8000) Node still trusts the 8 eMudhra intermediates whose private keys are recoverable. Install a tls.checkServerIdentity wrapper from bin/helpers/utils.js, which the CLI and the reporter/plugins that run inside the customer's Cypress all load, and reject any chain containing one of the 8 SPKI SHA-256 hashes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Note Some coding-guideline sources were skipped; the reason is listed next to each one. Other guidelines were applied as usual. On a public repository, sources from your configuration are listed without their names. Skipped guideline sources: 1
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📜 Recent review details
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
|
✅ Good to go Re-review at 1. Missing unit test: resolved.
I also broke the module one change at a time and re-ran the file. Each break was caught: a wrong G3B hash fails the key case (1 failure); the chain check disabled fails 3 cases; name matching removed fails both name cases (2); the hostname early-return removed fails the hostname-order case (1); the existing-install guard removed fails the double-wrap case (1). 2. Unguarded install in 3. Suggestion (non-blocking, does not gate): when the install fails, the protection turns off silently and the only trace is a debug-level log. Not breaking the customer's run is the right call, but this is a security control failing open. Consider logging that single line at warn instead of debug so the unprotected state is visible. 4. Caveat on test coverage: the tests exercise only 1 of the 8 SPKI hashes (G3B DV) and 1 of the 8 names. The PR author reports checking all 16 published certificates against the advisory JSON; that was not re-verified here. A mismatch in the other 7 hashes or names would not be caught by these tests. Per-File Confidence
Change map (generated deterministically from the diff)graph LR
subgraph nnode_agent["node-agent"]
nbin_helpers_distrustedCaCheck_js["distrustedCaCheck.js<br/>~119 lines"]
ntest_unit_bin_helpers_distrustedCaCheck_js["distrustedCaCheck.js<br/>~95 lines"]
nbin_helpers_utils_js["utils.js<br/>~7 lines"]
end
↻ This verdict comment is the review anchor — it's updated in place on each run (the gate posts its status separately). — SDK PR Review Agent |
…CA names (SDK-8000) Two gaps from review: a parse failure on one certificate ended the walk for the whole chain, and Node links issuerCertificate by name, so a decoy certificate sent ahead of the real intermediate diverted the walk past the distrusted key. Skip only the certificate that fails to parse, and also match the 8 intermediates' subject names against each certificate's subject and issuer (they are pathlen:0, so the leaf's issuer is always one of them). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review follow-up — two gaps in the chain check, fixed in the latest commit (same change in browserstack-binary#1960, browserstack-node-agent#2742, browserstack-cypress-cli#1203, browserstack-local-nodejs#188):
Verified locally: the decoy chain is now refused in both orders on Node 16/18/22 (20/20), the earlier cases still hold (50/50, including a forced parse failure on the leaf), the 8 listed names match the parsed subjects of all 16 published certificates, and 20 hosts the SDK, binary and Local use still connect. |
OpenSSL chains names case-insensitively with whitespace collapsed, so a leaf naming its issuer 'em dv tls ca - g3b' still validates through the real intermediate; with a differently-cased decoy diverting the issuer walk, exact matching missed both names. Normalize names the same way before comparing, and never report an empty name when a key matches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security-review follow-up (latest commit): names are now compared the way OpenSSL chains them (case-insensitive, whitespace collapsed). Reproduced the gap first: a leaf naming its issuer |
Jest runs each test file in its own context with its own global but shares core modules, so the SDK's per-file setup re-ran the install and wrapped tls.checkServerIdentity again for every test file. Keep the install state on the tls module instead; worker threads still get their own tls module and install their own check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re-review follow-up (latest commit): the install state now lives on the |
SDK PR Review: changes requested (2 findings)Reviewed HEAD 1. 🟠 No unit test for the distrusted-CA check (
|
…DK-8000) utils.js also loads inside the customer's Cypress process, so an install that threw would stop it loading. Log at debug and carry on instead. Add unit tests for the name, normalised-name and key matches, a clean chain, the hostname error taking precedence, and a second install not wrapping again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review follow-up (96b3a51)
On the "not covered" notes:
|
|
RUN_TESTS |
Summary
Jira: SDK-8000. Same fix as SDK-7999 in browserstack-binary#1960 and browserstack-node-agent#2742.
The private keys of 8 eMudhra intermediate CAs are recoverable (incident), and Node.js has not shipped the distrust yet (nodejs/node#66871 is unreleased). The CLI runs on the customer's Node and sends the username and access key to
api,api-cloud,automateandeds.browserstack.com, so anyone on the network path can present a certificate the CLI accepts and read the key and the uploaded test suite.Change
bin/helpers/distrustedCaCheck.jswrapstls.checkServerIdentity: Node's hostname check runs first, then the connection is refused (ERR_TLS_DISTRUSTED_INTERMEDIATE) if any certificate in the chain has one of the 8 published SPKI SHA-256 hashes. Matching the key catches both the emSign-issued and the Sectigo cross-signed copies.bin/helpers/utils.jsinstead ofbin/runner.js:utils.jsis loaded by every CLI command and by the code that runs inside the customer's Cypress process (Test Observability reporter and crash reporter, Accessibility plugin), which a hook inrunner.jswould not reach.browserstack-local's asyncstart()(1.5.13), which does the endpoint lookup and the binary download in-process, so this hook covers them too.crypto.createPublicKeyfrom the certificate:cert.pubkeyis the raw point for EC keys (hashing it never matches), andX509Certificateneeds Node 15.6.Verification
build-infowithRAILS_HOST/USAGE_REPORTING_URLpointed atdv-g3b-emudhraanddv-g3b-aaa-emudhra.we-have-a-problem.com(fake credentials):mastercompletes TLS and receives the host's response on Node 14 and 18 (both chains); this branch getsERR_TLS_DISTRUSTED_INTERMEDIATEand receives nothing, on Node 14, 18 and 22.api.browserstack.comis still reached.start()withbs-hostpointed at the incident host (fake key):mastercompletes TLS and parses the host's reply; this branch refuses the connection.master.browserstack-cypress runon BrowserStack.Summary by CodeRabbit