The Certificate Proved Nothing: Bouncy Castle’s MLS Credential Was Never Bound to the Signing Key

CVE-2026-71885 was published to NVD at 09:17 UTC on 3 October 2026, scored CVSS 4.0 9.2 Critical, classified CWE-287 and CWE-295, and affecting Bouncy Castle for Java before 1.86. The bug is in the Messaging Layer Security implementation, and it is the kind that is hard to describe without sounding like an exaggeration: for an X.509 credential, the certificate authenticated nothing at all.

RFC 9420 section 5.3 makes a group member's signature_key and its credential two views of one identity — for an X.509 credential, the leaf's signing key is the key certified by the credential's end-entity certificate. Bouncy Castle stored the certificate chain and never looked at it.

Two checks that never met

The project's own advisory describes the mechanism plainly. LeafNode.verify() verified a leaf's signature against the signature_key carried in the leaf itself, and its x509 branch was an empty placeholder. The credential's certificate chain was decoded onto the Credential object but, in the advisory's words, “never parsed, validated, or compared to anything — there was no accessor for it and no code read it.”

So two things were true at once, and nothing connected them: the signature was valid with respect to a key the submitter chose, and the credential was a certificate the submitter also chose. A party could present another party's certificate as its credential while signing the leaf, and the enclosing KeyPackage, with an unrelated key of its own. Both KeyPackage.verify() and the Group leaf-validation path authenticate through LeafNode.verify(), so the forged leaf was accepted under the certificate subject's identity.

This is the structural cousin of every authentication bug where a system verifies something real and then attributes it to the wrong principal. A signature check that passes is not an identity check. The question is always whose key was that, and the code never asked.

Where it becomes group compromise

Identity confusion in a group-messaging protocol does not stay confined to identity. The advisory sets out the deployment shape that turns it into full access: a deployment that publishes a current GroupInfo and ratchet tree for external joining, and feeds external commits to Group.handle(...) without an independent credential-admission check.

In that configuration, an unauthenticated attacker could be admitted under a victim's X.509 identity and evict the victim — because resynchronization compares whole encoded credentials rather than signing keys, so the attacker's leaf was treated as the same participant. From there the advisory describes deriving the current epoch, decrypting subsequent group messages, and sending messages accepted as the victim.

That chain is worth reading slowly. The end state is not “an attacker can impersonate a member”; it is an attacker who holds the victim's seat, reads the group's traffic going forward, and speaks under the victim's name, in a protocol whose entire purpose is continuous group key agreement with forward secrecy. The VC:H/VI:H in the CVSS 4.0 vector — CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N — is doing exactly the work it claims to. Note AT:P: the attack requires the deployment precondition above, which is the honest part of the score.

The fix, and the part that is still yours

The fix landed in commit 77632a57ed, authored 18 August 2026, with a message that states the requirement directly: require an MLS X.509 credential's end-entity certificate to certify the LeafNode signature key. The diff is small and tells you where the hole was — LeafNode.java (+34/−2), Certificate.java (+13), Credential.java (+5) — alongside a new 122-line LeafNodeX509BindingTest. Thirty-four lines of verification and a regression test for a 9.2.

TreeKEM.LeafNode.verify() now requires the end-entity certificate's subject public key, serialised in the cipher suite's signature encoding, to equal signature_key, and rejects the leaf otherwise — including an empty chain, or a certificate whose key type does not match the cipher suite. A public Certificate(byte[]) constructor and a Credential.getCertificates() accessor were added so callers can finally inspect what they are being handed.

Read the next sentence of the advisory carefully, because it is a boundary and not a footnote: certificate-chain and identity validation to a trust anchor remain the application's responsibility, per RFC 9420 section 5.3.1. The library now guarantees the certificate and the signing key describe the same party. It does not tell you whether that party should be in your group. If your admission logic assumed the library was doing chain validation, patching to 1.86 does not supply the check you thought you had.

Deployments using only basic credentials are unaffected.

The timing gap nobody announced

Here is the part that matters for anyone tracking supply-chain exposure windows. The fix commit is dated 18 August 2026. Bouncy Castle 1.86 was published to Maven Central on 11 September 2026 — both bcprov-jdk18on and the bcmls-jdk18on module that carries this code. The CVE was published on 3 October 2026.

So the fix shipped in a general release roughly three weeks before anyone was told what it fixed, and the public record arrived about seven weeks after the commit. That is not misconduct — it is a normal embargo, and a library whose releases are consumed by banks and governments has good reasons to stage disclosure. But it means something specific for defenders: for three weeks, the diff explaining the attack was public in the commit history while the advisory was not, and dependency scanners had nothing to flag. We have written about how little that window is worth now that fix-commit-to-probe is measured in minutes, and about what happens on the other extreme, when advisories trail fixes by a year.

The practical consequence is a scanning gap of a familiar shape: anything that upgraded to 1.86 in September is already fine and will now be flagged as remediated; anything still on 1.85 or earlier has been exposed the entire time and only learned on 3 October. It is the same alerting blind spot as advisories that never reach the tooling that gates your builds.

A second record landed the same day from the same release. CVE-2026-97873 (CVSS 4.0 5.3, CWE-770) covers unbounded iteration counts in the raw JCA provider's legacy PBES1 and PKCS#12 PBE families: the parameter parser accepted any count from an encoded PKCS12PBEParams or PBEParameter and narrowed out-of-range values with intValue(), and the derivations honoured whatever they were given. The advisory's own illustration is the memorable one — a 52-byte encrypted key carrying the maximum count held a single getKeySpec call for about twenty minutes of CPU. Both the parse and the derivations now enforce org.bouncycastle.pbe.max_iteration_count (default 10,000,000), the same bound BC 1.85 applied to PBKDF2 for CVE-2026-17508. It is fixed in 1.86 and BC-LTS 2.73.13. If you raised org.bouncycastle.pkcs12.max_it_count above the default, you need to raise the new property with it or you will break working key stores.

What to do

  • Upgrade to 1.86 — and move all Bouncy Castle modules together. The MLS code lives in bcmls-jdk18on, which many teams never think about because they only ever named bcprov. Mixed module versions across a classpath are their own failure; pin the set.
  • Scope by credential type, not by dependency presence. If your MLS deployment uses only basic credentials you are not affected by CVE-2026-71885. If it uses X.509 credentials, you are, and the exposure window runs back to whenever you adopted the MLS module.
  • Audit your external-commit admission path specifically. The advisory's escalation requires publishing GroupInfo and a ratchet tree for external joining and handing external commits to Group.handle(...) without an independent credential check. If that describes your service, you were in the exploitable configuration and the fix alone does not add the admission check — write it.
  • Write the trust-anchor validation you may have assumed existed. Chain and identity validation to a trust anchor is explicitly the application's job under RFC 9420 5.3.1. Use the new Credential.getCertificates() accessor to actually inspect the chain, and decide admission on the result.
  • Treat any X.509 MLS group with unexplained membership churn as needing review. The attack's signature is a participant being silently replaced during resynchronization, because whole encoded credentials were compared rather than signing keys. Eviction of a legitimate member followed by continued apparent participation is the thing to look for.
  • For CVE-2026-97873, check your configured bounds before upgrading. A deployment that raised org.bouncycastle.pkcs12.max_it_count above 10,000,000 must raise org.bouncycastle.pbe.max_iteration_count to match, or legitimate key stores will start failing after the upgrade. The PKIX JcePKCSPBEInputDecryptorProviderBuilder path and the provider's PKCS#12 key store were already bounded.

Verification note: we read the NVD records for CVE-2026-71885 (published 3 October 2026, status Received, CVSS 4.0 9.2, CWE-287 and CWE-295) and CVE-2026-97873 (published 3 October 2026, CVSS 4.0 5.3, CWE-770) via the NVD API, and the project's own advisory pages on the bcgit/bc-java wiki for both, which are the records NVD references. The fix commit 77632a57ed was confirmed through the GitHub API: author date 2026-08-18T00:59:20Z, commit message as quoted, and the changed files and line counts in LeafNode.java, Certificate.java, Credential.java and the new LeafNodeX509BindingTest.java. Release timing comes from Maven Central directory listings for org/bouncycastle/bcprov-jdk18on and org/bouncycastle/bcmls-jdk18on, which both show 1.86 dated 2026-09-11. The attack chain — admission under a victim's identity, eviction via credential-based resynchronization, epoch derivation and subsequent message decryption — is the advisory's description of the vulnerability and its stated deployment precondition; we did not build an MLS group, construct a KeyPackage, or attempt any part of it. Credit for CVE-2026-71885 is given in the advisory to Joshua Rogers and Khaled Suliman of AISLE Research, and for CVE-2026-97873 to Arpan Sharma.

Sources: