Skip to content

Discover and patch Java certificate blocklists with shared Python script - #117

Merged
roryabraham merged 27 commits into
mainfrom
rory/shared-java-certificate-blocklist
Oct 22, 2026
Merged

roryabraham merged 27 commits into
mainfrom
rory/shared-java-certificate-blocklist

Conversation

@roryabraham

@roryabraham roryabraham commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Details

This eMudhra certificate-blocklist mitigation is intended to be used across CI and developer laptops. It's one, standard-library-only Python 3.9+ script.

Add scripts/blockCompromisedJavaCertificates.py and a thin Expensify/GitHub-Actions/block-compromised-java-certificates composite action:

  • Use the system Python 3.9+ interpreter and standard library. The composite protects the JVM selected by actions/setup-java using JAVA_HOME or its java-home input.
  • Add --all discovery across configured/PATH runtimes, the macOS registry, standard JDK directories, Homebrew, version managers, IDE bundles, Gradle JDKs, and runner caches. Allow custom locations with --search-root.
  • Default to dry-run; require --apply to write. Support JSON inventory/results, deduplicate runtime aliases and resolved blocklist paths, and continue processing other installations while returning failure for any search or patch errors.
  • Derive all 16 SHA-256 certificate fingerprints from one committed PEM bundle. Preserve vendor contents, algorithm, ownership, permissions, and symlinks; append only missing entries and verify afterward.
  • Discover as the normal user; elevate only individual file updates. CI uses noninteractive sudo; laptop apply mode can prompt. Never elevate discovered Java executables.
  • Add Python unit tests and an 11-combination vendor/OS matrix checking actual JVM rejection, an unaffected-root control, and idempotence.

Supports Linux/macOS JVMs with OpenJDK-style blocklists. Discovery is bounded and reports its scope; custom paths, inaccessible directories, other users, containers/VMs, and future downloaded toolchains may require separate runs. Restart existing JVMs/IDEs/Gradle daemons after patching and rerun after installations/updates. This PR does not migrate consumers, patch employee installations, or deploy anything.

Rollout plan

My intention is that this script and action can be used:

  • On employee workstations via auditbot
  • In the Expensidev VM during its provisioning
  • Across the org in GitHub Workflows that use actions/setup-java and/or actions/setup-gradle via the composite action (hence why the script lives in this repo)

Related Issues

Remediation plan: https://expensify.slack.com/docs/T03SC9DTT/F0CBWHPE856

Manual Tests

  • macOS laptop: Ran --all --dry-run, then --all --apply across all 5 discovered JVMs, adding 16 blocklist entries to each.
  • Expensidev VM: Repeated the dry-run/apply flow on Java 8 and Java 21, successfully patching both.
  • Verification: Follow-up dry-runs reported zero missing entries and no errors. Fresh JVMs in both environments rejected all 16 compromised certificates and passed the unaffected-root control. Existing IDEs, daemons, and GlassFish were not restarted.

Linked PRs

None.

Centralize the eMudhra mitigation after Java setup so repositories can
protect their actual JVM without relying on vendor patch availability.
Run dependency-free TypeScript with Bun, preserve vendor blocklists,
and fail closed when protection cannot be verified.

Add offline certificate fixtures, unit tests, and a cross-vendor/OS JVM
verification matrix. Allow local root-level actions in the immutable-ref
check so the new composite action can test itself.
Apply the approved Crit review: derive fingerprints from one PEM bundle,
use Bun file and shell APIs, and keep support scoped to Linux and macOS.
Guard the entrypoint on import, trim documentation, and leave the shared
immutable-reference validator unchanged.

Retain actual JVM rejection tests and add a separate Bun-native unit job.
Share one dependency-free patcher between developer machines and CI.
Inventory known JVM, IDE, version-manager, Gradle, and runner-cache
locations, with explicit extra roots and dry-run/apply modes.

Preserve vendor blocklists, deduplicate shared runtimes and files, and
report partial failures without claiming complete filesystem coverage.
Discover as the user and elevate only file updates. Replace Bun setup
with a thin Python composite step and add stdlib compatibility tests.
@roryabraham roryabraham changed the title Add shared Bun/TypeScript action to block compromised Java certificates Discover and patch Java certificate blocklists with shared Python script Oct 21, 2026
Add function and class docstrings and follow the Python style guide's
single quotes, multiline layout, argument conventions, and line limit.
Keep the patching and discovery behavior unchanged.
Put opening and closing triple quotes on separate lines and leave a
blank line after each docstring, following the explicit style-guide
examples rather than conventional Python docstring formatting.
Separate completed conditionals and loops from subsequent operations
while keeping related branches and exception handlers together.
Whitespace only; the Python AST is unchanged.
@roryabraham

Copy link
Copy Markdown
Contributor Author

@codex review

@roryabraham

Copy link
Copy Markdown
Contributor Author

@MelvinBot review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-10-22T08:07:48.117778Z 2b9d333 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6b4c4a806a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +500 to +501
if os.geteuid() == 0:
parser.error('Run as your normal user, not with sudo; only individual file updates will be elevated.')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow the action to run on root-based runners

When the composite action runs in a Linux job container or self-hosted runner whose job user is UID 0, this unconditional check exits before inspecting the explicitly selected JAVA_HOME. Because action.yml always invokes this entry point, such jobs cannot use the action at all, even though selected-home mode performs no broad discovery and already has permission to update the file; permit this mode as root while retaining the restriction for --all if executing discovered JVMs is the concern.

Useful? React with 👍 / 👎.

@roryabraham roryabraham Oct 21, 2026 •

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.

I don't think this will be a real issue. Though I don't know if we really need to error if it's run as sudo 🤷🏼

@roryabraham
roryabraham marked this pull request as ready for review October 21, 2026 23:49
francoisl
francoisl previously approved these changes Oct 22, 2026
Comment on lines +49 to +51
def certificateFingerprints(bundle: str) -> list[str]:
"""
Derive uppercase SHA-256 certificate fingerprints from the reviewed PEM bundle.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we save the precomputed fingerprints instead in compromised-java-certificates.pem, then skip this step? NAB

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.

The Java piece of the test that ensures that the JVM actually rejects the invalid certificates needs the whole .pem file, so saving the precomputed fingerprints separately would create a second source of truth.

Use literal property separators and Java major-version prefix removal
instead of regexes. Keep required-field validation and significant path
whitespace, with regression tests for malformed versions and paths.
Resolve targets into a path set before any writes, without inode maps
or a special shared-file status. Keep runtime inventory separate from
per-file update results so shared paths do not inflate entry counts.
Remove locking, the second pre-write read, inode guards, and metadata
assertions. Append missing entries normally and keep final fingerprint
verification, with a regression test for missing post-write entries.
Replace privileged Python re-execution and its private CLI mode with
sudo tee -a on permission-denied appends. Feed only missing entries on
stdin, suppress tee output, and verify file contents afterward.

Keep dry-runs and reads unprivileged. Cover prompt policy, failed sudo,
and successful commands that fail to persist the expected entries.
Derive fingerprints from the trusted PEM bundle without a fixed count
or ad hoc DER checks. Keep an empty-policy guard and validate the
checked-in data in tests. Make test counts follow the bundle size.
Always append through sudo -n after a permission-denied write. Remove
the interactive-mode parameter and CLI flag, and update the action,
workflow, documentation, and tests to match.
Build the ASCII additions with an f-string and use explicit ASCII text
I/O for normal and sudo appends. Keep original vendor-file reads binary
so existing bytes and line endings are not reinterpreted or rewritten.
Return immutable JavaRuntime and BlocklistUpdate dataclasses instead
of anonymous dictionaries. Type successful update statuses with Literal
while keeping JSON values plain strings and avoiding an enum wrapper.
Move stdlib tests to tests/python and give them a dedicated workflow.
Rename the certificate integration workflow and keep its path filters
focused on the action, implementation, and actual-JVM test fixture.
Linux/macOS runners already default to bash. Document why the local
action test checkout lives under .github without changing the shared
action-reference validator.
Allow sudo to request authorization in interactive non-CI sessions,
preceded by a clear permission message on stderr. Keep sudo -n for CI
and headless execution, preserving clean JSON output and write-only
elevation. Update the prompt-policy tests and usage documentation.
@roryabraham
roryabraham requested a review from francoisl October 22, 2026 05:52
@roryabraham

Copy link
Copy Markdown
Contributor Author

pushed a bunch of simplifications

Handle missing, inaccessible, and non-directory roots inside the walk
instead of duplicating the root check before entering it. Preserve
optional-root reporting and add root-validation regression coverage.
Use canonical directory paths instead of device/inode tuples to avoid
revisiting aliases and symlink cycles. Reuse resolved runtime homes and
report resolution failures on both older and newer Python versions.
Accept an iterable in run and pass Discovery.homes directly. Keep the
single candidate deduplication/sort in run, with coverage for sets and
one-pass iterators.
Move JSON and text rendering out of main so CLI orchestration and exit
status handling remain separate from presentation. Cover output streams,
report contents, and apply-mode reminders without launching Java.
Replace string-valued root kinds with an isHomebrew boolean. Preserve
the top-level Java-package filter and cover default/custom prefixes,
nested Java layouts, and ordinary unfiltered roots.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6166330657

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

SearchRoot(Path(environment.get('JENV_ROOT', str(home / '.jenv'))) / 'versions'),
SearchRoot(Path(environment.get('GRADLE_USER_HOME', str(home / '.gradle'))) / 'jdks'),
]
if platform == 'darwin':

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Search configured Homebrew prefixes on Linux

When --all runs on a Linux developer machine using Linuxbrew, this platform guard prevents HOMEBREW_PREFIX/opt from ever being searched. Keg-only JDKs such as Homebrew OpenJDK may not appear in PATH, JAVA_HOME, or /usr/lib/jvm, so they are silently omitted despite the documented Homebrew discovery coverage; add the configured Homebrew prefix on Linux as well.

Useful? React with 👍 / 👎.

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.

Nathan is 1/2 developers with a linux workstation. Do you use Linuxbrew?

Drop refactor-specific spies and redundant synthetic version checks.
Consolidate coverage around file safety, discovery, sudo behavior, CLI
outcomes, and partial failures. Keep real-JVM verification unchanged.
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.

2 participants