Repository navigation
Conversation
The private keys of eight eMudhra (emSign) intermediate CA certificates can be recovered from the public certificates (https://emudhra.we-have-a-problem.com). - Vercel Functions: remove "emSign ECC Root CA - G3" and, as a defensive measure, "AAA Certificate Services" from Node's default CA list when a server instance starts (src/instrumentation.ts), matched by SHA-256 fingerprint. Vercel Functions still run Node.js 24.21.0, whose compiled-in CA list includes the emSign root, and the /api/proxy route forwards Authorization headers to the Modal backend. - Modal backend image: after the requirements step, upgrade Debian's ca-certificates, require certifi>=2026.10.19, and point gRPC at the refreshed Debian bundle with GRPC_DEFAULT_SSL_ROOTS_FILE_PATH. - Modal eval image: require certifi>=2026.10.19 after the requirements step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #305 +/- ##
==========================================
+ Coverage 74.04% 75.12% +1.07%
==========================================
Files 153 157 +4
Lines 11769 11914 +145
Branches 2010 2047 +37
==========================================
+ Hits 8714 8950 +236
+ Misses 2313 2209 -104
- Partials 742 755 +13
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
The frontend jobs in tests.yml and deploy.yml ran Node.js 20.19, which gets no release without the compromised eMudhra root "emSign ECC Root CA - G3". Under it, npm ci sends requests whose responses the lockfile's integrity hashes do not cover: the audit request (POST /-/npm/v1/security/advisories/bulk) and package-metadata requests for the packages the audit reports. Use Node.js 24, the version the Vercel project runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #306
Incident
On 2026-10-20 it was disclosed that the private keys of eight eMudhra (emSign) intermediate CA certificates can be recovered from the public certificates (https://emudhra.we-have-a-problem.com). Anyone can therefore issue a certificate for any domain that chains to "emSign ECC Root CA - G3" or, through Sectigo's cross-signatures, to "AAA Certificate Services". Browsers already block them; what stays exposed is software that carries its own CA list, or reads an operating-system CA store that has not been updated.
What in this repository trusted the affected roots
/uk/chat/api/proxy/*route on Vercel.frontend/src/app/api/proxy/[...slug]/route.tsruns as a Vercel Function (runtime = "nodejs", projectpolicyengine-uk-chat, Node.js 24.x) andfetches the Modal backend, forwarding the user'sAuthorizationandX-User-Idheaders. Vercel Functions ran Node.js 24.21.0 when last measured (another PolicyEngine project's preview function on 2026-10-26), and the CA list compiled into that release includes "emSign ECC Root CA - G3". Vercel offers only 24.x and 22.x, so no project setting changes this. The production deployment is still serving (the backend behind it is stopped, Production backend stopped until auth issues #128, #130 and #131 are fixed #275, so the route currently returns Modal's 404).modal_app.py, apppolicyengine-uk-chatand thepe-uk-chat-<PR>previews):modal.Image.debian_slim(python_version="3.13")is built frompython:3.13.3-slim-bookworm, which carries Debianca-certificates 20230311(includes both roots), and no step upgrades it.backend/requirements.txtdoes not pin certifi, so the image has whatever certifi was current when Modal last built the requirements step. The last production deploy was 2026-09-02 (run 33631583740), before certifi 2026.10.19, the first release without the emSign root.policyengine-observability[google]writes logs to Google Cloud Logging throughgoogle-cloud-logging, which uses gRPC. gRPC verifies against the CA list bundled in grpcio, which includes both roots in every release so far. Previews use this destination by default; production currently setsOBSERVABILITY_LOG_DESTINATIONS=stdout.modal_eval_app.py) sends the eval requests with httpx, which uses certifi, again unpinned inbackend/requirements-modal-eval.txt.Not affected or not changed:
stripe16.1.0 bundles its own 12-certificate CA list; neither root is in it (checked by SHA-256 fingerprint).OTEL_EXPORTER_OTLP_ENDPOINTis set, andsync-modal-secret.shdoes not set it.backend/Dockerfile,frontend/Dockerfileanddocker-compose.ymlare local development only.Also changed: the frontend CI jobs (
tests.ymlanddeploy.yml) ran Node.js 20.19, which gets no release without the emSign root. Run under Node.js 20.19.6 and npm 10.8.2 (the versions CI installed),npm ci --loglevel httpfetched 386 package tarballs, which the lockfile's integrity hashes check, and also sent requests whose responses no hash covers: the audit requestPOST https://registry.npmjs.org/-/npm/v1/security/advisories/bulk(CI's log shows it: "audited 383 packages … 11 vulnerabilities") and package-metadata requests for 16 packages the audit reports on (for examplenext,vite,ws,undici), plus npm's own update check, which npm skips whenCIis set.strace -e trace=connectrecorded no network connections at all fromnpm run test:coverageandnpm run build(a controlfetchunder the same tracing recorded its port-443 and DNS connections); the Google Fonts stylesheet inlayout.tsxis a<link>the browser loads.Changes
frontend/src/utils/compromisedCaRoots.ts(new):removeCompromisedEmudhraRoots()removes from Node's default CA list every certificate whose SHA-256 fingerprint (X509Certificate#fingerprint256) is one of:86:A1:EC:BA:08:9C:4A:8D:3B:BE:27:34:C6:12:BA:34:1D:81:3E:04:3C:F9:E8:A8:62:CD:5C:57:A3:6B:BE:6BD7:A7:A0:FB:5D:7E:27:31:D7:71:E9:48:4E:BC:DE:F7:1D:5F:0C:3E:0A:29:48:78:2B:C8:3E:E0:EA:69:9E:F4, removed only as a defensive measure. Node's compiled-in list does not contain it (Mozilla trusts it for email only); it can reach the default list only throughNODE_EXTRA_CA_CERTSor--use-system-ca.It uses
tls.getCACertificates("default")andtls.setDefaultCACertificates. On each server start it logs, withconsole.info, how many certificates it removed, the list size before and after, andprocess.version. It logsconsole.warnwhen either Node.js function is missing. An error is logged withconsole.error, not rethrown, because an error from instrumentation'sregister()makes every function-served route fail. A comment says to delete the module once Vercel Functions run Node.js 24.22.1 / 22.23.4 or later. This follows the reviewed design in Stop trusting the compromised eMudhra CA certificates uk-cgt-reform#19.frontend/src/utils/compromisedCaRoots.test.ts(new): Vitest cases (Node environment) with a stubtlsand logger: removing G3 while keeping an unrelated root, removing AAA and repeated copies, a list with neither root, the early return for each missing function, and errors from reading the list, parsing a certificate and setting the list.frontend/src/instrumentation.ts(new):register()imports the module dynamically and calls it only whenprocess.env.NEXT_RUNTIME === "nodejs". Next.js 15.5.14 runs instrumentation without a configuration flag.@types/node22.19.15 already declares both TLS functions.modal_app.py: afterpip_install_from_requirements, the image now runs.apt_install("ca-certificates")(Modal runsapt-get updatethenapt-get install -y ca-certificates),.pip_install("certifi>=2026.10.19"), and.env({"GRPC_DEFAULT_SSL_ROOTS_FILE_PATH": "/etc/ssl/certs/ca-certificates.crt"}), so gRPC reads the refreshed Debian bundle instead of grpcio's. They go after the requirements step so Modal can reuse that step's cached build instead of re-resolving every unpinned package. certifi is a minimum, not a pin, so a rebuild picks up later removals.modal_eval_app.py:.pip_install("certifi>=2026.10.19")after its requirements step..github/workflows/tests.ymland.github/workflows/deploy.yml: the frontend job'ssetup-nodenode-version"20.19" → "24", the release series the Vercel project runs (24.22.2 today, which has the fixed CA list).backend/tests/test_modal_app.py: the image-step assertions for both apps include the new steps and their arguments; the fake image gainsenv.Checks
npx vitest run src/utils/compromisedCaRoots.test.ts: 8 passed.npx tsc --noEmit -p .infrontend/: no errors.next buildwith Node.js 24.21.0: succeeded.node .next/standalone/server.js) on Node.js 24.21.0: at startup it logs[compromisedCaRoots] Removed 1 certificate(s) of the compromised roots (emSign ECC Root CA - G3) from Node's default CA list (121 -> 120 certificates) on Node.js v24.21.0. WithPOLICYENGINE_UK_CHAT_BACKEND_URLset to each host,GET /uk/chat/api/proxy/healthgave:https://dv-g3b-emudhra.we-have-a-problem.comandhttps://dv-g3b-aaa-emudhra.we-have-a-problem.com: HTTP 500fetch failed(the request to the test host failed);https://pypi.organdhttps://policyengine--policyengine-uk-chat-web.modal.run: the upstream 404 passed through, so TLS to other hosts still works.fetchon Node.js 24.21.0, importing the module directly: without it, the emSign test host answered HTTP 200 and the AAA host failed (UNABLE_TO_GET_ISSUER_CERT_LOCALLY); with it, both failed withUNABLE_TO_GET_ISSUER_CERT_LOCALLYandhttps://pypi.org/answered HTTP 200.python:3.13.3-slim-bookworm, starting from certifi 2026.7.22 andca-certificates 20230311:apt-get update && apt-get install -y ca-certificatesandpip install 'certifi>=2026.10.19'(ca-certificates 20250419~deb12u2, certifi 2026.10.23): both test hosts failed withCERTIFICATE_VERIFY_FAILEDthrough both CA lists, andhttps://pypi.org/answered HTTP 200.GRPC_DEFAULT_SSL_ROOTS_FILE_PATHwas verified for the Public API in Stop trusting the compromised eMudhra CA certificates policyengine-api#3999; it was not re-tested here.pytest backend/tests/test_modal_app.py::test_modal_deployment_definition_imports_without_remote_calls backend/tests/test_modal_app.py::test_modal_eval_app_fans_out_twenty_cases_with_a_25_container_cap: 2 passed.modal_appandmodal_eval_appwith modal 1.6.3 constructs both images.node:24-bookworm, Node.js 24.22.2, npm 11.19.1,CI=true):npm cisucceeded (npm 11 skips sharp's install script by default and reports it; the build does not need it, since sharp's binaries come from the locked@img/sharp-*packages),npx tsc --noEmit -p .reported no errors,npx vitest run src/utils/compromisedCaRoots.test.tspassed 8 tests, andnpm run buildsucceeded. The frontend has no lint script.Operator actions after merge
mainto production as usual; nothing else is needed for the frontend. After the deploy, the function logs should show the[compromisedCaRoots] Removed 1 certificate(s)message with the running Node.js version.ca-certificates20250419~deb12u2 or later, certifi 2026.10.19 or later). The same applies to PR previews oncepr-beta-deploy.ymldeploys them again.modal run modal_eval_app.pybuilds the certifi step automatically.Environment-variable verification
This PR changes Modal image definitions, so every variable the changed files read or declare was traced:
GRPC_DEFAULT_SSL_ROOTS_FILE_PATH(new)modal_app.py.env(); read by gRPC in the backendsync-modal-secret.shputs no variable of that name in the Modal secret, so nothing overrides itNEXT_RUNTIME(new reader)frontend/src/instrumentation.tsPOLICYENGINE_UK_CHAT_MODAL_APP_NAME,POLICYENGINE_UK_CHAT_MODAL_SECRET_NAME(unchanged)modal_app.pyat deploy timemodal deploydeploy.ymlsets neither, so the defaults apply;deploy-modal-preview.shsets both from the workflow-levelMODAL_PREVIEW_*valuesALEMBIC_DATABASE_URL(unchanged)modal_app.pyfunctionsPOLICYENGINE_UK_CHAT_DATABASE_URL(repository and organization level)sync-modal-secret.shstepenv:→modal secret create; attached withsecrets=[chat_secrets]POLICYENGINE_UK_CHAT_EVAL_MODAL_APP_NAME,..._SECRET_NAME,..._VOLUME_NAME(unchanged)modal_eval_app.pymodal runUK_CHAT_EVAL_TOKEN(unchanged)modal_eval_app.pypolicyengine-uk-chat-secrets, filled from the GitHub Actions repository secretUK_CHAT_EVAL_TOKENsync-modal-secret.shMODAL_TOKEN_ID,MODAL_TOKEN_SECRET(unchanged)modal deployindeploy.ymlandpr-beta-deploy.ymlenv:fromsecrets.*POLICYENGINE_UK_CHAT_BACKEND_URL,VERCEL_ENV,VERCEL_GIT_PULL_REQUEST_ID(unchanged; on the path this protects)frontend/src/app/api/proxy/backend-url.tsNODE_EXTRA_CA_CERTS,NODE_USE_SYSTEM_CA,NODE_OPTIONS(not read; they would change the list the module filters)🤖 Generated with Claude Code