-
Notifications
You must be signed in to change notification settings - Fork 1.3k
CVE‐2026‐18040
Title: HQC leaks private key information through secret-indexed GF(2^8) tables and a secret-dependent fixed-weight sampler.
Issue affecting: BC before 1.86 (from 1.73).
Fixed versions: BC 1.86.
Platform affected: Java 8 and later.
HQC (org.bouncycastle.pqc.crypto.hqc) leaked secret-derived data through two side channels.
Secret-indexed field arithmetic. GF implemented GF(2^8) multiplication, squaring and inversion with log/exp/inv/sqr lookup tables (256 int entries each, so 16 cache lines apiece) indexed by field elements, making the cache line touched a function of the operand — the same weakness as an AES T-table implementation. The tables were reached with secret inputs on both sides of the KEM: ReedSolomon.encode multiplies by coefficients derived from the secret message during encapsulation, and ReedSolomon.decode and FastFourierTransform operate on syndromes, error-locator coefficients and root sets derived from the secret error vector during decapsulation. GF is now table-free and branch-free: a carryless multiply with a masked reduction modulo X^8 + X^4 + X^3 + X^2 + 1, squaring by bit-spreading, and inversion as a^254 over a fixed addition chain.
Secret-dependent fixed-weight sampling. HQCEngine.generateRandomSupport left its duplicate-detection scan as soon as a collision was found, so the number of comparisons revealed which accepted position had collided, and it stored each accepted position at support[count], a store to a secret-derived address. This sampler draws the secret key supports x and y during key generation, and — more importantly — re-expands y from the long-term secret key seed on every decapsulation. Because the seed is the same each time, the draw sequence is identical on every call, so the timing is a fixed fingerprint of the private key that an attacker can measure to arbitrary precision by repeatedly timing decapsulations, with no need for chosen ciphertexts. Every candidate in a batch is now examined even once enough positions have been accepted, the duplicate scan visits every slot with no early exit, and an accepted position is written by masking across all slots rather than by indexing with the running count.
The rewritten sampler produces the identical output and consumes the identical number of bytes from the SHAKE stream, so generated keys, ciphertexts and shared secrets are unchanged and the known-answer vectors still hold. The number of candidate batches squeezed remains data-dependent (a second batch is needed with probability roughly 0.15 to 0.3 depending on the parameter set); that residual is inherent to the specified sampler and cannot be removed without diverging from its output, and therefore from interoperability with other HQC implementations.
The sibling sampler used for the encapsulation error vectors r1, r2 and e was already branch-free (multiply-shift reduction, fixed-trip masked duplicate handling) and was not affected.
HQC is exposed through the lightweight API and the BouncyCastlePQCProvider; the BouncyCastleProvider registers only its key-info converters, so the algorithm is not reachable as a KEM from the default provider.
The fix was introduced in commit 283acd8104.
Credit: The Robusta team: Deepak Bhargavan Pillai, Anirban Chakraborty, Chitchanok Chuengsatiansup, Matthew Roughan, Peter Schwabe, and Yuval Yarom.