Skip to content

[improve][build] Upgrade BouncyCastle to 1.85 and BouncyCastle FIPS to 2.1.x - #26349

Open
lhotari wants to merge 2 commits into
apache:masterfrom
lhotari:lh-improve-bouncycastle
Open

[improve][build] Upgrade BouncyCastle to 1.85 and BouncyCastle FIPS to 2.1.x#26349
lhotari wants to merge 2 commits into
apache:masterfrom
lhotari:lh-improve-bouncycastle

Conversation

@lhotari

@lhotari lhotari commented Aug 17, 2026

Copy link
Copy Markdown
Member

Motivation

BouncyCastle and BouncyCastle FIPS are both behind. This brings the non-FIPS provider stack to 1.85
and the FIPS stack to the 2.1.x line.

Modifications

gradle/libs.versions.toml:

library from to
bcpkix-jdk18on / bctls-jdk18on 1.84 1.85
bcprov-jdk18on 1.84 1.85.2
bc-fips 2.0.1 2.1.3
bcpkix-fips 2.0.11 2.1.12
bcutil-fips 2.0.6 2.1.7

Plus the corresponding jar names in the server and shell distribution LICENSE.bin.txt files, and a
comment in the version catalog recording the FIPS certificate mapping described below, so the next
person bumping these does not have to re-derive it.

A new bouncycastle-bcprov version reference is introduced. BouncyCastle published a
provider-only patch release, 1.85.2, that was not released for bcpkix-jdk18on or
bctls-jdk18on — those remain at 1.85 on Maven Central. bcprov-jdk18on previously shared the
single bouncycastle reference, which can no longer express this, so it now has its own.

FIPS: which jar is the validated module

Thanks @david-streamlio — you were right to push on this, and the original description overstated
things. Of your three outcomes, (b) holds: only 2.1.1 is validated. But the remedy does not
follow, and the reason is worth spelling out because it applies to the 2.0.x line just as much.

The cryptographic boundary is the jar file, by name. The
security policy for certificate 4943 (BC-FJA, Software Version 2.1.1, 2025-07-18) says so
in §2:

The Module is of type software and the module has a Multi-Chip Stand Alone embodiment; the
cryptographic boundary is the Java Archive (JAR) file, bc-fips-2.1.1.jar.

So there is no jar-version/module-version indirection to map — exactly one published jar per line is
the validated module, and it is named in the policy:

CMVP certificate Validated jar Status Sunset Tested Java
#4943 bc-fips-2.1.1.jar Active, caveat "Interim Validation" 2027-01-16 8, 11, 17, 21
#4743 bc-fips-2.0.0.jar Active 2029-07-28 8, 11, 17, 21

BouncyCastle's download page shows the same shape from the vendor side, listing two provider
jars per line — bc-fips-2.1.0.jar under "Provider" and bc-fips-2.1.3.jar under
"Provider (patched)":

The 2.1.0 release, BC-FJA 2.1.0 (Certificate #4943), is certified for use on Java 8, Java 11,
Java 17, and Java 21. The 2.1.3 release is a patch release on 2.1.0/2.1.1/2.1.3, going into
submission
.

The roadmap adds that 2.1.1 is a "Bug fix release for 2.1.0 (GC related issues)",
"Superseded by 2.1.2", and 2.1.2 is "Under submission". (BouncyCastle's pages still name 2.1.0 as
the certified jar; CMVP rolled #4943 forward to 2.1.1 in its 2025-11-14 update.) So 2.1.3's status
is "pending submission" — not certified, not rejected.

Why this PR keeps 2.1.3

Because BouncyCastle ships security fixes only in the patch releases and certifies them afterwards,
no certified bc-fips jar is free of known CVEs — on either line:

bc-fips Certified? Known CVEs
2.0.0 #4743 11 — incl. CVE-2026-8763, CVE-2026-58062 (both CVSS 9.3)
2.0.1 — the pin this PR replaces 10 — incl. the same two 9.3s
2.0.2 0
2.1.1 #4943 13 — incl. the same two 9.3s
2.1.2 12
2.1.3 — this PR ❌ pending submission 0

The two criticals are certificate-validation bypasses: CVE-2026-8763 (Name Constraints bypass via a
trailing dot in rfc822Name/URI) and CVE-2026-58062 (stapled OCSP response accepted without binding
to the checked certificate). Pinning 2.1.1 to match the certificate would knowingly ship both.

The key point for this PR: Pulsar's existing bc-fips 2.0.1 pin is not a certified version
either
— certificate #4743 names 2.0.0 only. So 2.1.3 is no worse than the status quo from a
certification perspective
, and strictly better on CVEs, which is why the upgrade to the 2.1.x line
is the right move rather than a downgrade to a certified-but-vulnerable jar. The same reasoning
rules out the 2.0.x alternative: 2.0.2 would also be uncertified.

The other two bumps are the vendor's minimum fixed versions as well, and neither is inside the
certified boundary — the security policy states "This module is the only software component within
the Cryptographic Boundary" — so they carry no certification consequence:

The two secondary points

Certificate horizon (your point 2) — confirmed: #4943 is an interim validation sunsetting
2027-01-16, ~2.5 years earlier than #4743's 2029-07-28. What takes the sting out of it here is that
bc-fips is test-only in this build: it is declared once, as testImplementation in
tests/pulsar-client-test-bcfips; the server distribution explicitly excludes it and ships the
non-FIPS provider instead; it appears in no LICENSE.bin.txt, no published POM, and not in
pulsar-bom. Resolving :distribution:pulsar-server-distribution yields only
bcprov-jdk18on:1.85.2, bcpkix-jdk18on:1.85 and bcutil-jdk18on:1.85 — no bc-fips at any
version. A FIPS deployment assembles its own provider classpath today (as PIP-478 notes), so this
pin does not commit any operator to the shorter horizon; it determines what Pulsar's FIPS test module
exercises. That also means the "swaps out the certified cryptographic module underneath anyone
running Pulsar in FIPS mode"
framing in the original description was wrong on both halves, and it
has been removed.

Tested Java ceiling (your point 3) — confirmed, and the security policy is more specific than the
certificate page. Beyond the tested environments (Table 2) it lists 78 Vendor Affirmed environments
(Table 3), and every one tops out at Java SE Runtime Environment v21; Java 25 is on neither list. The
policy's porting terms:

The cryptographic module will remain compliant with the FIPS 140-3 validation when operating on any
general purpose computer (GPC) provided that: 1) No source code has been modified. 2) The GPC uses
the specified single-user platform, or another compatible single-user platform such as one of the
Java SE Runtime Environments listed on any of the following: […]

For the avoidance of doubt, it is hereby stated that the CMVP makes no statement as to the correct
operation of the module or the security strengths of the generated keys when so ported if the
specific operational environment is not listed on the validation certificate.

Whether vendor affirmation suffices is the operator's compliance programme's call. For this
repository it affects only which JDK pulsar-client-test-bcfips happens to run on in CI.

On 1.85 release notes

BouncyCastle publishes no formal release notes for 1.85 (see bcgit/bc-java#2355). Based on the
project's release announcement, 1.85 is feature-additive over 1.84: post-quantum signature
algorithms from recent NIST and Korean standardisation work, BLS12-381 signatures, BIP-340 Taproot
Schnorr signatures, and hybrid X.509 certificate support. No API removals are announced.

Class file versions were checked in the published jars: all artifacts keep Java 8 roots with
multi-release overlays, so the Java 17 baseline is unaffected.

Verifying this change

  • Make sure that the change passes the CI checks.

This change is already covered by existing tests, in particular the tests/pulsar-client-test-bcfips
module, which exercises the FIPS provider end to end.

Verified locally with:

  • ./gradlew sanityCheck and ./gradlew checkBinaryLicense
  • ./gradlew :tests:pulsar-client-test-bcfips:test — passes against bc-fips 2.1.3 / bcpkix-fips
    2.1.12 / bcutil-fips 2.1.7
  • ./gradlew :distribution:pulsar-server-distribution:dependencies and
    :tests:pulsar-client-test-bcfips:dependencies — confirm the resolved BouncyCastle sets quoted
    above

Does this pull request potentially affect one of the following parts:

  • Dependencies (add or upgrade a dependency)
  • The public API
  • The schema
  • The default values of configurations
  • The threading model
  • The binary protocol
  • The REST endpoints
  • The admin CLI options
  • The metrics
  • Anything that affects deployment

The non-FIPS BouncyCastle jars bundled in the server and shell distributions change version.
bc-fips is in no distribution, so the FIPS bump affects only the in-tree FIPS test module.

…o 2.1.x

Non-FIPS:
- bcpkix-jdk18on / bctls-jdk18on  1.84 -> 1.85
- bcprov-jdk18on                  1.84 -> 1.85.2

bcprov now uses its own 'bouncycastle-bcprov' version reference. BouncyCastle published a
provider-only patch release 1.85.2 that was not released for bcpkix-jdk18on or
bctls-jdk18on, so a single shared version reference can no longer cover all three.

FIPS:
- bc-fips        2.0.1  -> 2.1.3
- bcpkix-fips    2.0.11 -> 2.1.12
- bcutil-fips    2.0.6  -> 2.1.7

Note for operators running Pulsar in FIPS mode: BC-FJA 2.1.x is a different FIPS 140-3
certified module than 2.0.x (Certificate apache#4943), certified for Java 8, 11, 17 and 21.
This upgrade therefore swaps the certified cryptographic module, which may require
re-validation depending on your compliance process.

BouncyCastle publishes no formal 1.85 release notes; 1.85 is feature-additive over 1.84
(post-quantum signature algorithms, BLS12-381, BIP-340 Taproot, hybrid X.509 certificates).

Assisted-by: Claude Code (Opus 5)
@lhotari lhotari added the area/dependency Pull requests that update a dependency file label Aug 17, 2026
@lhotari lhotari added this to the 5.0.0-M2 milestone Aug 17, 2026

@david-streamlio david-streamlio 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.

The mechanical half of this checks out completely — I verified the resolution and the license bookkeeping rather than reading past them. The compliance half has one thing I think is worth resolving before this merges, because the PR adds an explicit certification claim and the certificates do not quite say what it says.

Verified rather than taken on trust:

  • All seven coordinates resolve. bcprov-jdk18on:1.85.2, bcpkix-jdk18on:1.85, bctls-jdk18on:1.85, bcutil-jdk18on:1.85, bc-fips:2.1.3, bcpkix-fips:2.1.12, bcutil-fips:2.1.7 all return their POM from Maven Central.
  • The separate bouncycastle-bcprov reference is justified exactly as described. 1.85.2 is published for bcprov-jdk18on and for nothing else — bcpkix-jdk18on, bctls-jdk18on and bcutil-jdk18on all 404 at 1.85.2. The catalog could not express this with one reference, so the split is right rather than merely convenient.
  • The version skew is safe and the LICENSE entries match what actually resolves. Both bcpkix-jdk18on:1.85 and bctls-jdk18on:1.85 declare bcutil-jdk18on:1.85, and bcprov is a direct catalog entry forced to 1.85.2, so the assembled set is bcpkix 1.85 / bcutil 1.85 / bcprov 1.85.2 — precisely what both LICENSE.bin.txt files now list. Build and License check passing confirms that against the real distribution rather than against my reading.
  • Nothing is left behind. No 1.84 or 2.0.x BouncyCastle reference survives anywhere in the tree, and the only two files that name BC jars are the two updated here. bcprov-ext-jdk18on appears solely as a defensive exclude in the bcfips test module, so it needs no bump.
  • The FIPS suite did run. :tests:pulsar-client-test-bcfips:test compiled and executed inside CI - Unit - Other, which passed — it is not excluded from test_group_other in pulsar-build/run_unit_group_gradle.sh. So the end-to-end FIPS exercise happened against 2.1.3 in CI, not only locally.

1. The pinned bc-fips version is not the version on the certificate — before or after this PR.

I read both CMVP entries rather than going from the release notes:

Pinned in Pulsar Certificate Version(s) on the certificate
today bc-fips 2.0.1 #4743 2.0.0
this PR bc-fips 2.1.3 #4943 2.1.1

So this is not a regression — we already ship a patch release that is not the validated one, and I want to be clear I am not accusing this PR of introducing the problem. But it does newly add a FIPS: please note for operators section asserting that this "swaps out the certified cryptographic module," and that framing implies 2.1.3 is the certified module. An operator who reads that, upgrades, and then has to evidence their module against #4943 finds a version they are not running.

I could not resolve from BouncyCastle's own download page which jar corresponds to the validated module, so I am raising this as a question rather than asserting a defect. Three honest outcomes and I do not know which holds:

  • 2.1.3 is covered by #4943 via a non-security-relevant maintenance update. Then the description should cite it, because the certificate page alone does not show it.
  • Only 2.1.1 is validated. Then pinning 2.1.1 — on Maven Central today — would both land the upgrade and fix the pre-existing mismatch in one go.
  • Jar version and module version genuinely differ. Then that mapping is the single most useful sentence this PR could give an operator.

This is the same shape we have been chasing across PIP-478: a control that is shaped like compliance without being compliance. Getting BCFIPS onto the classpath is not the claim — running the validated module is. Since you are in here anyway and 2.1.1 is available, this looks cheap to settle.

2. This upgrade shortens the certificate horizon by about two and a half years.

Worth stating plainly because it cuts against the intuition that newer is safer:

  • #4743 (the 2.0.x line we are on) — Active, sunset 2029-07-28
  • #4943 (the 2.1.x line this moves to) — interim validation, sunset 2027-01-16

That is inside the plausible support life of a 5.0.x line, and it is the kind of thing an operator planning a FIPS deployment on Pulsar 5.0 needs told. It does not make the upgrade wrong — 2.1.x presumably carries fixes worth having, and BC will re-validate — but the tradeoff should be in the description rather than discovered later.

3. The tested Java ceiling is 21 on both certificates; our default CI JDK is 25.

#4943's tested configurations are Java SE 8, 11, 17 and 21 (Ubuntu 22.04, 11th-gen Intel), and #4743's are the same four. So the JDK ceiling is unchanged by this upgrade — flat, not improved.

pulsar-ci.yaml defaults jdk_major_version to 25, so the FIPS run I confirmed above executed outside the tested set. Not overstating it: FIPS 140-3 permits a software module ported to a different operational environment to remain validated under vendor affirmation, so Java 25 is not automatically disqualifying — but whether that suffices is the operator's compliance programme's call, and some read it strictly. Since this PR already tells operators the module changed underneath them, one more sentence — the tested environments top out at Java 21; FIPS mode on a newer JDK relies on vendor affirmation — turns a surprise into a decision. Non-blocking.


On the red CI: none of it is this change. Six of the seven failures are the runner failing to fetch actions/setup-java or actions/download-artifact from codeload.github.com with 429/502/503 after three retries; the seventh (java-test-image) got further and then had its bitshuffle-0.3.4.tar.gz download return a 199-byte error body that tar rejected with gzip: invalid magic — the same throttling wearing a different hat. Every job died in setup and no test executed. Nearly every PR pushed in that 15:22–15:29 UTC window has an identically failed run, so the burst rate-limited itself. A rerun should clear it; no change needed here.

Nothing above touches the non-FIPS 1.85 bump, which looks fine as it stands. Point 1 is the only one I would want settled before merge, and it may well be settled by a link.

@david-streamlio
david-streamlio requested a balanced review from Copilot August 17, 2026 17:18

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.

david-streamlio added a commit to david-streamlio/pulsar that referenced this pull request Aug 17, 2026
… not missing machinery

The first gap bullet claimed the BC/BC-FIPS swap "lost its packaging story in
the Gradle migration", which reads as though the Maven-era swappable
bouncy-castle-bc / bouncy-castle-bcfips NAR modules need resurrecting. They do
not, and this PIP never proposed to: BC and BC-FIPS are mutually exclusive on a
classpath, so the swap is a matter of shipping one set of jars instead of the
other, and SecurityUtility's reflective loading already accepts either flavor.
Raised on the DISCUSS thread.

The bullet now concedes that mechanics up front, states explicitly that the NAR
modules are not coming back, and locates the actual gap where it belongs: no
supported route for the FIPS artifacts to reach a server runtime (the server
distribution excludes bc-fips outright), no server-side coverage (the only
consumer is a client-side JCA-swap test), and no documentation telling an
operator which jars to swap.

Also drops the pinned FIPS version numbers and the libs.versions.toml line
reference, both of which go stale when the BouncyCastle upgrade in apache#26349
lands; the surviving citation is the distribution/server exclusion, which is
the load-bearing evidence.
@lhotari
lhotari marked this pull request as draft August 18, 2026 08:33
…the version catalog

The FIPS 140-3 cryptographic boundary for BC-FJA is the bc-fips jar file name: the security
policy for CMVP certificate apache#4943 names bc-fips-2.1.1.jar, and apache#4743 names bc-fips-2.0.0.jar.
Exactly one published jar per line is therefore the validated module, and BouncyCastle ships
security fixes only in the later "Provider (patched)" releases, certifying them afterwards.

The consequence, which is easy to get wrong when bumping these versions, is that no certified
bc-fips jar is free of known CVEs: 2.1.1 has 13 and 2.0.0 has 11, two CVSS 9.3 in each. Record
the mapping and that reasoning next to the versions so the trade-off does not have to be
re-derived. Also note that bc-fips is test-only in this build.

No version changes.

Assisted-by: Claude Code (Opus 5)
@lhotari

lhotari commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Thanks @david-streamlio — this was worth blocking on. You were right that the description made a certification claim the certificates don't support, and chasing it down turned up something that changes the shape of the answer. The description is rewritten; summary here.

Your question: outcome (b), and there is no ambiguity to resolve

I went to the security policy rather than the certificate page, and it removes the guesswork — the boundary is the jar file, by name. 140sp4943.pdf (BC-FJA, Software Version 2.1.1, 2025-07-18) §2:

The Module is of type software and the module has a Multi-Chip Stand Alone embodiment; the cryptographic boundary is the Java Archive (JAR) file, bc-fips-2.1.1.jar.

So your outcome (c) is out — there is no jar-version/module-version indirection to map. Exactly one published jar per line is the validated module, and it is named in the policy. Outcome (a) is out too: 2.1.2 and 2.1.3 appear nowhere in the 58-page document, and there is no maintenance-coverage language.

BouncyCastle's own download page shows the vendor side of the same structure, listing two provider jars per line — bc-fips-2.1.0.jar under "Provider" and bc-fips-2.1.3.jar under "Provider (patched)":

The 2.1.0 release, BC-FJA 2.1.0 (Certificate #4943), is certified for use on Java 8, Java 11, Java 17, and Java 21. The 2.1.3 release is a patch release on 2.1.0/2.1.1/2.1.3, going into submission.

The roadmap fills in the middle: 2.1.1 is a "Bug fix release for 2.1.0 (GC related issues)", "Superseded by 2.1.2"; 2.1.2 is "Under submission". (BC's pages still name 2.1.0 as certified — CMVP rolled #4943 forward to 2.1.1 in its 2025-11-14 update.) So 2.1.3's status is pending submission: not certified, not rejected.

Why the PR keeps 2.1.3 rather than taking your suggested fix

Your suggestion was the obvious cheap fix, and I expected to take it. It doesn't survive the CVE data. Because BouncyCastle ships security fixes only in the patch releases and certifies them afterwards, no certified bc-fips jar is free of known CVEs — on either line:

bc-fips Certified? Known CVEs
2.0.0 #4743 11 — incl. CVE-2026-8763, CVE-2026-58062 (both CVSS 9.3)
2.0.1 — the pin this PR replaces 10 — incl. the same two 9.3s
2.0.2 0
2.1.1 #4943 13 — incl. the same two 9.3s
2.1.2 12
2.1.3 — this PR ❌ pending submission 0

The two criticals are certificate-validation bypasses: CVE-2026-8763 (Name Constraints bypass via a trailing dot in rfc822Name/URI) and CVE-2026-58062 (stapled OCSP response accepted without binding to the checked certificate). Pinning 2.1.1 to match the certificate would knowingly ship both.

One caution if you go to reproduce this: OSV and the GitHub Advisory Database are currently stale here. An OSV query for org.bouncycastle:bc-fips at 2.1.1 returns one advisory. The 2026-08-03 batch is still "Undergoing Analysis" at NVD and not yet mapped to Maven coordinates, so it has to be read from NVD directly.

And the point that decides it for this PR: the existing bc-fips 2.0.1 pin is not a certified version either#4743 names 2.0.0 only. So 2.1.3 is no worse than the status quo on certification and strictly better on CVEs. The same reasoning rules out retargeting to 2.0.2, which would also be uncertified. There is no available option that is both certified and patched; the PR picks patched, and the version catalog now carries the mapping and that trade-off in a comment so the next bump doesn't have to re-derive it.

Your point 2 — confirmed, with a mitigating fact

The interim horizon is real: #4943 sunsets 2027-01-16 vs #4743's 2029-07-28. What takes the sting out is something I should have checked before writing that operator note: bc-fips is test-only in this build. It's declared once, as testImplementation in tests/pulsar-client-test-bcfips; distribution/server/build.gradle.kts explicitly excludes it; it's in no LICENSE.bin.txt, no published POM, and not in pulsar-bom. Resolving the distribution confirms it:

$ ./gradlew :distribution:pulsar-server-distribution:dependencies | grep -o 'org.bouncycastle:[a-z0-9-]*:[0-9.]*' | sort -u
org.bouncycastle:bcpkix-jdk18on:1.85
org.bouncycastle:bcprov-jdk18on:1.85.2
org.bouncycastle:bcutil-jdk18on:1.85

No bc-fips at any version. A FIPS deployment assembles its own provider classpath today (PIP-478 says as much), so this pin commits no operator to the shorter horizon — it determines what Pulsar's FIPS test module exercises. Which means my "swaps out the certified cryptographic module underneath anyone running Pulsar in FIPS mode" line was wrong on both halves: Pulsar ships no bc-fips, and 2.1.3 isn't the certified module. That section is gone.

Your point 3 — confirmed, and the policy is more specific than the certificate

Beyond the tested environments (Table 2), the policy lists 78 Vendor Affirmed environments (Table 3) — and every one tops out at Java SE Runtime Environment v21. Java 25 is on neither list. The porting terms:

The cryptographic module will remain compliant with the FIPS 140-3 validation when operating on any general purpose computer (GPC) provided that: 1) No source code has been modified. 2) The GPC uses the specified single-user platform, or another compatible single-user platform such as one of the Java SE Runtime Environments listed on any of the following: […]

For the avoidance of doubt, it is hereby stated that the CMVP makes no statement as to the correct operation of the module or the security strengths of the generated keys when so ported if the specific operational environment is not listed on the validation certificate.

Worth noting condition (1) also independently rules out reading vendor affirmation as covering a different jar — it varies the operational environment only. Given the test-only scoping above, for this repo the Java-25 point affects which JDK pulsar-client-test-bcfips runs on in CI, nothing shipped.

Also confirmed on the other two: bcpkix-fips and bcutil-fips are outside the certified boundary ("This module is the only software component within the Cryptographic Boundary"), so those bumps carry no certification consequence — they're the vendor's minimum fixed versions for their own CVEs.

On the red CI

Agreed with your diagnosis, and it cleared on rerun — all checks are green now, no change needed.

Separately: your review surfaced that the non-FIPS half matters more than either of us framed it. 1.85 is the fixed version for BouncyCastle's 2026-08-03 batch, and the maintenance branches ship 1.84, so that bump is going to branch-4.2 in its own PR with the CVEs enumerated.

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

Labels

area/dependency Pull requests that update a dependency file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants