Skip to content

PER-17672, PER-17729: fix: backport the eMudhra CA distrust and PyJWT/GitPython floors to 0.9.x (#974, #976) - #978

Merged
EliMoshkovich merged 4 commits into
release/0.9.xfrom
eli/per-17672-per-17729-backport-0.9.x
Oct 22, 2026
Merged

EliMoshkovich merged 4 commits into
release/0.9.xfrom
eli/per-17672-per-17729-backport-0.9.x

Conversation

@EliMoshkovich

@EliMoshkovich EliMoshkovich commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Fixes Issue

Backports #974 and #976 to the 0.9 line (pydantic v1), so that 0.9.10 carries them. The fixes are on master (the 0.10 line) since today, but 0.9.9 users and the PDP need a 0.9.x release.

Changes proposed

Two git cherry-pick -x of the squash commits from master, in merge order. Both apply cleanly to release/0.9.x: 73f989fe (#974) and 62d8887d (#976). One follow-up commit, b2c7b7a3, writes the trust-store test's str | None / bytes | None annotations as Optional[...], because release/0.9.x still tests Python 3.9 (master no longer does). A second, f59a8f71, adds packages/**/tests/*.pem to .dockerignore, so the trust-store test's certificate fixture is left out of the build context of path-context builds such as make (the same change as #979 on master). There are no other changes; the two PRs' descriptions cover the details.

Verification

Run locally on this branch's exact tree, with the published 0.9.9 images as the baseline:

0.9.9 this branch
Unit tests, Python 3.9, 3.10 and 3.12 696 passed each; pip check clean
#976's PyJWT guard test on 2.13.0 / 2.14.0 / 2.15.0 / 2.15.1 fails / fails / fails / passes, as intended
client, server, client-alpine, server-alpine with --no-cache all build; the trust-store check passes in each
Disclosure's 12 chains, offline (certifi, system store) and live (stdlib ssl, requests, httpx, aiohttp, pygit2, git, OPA) 6–12 trusted per client 0
Trivy findings on PyJWT, GitPython, certifi, ca-certificates 17–18 per image 0
Server + client with JWT auth: policy over HTTPS, data update, decision pass, incl. a 0.9.9 client against this server and the reverse
A non-canonical JWT signature (GHSA-hxm8-2xgr-2p9m) accepted (HTTP 200, websocket 101) rejected (401 / 403)
A PDP (pdp-v2:0.9.16) with this branch's opal-common/opal-client installed starts, syncs policy and data, live updates arrive, every decision matches the stock PDP

Check List (Check all the applicable boxes)

  • I sign off on contributing this submission to open-source
  • My code follows the code style of this project.
  • My change requires changes to the documentation.
  • I have updated the documentation accordingly.
  • All new and existing tests passed.
  • This PR does not contain plagiarized content.
  • The title of my pull request is a short description of the requested changes.

Description updated during review to match f59a8f7 (the Python 3.9 test fix and .dockerignore commits).

🤖 Generated with Claude Code

EliMoshkovich and others added 2 commits October 22, 2026 14:07
…ER-17672) (#974)

* fix(docker): distrust the compromised eMudhra CAs (CVE-2026-111943)

The private keys of eight eMudhra intermediate CAs are recoverable from
their certificates, so anyone can mint a certificate for any host that a
client trusting their roots accepts. The published opal-client and
opal-server images still trust both roots (emSign ECC Root CA - G3 and,
through a Sectigo cross-sign, AAA Certificate Services) in the system
store and the first in certifi's bundle.

- opal-common floors certifi at 2026.10.19, which drops emSign ECC Root CA - G3.
- Both common stages upgrade ca-certificates (Debian 20250419~deb12u2 from
  bookworm-security, Alpine 20261020-r0); nothing upgraded it before.
- scripts/check_trust_store.py, bind-mounted into those layers, fails the
  build if any trust store still holds either root.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* review: harden check_trust_store.py and test it

- Deny all four roots the compromised certificates anchor at (roots.json on
  the disclosure site), adding emSign Root TLS CA - G3 and emSign Root CS
  CA - G3, which are only in Microsoft's store today.
- A missing certificate directory is skipped instead of falling back to
  Path("") and scanning the working directory.
- OpenSSL's TRUSTED CERTIFICATE blocks are parsed too.
- Paths are resolved and each file read once.
- docker_trust_store_check_test.py feeds it stores that do hold the roots
  (the image's never do) and checks each fingerprint against the roots'
  certificates.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* review: match compromised keys; no BuildKit requirement; check server stages

- Deny by SubjectPublicKeyInfo SHA-256 (roots.json and
  impacted-intermediates.json), including the eight intermediates' keys, so
  a re-issued root or cross-sign for a denied key is caught. The key comes
  from a walk over the TBSCertificate fields; the tests check it against
  cryptography on every certifi root.
- Read the legacy X509 CERTIFICATE label; an unparsable block fails the
  build naming its file.
- COPY the script instead of RUN --mount, which made BuildKit a hard
  requirement for every target. It stays at /opal/check_trust_store.py, so
  a built image can be re-checked.
- Run it after the server stages' own package installs too.
- The docstring no longer claims to catch a cached layer: a cached layer
  was checked when it was built.
- The test skips where the script is not next to the package.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* review: same gate hardening as the PDP copy (Zeev on permitio/PDP#415)

- main() returns the gate's status; a subprocess test runs the script with
  SSL_CERT_FILE/SSL_CERT_DIR stores and checks 1 compromised / 0 clean.
- Each failing store gets its fix (the certifi floor in opal-common, or a
  rebuild of the ca-certificates layer); a parse error names its block.
- _element rejects BER indefinite and oversized lengths; spki() checks the
  walk ends on a SEQUENCE.
- Tests for v1 certificates, no default CA file and the directory walk;
  docstrings for what find_distrusted() returns and when it exits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* review: strict DER and SPKI shape check, as in the PDP copy (Zeev, permitio/PDP#415 round 2)

- _element rejects high-tag-number tags, leading zero length octets and
  the long form for a short length; spki() checks SubjectPublicKeyInfo's
  shape, so a crafted or misaligned certificate fails instead of hashing
  other bytes.
- Tests for each non-DER encoding, each length rule on its own and a walk
  ending on the wrong field.
- SYSTEM_FIX points at what can still trip the check (a package or file
  still carrying a key), not at the upgrade the Dockerfile already does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: pin the primitive high-tag form and each SPKI condition (parity with permitio/PDP#415)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* review (Zeev): declare the ca-certificates floor; accurate fix text and readers; sturdier tests

- Alpine: `apk add 'ca-certificates>=20261020-r0' 'ca-certificates-bundle>=…'`
  instead of `apk upgrade`, so a stale index fails at the package step with
  apk's own error and /etc/apk/world keeps the floor. Debian: the installed
  version is checked right after `apt-get install` (apt has no `>=`), naming
  bookworm-security as the likely cause.
- SYSTEM_FIX no longer asserts a premise the build didn't check, and points
  at /usr/share/ca-certificates and /etc/ca-certificates.conf for the
  generated bundle, which `dpkg -S` can't attribute.
- The docstring names the real system-store readers: pygit2 via the stdlib's
  default paths, git's HTTPS through libcurl (GnuTLS on Debian), OPA/EOPA.
- Tests: _key_hash returns None for a root cryptography refuses (six certifi
  roots have non-positive serials) and ignores only that deprecation
  warning; a moved script fails collection in a source checkout instead of
  skipping all 68 tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* review (Zeev): the fix text names /usr/local/share/ca-certificates too

update-ca-certificates also adds every certificate there, and that is where
a later step or a derived image normally adds a CA.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 73f989f)
… published advisories (#976)

* fix(deps): raise the PyJWT and GitPython floors past their published advisories

PyJWT 2.14.0 and earlier have 14 advisories published 2026-09-29..10-05
(one critical, GHSA-ffc3-869f-jxw9); the last are fixed in 2.15.0, and
2.15.1 decodes '='-padded segments again. GitPython 3.1.61 and earlier
have GHSA-59cr-6r3x-644w, fixed in 3.1.62. The old floors (pyjwt 2.1/2.4,
gitpython 3.1.32) let any install resolve an affected version.

The git-leak test seed image pinned GitPython 3.1.50 and requests 2.32.3,
both with known advisories; bump them to the current releases.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* review: pin the PyJWT decode behaviours behind the floor; accurate floor comment

The 2.15.1 floor changes what JWTVerifier accepts: a non-canonical
signature segment is now rejected (GHSA-hxm8-2xgr-2p9m, since 2.14.0)
and a '='-padded one is accepted (2.14.0 and 2.15.0 rejected it). The
new test fails on 2.13.0 and on 2.15.0, so lowering the floor or a
future PyJWT regression shows up in the suite.

The requires.txt comment said 2.14.0 had 14 advisories; it has two
(both medium), the other 12 including the critical one were fixed in
2.14.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* review (Zeev): GitPython floor 3.2.1; pin the rejection reason in the test

GitPython's repository advisories (not yet in the global GitHub database
or OSV) affect 3.1.62: GHSA-f9j4-qggq-h239 (critical) and five more are
fixed in 3.2.0, and GHSA-83vg-56qc-22m7 (high, CVE-2026-114145) affects
<= 3.2.0 and is fixed in 3.2.1.

JWTVerifier.verify maps every failure to Unauthorized, so the
non-canonical test now asserts the "Could not decode access token"
detail, and asserts that the tampered segment decodes to the same
signature bytes instead of the vacuous tampered != token.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 62d8887)
@EliMoshkovich
EliMoshkovich requested review from zeevmoney and a balanced review from Copilot October 22, 2026 19:07
@linear-code

linear-code Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

PER-17729

PER-17672

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

release/0.9.x still tests Python 3.9, which master no longer does, and the
test backported from #974 used PEP 604 unions (str | None) in annotations
that 3.9 evaluates at import. typing.Optional instead, as elsewhere in
opal-common.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 19:23

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@zeevmoney zeevmoney changed the title fix: backport the eMudhra CA distrust and PyJWT/GitPython floors to 0.9.x (#974, #976) PER-17672, PER-17729: fix: backport the eMudhra CA distrust and PyJWT/GitPython floors to 0.9.x (#974, #976) Oct 22, 2026

@zeevmoney zeevmoney left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approved — no CRITICAL or HIGH issues found.

Non-blocking:

  • LOW packages/opal-common/opal_common/tests/emudhra_compromised_cas.pem:1 — the compromised-certificates fixture is copied into every client and server image

Details are in the inline comments on each line.

Comment thread packages/opal-common/opal_common/tests/emudhra_compromised_cas.pem
`COPY ./packages` put the trust-store test's fixture, which holds the
compromised eMudhra certificates, in every client and server image, where an
image scanner looking for those certificates finds it. Nothing loads it as a
trust anchor, and the wheels never had it. The fixture stays in git for the
test; only the Docker build context drops it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 19:36

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@zeevmoney zeevmoney left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approved — no CRITICAL or HIGH issues found.

Still open from the previous review:

  • LOW packages/opal-common/opal_common/tests/emudhra_compromised_cas.pem:1 — the fixture still ships in the images CI and the release job build: they use the Git build context, which ignores .dockerignore

Details are in the review thread on that line.

Comment thread packages/opal-common/opal_common/tests/emudhra_compromised_cas.pem
@EliMoshkovich
EliMoshkovich merged commit 3667977 into release/0.9.x Oct 22, 2026
12 of 13 checks passed
@EliMoshkovich
EliMoshkovich deleted the eli/per-17672-per-17729-backport-0.9.x branch October 22, 2026 20:19
EliMoshkovich added a commit that referenced this pull request Oct 23, 2026
…to 0.9.x) (#980)

Backport of #979. The trust-store test's fixture holds the compromised eMudhra certificates (CVE-2026-111943). #978's .dockerignore rule doesn't apply to the Git context CI and the release build from, so docker/Dockerfile now uses COPY --exclude=**/tests/*.pem (dockerfile frontend 1.19, pinned by digest), .dockerignore keeps the rule for local builds, and both E2E jobs fail if any .pem is under /opal/packages in the images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

3 participants