Many RSA Libraries Leave the Bleichenbacher Oracle Open

· 9 min read · Mark Esler

Companion to An Ok/Err Result Is a Bleichenbacher Oracle. It showed that one library’s Ok/Err decrypt result is a deterministic Bleichenbacher oracle. That Ok/Err reveals the bit: was the PKCS#1 v1.5 padding valid? This post surveys which other implementations leak it.

That bit leaks more than one way, and has since Bleichenbacher’s 1998 paper: in the response itself (behavioral, as ROBOT re-found across TLS servers), or through a side channel the code failed to close, by timing (Marvin) or by cache (9 Lives). This post is about the behavioral channel, where the bit is in the result, no statistics required. Implicit rejection closes that channel: on bad padding, return a deterministic synthetic message instead of an error, so there is no Ok/Err to read (CFRG draft, Alicja Kario). It is a stopgap, since the fix is to stop using RSA v1.5 encryption. A “closed” verdict means that success/error bit is gone; a “removed” verdict means a library dropped v1.5 decryption.

Verdicts are runtime-confirmed. Where a library exposes a v1.5 decrypt API, that API was run on a valid ciphertext and on garbage ciphertexts to see whether valid-vs-invalid is distinguishable. Two of the 27 expose no general-purpose decrypt API: BearSSL and s2n-tls (no callable decrypt entry point, so their own depad routines were run from source).

Tally: 10 closed (5 by design, 5 OpenSSL-backed wrappers †), 12 open, 1 masked, 3 removed, 1 token-dependent.

Scope. No timing claims: timing-safety (Marvin) is not measured here. “Open at the API” ≠ exploitable: it takes the consuming protocol surfacing the error and acting on it. Each row cites a public reference (Discussion column): a survey of the known 1998 oracle, not a disclosure of new vulnerabilities.

ImplementationLangOracleMechanism / maintainer stanceDiscussion
.NET runtimeC#openMicrosoft’s build disables OpenSSL IR to keep Decrypt throwing (as on Windows/CNG); Red Hat’s downstream build re-enables it (closed)#95157 · #97866
BearSSLCclosedonly v1.5 API is br_rsa_ssl_decrypt: constant-time random-PMS swap, no app-facing Ok/Err. Runtime-confirmed: even a conforming non-PMS block folds to the same 0 outcome (there is no general-purpose v1.5 decrypt API to leak a bit)bearssl_rsa.h
BoringSSLCopenreturns -1 on bad pad (runtime-confirmed, BoringSSL git 7883578: garbage → -1 + PKCS_DECODING_ERROR); no library IR by design. Its own API doc warns the v1.5 decrypt scheme “is vulnerable to a chosen-ciphertext attack” that “may give the attacker control over your private key”, citing Bleichenbacher (Crypto ‘98)rsa.h API doc
BouncyCastleJavaopendefault decodeBlock throws InvalidCipherTextException (the JCE Cipher surfaces it as BadBlockException); an opt-in fallback constructor (PKCS1Encoding(cipher, fallback)) hides validity instead, as the TLS layer uses for ROBOT (CVE-2017-13098 was that fix); runtime-confirmed (bcprov 1.78.1)CVE-2017-13098
cjoseCmaskedJWE RSA1_5 returns the identical CJOSE_ERR_CRYPTO for both a conforming-but-wrong-CEK block and a non-conforming block, so cjose_jwe_decrypt exposes no clean padding bit; the RSA-layer oracle exists but is indistinguishable through the public APIsrc/jwe.c
Crypt-OpenSSL-RSAPerlremovedv1.5 decryption removed (0.35 croaks “Marvin attack”, CVE-2024-2467); ≤0.34 was open. Runtime-confirmed (0.41)POD security note
Go crypto/rsaGoopen (deprecated)DecryptPKCS1v15’s godoc warns that an attacker who can “learn whether each instance returned an error … can decrypt and forge signatures as if they had the private key.” Deprecated whole-API in Go 1.26 (incl. DecryptPKCS1v15SessionKey) but code-unchanged, so it still returns the bit. runtime-confirmed (go 1.26.3)#75302
jsrsasignJSremovedv1.5 decrypt removed in v11 (“for Marvin attack”); ≤v10 was open11.0.0
libgcryptCopenno implicit rejection. The linked Marvin report flagged that closing the channel needs IR; libgcrypt shipped only the constant-time unpad fix (CVE-2024-2236), so _gcry_rsa_pkcs1_decode_for_enc still errors on bad pad: the Ok/Err bit is observable, just not via timing. runtime-confirmed (1.12.2): valid → Ok, garbage → GPG_ERR_ENCODING_PROBLEMgcrypt-devel (Mar 2024)
LibreSSLCopenraw RSA_private_decrypt errors on bad pad: no implicit rejection (the fix OpenSSL added in 3.2). The man page documents it as a Bleichenbacher oracle and points to OAEP.RSA_private_decrypt(3)
M2CryptoPythonclosedOpenSSL wrapper; runtime-confirmed (0.48.0 / OpenSSL 3.5.4): bad pad → synthetic message, no None/exception84c53958
Mbed TLSCopendefault v1.5 decrypt is the PSA primitive now (psa_asymmetric_decrypt; legacy rsa.h removed in 4.x), returns PSA_ERROR_INVALID_PADDING; runtime-confirmed (4.1.0): garbage → -150. 4.x also removed the TLS RSA key exchange (#8170), so the general decrypt is the only v1.5 path left#8459 · #8170
node-rsaJSopenthrows; v1.5 is non-default (OAEP default); maintainer recommends OAEP; runtime-confirmed (node-rsa 2.0.0)v2.0.0 security notes
Node.js (built-in)C++/JSremovedthe Feb-2024 security release disabled RSA_PKCS1_PADDING for crypto.privateDecrypt (Marvin mitigation); only --security-revert restores it, so v1.5 private decrypt is off by defaultnode@54cd268 · Feb-2024 release
NSSCclosedRSA_DecryptBlock is implicit-rejection-only (synthetic message, always succeeds); added in NSS 3.61, the (timing-titled) bug 1651411 fix / CVE-2023-4421bug 1651411
opencryptokiCclosedimplicit rejection (CVE-2024-0914); runtime-confirmed (v3.26 soft token): bad pad → CKR_OK + synthetic message#737
OpenSSLCclosedimplicit rejection, default for RSA_PKCS1_PADDING since 3.2: returns a pseudo-random message, not an error#13421 · #13817 · EVP_PKEY_decrypt(3)
pkcs11-providerCtoken-depno v1.5 validation of its own: inherits the backend token’s CK_RV; closed with NSS softoken, open with a strict HSMasymmetric_cipher.c
pyca/cryptographyPythonclosedimplicit rejection via the bundled OpenSSL ≥3.2 (v42 wheels, Jan 2024); earlier versions open42.0.0 changelog
PyCryptodomePythonopennever raises on bad padding: decrypt(ct, sentinel, expected_pt_len=0) returns the caller-supplied sentinel, so the validity bit shows up by value or length, not Ok/Err (only a wrong-length ciphertext raises, and that length is public). Maintainer recommends OAEP; runtime-confirmed (3.23.0)#481 · docs
python-rsaPythonopen (archived)decrypt() raises on bad pad; its one public Bleichenbacher report (CVE-2020-25658) is the timing channel, not behavioral. Repo archived; the maintainer says a pure-Python library can’t be made timing-safe, and to fork it. runtime-confirmed (4.9.1)#165 · README
Ruby opensslRubyclosedOpenSSL::PKey::RSA#private_decrypt binding; runtime-confirmed both backends: OpenSSL 3.6.2 → synthetic (closed), LibreSSL 3.3.6 → RSAError (open). Maintainer points to OpenSSL 3.2’s IR, not ruby/openssl’s#732
Rust opensslRustclosedRsa::private_decryptRSA_private_decrypt; runtime-confirmed (openssl 0.10 / OpenSSL 3.6.2): garbage → synthetic (closed). Its Bleichenbacher CVE (CVE-2024-3296) is timing, not behavioral#2171 · Rsa::private_decrypt
RustCrypto rsaRustopenreturns Err on bad pad; an implicit-rejection API (PR #680) is unmerged#626 · #680
s2n-tlsCclosedunpad-or-dont, always returns success with random output, CBMC-proven (runtime-confirmed via depad functions vendored from source; s2n has no standalone decrypt CLI)PR #916
wolfSSLCopenfixed the TLS static-RSA path (ROBOT) and made the unpad constant-time for Marvin (CVE-2023-6935, explicitly covering RSA decrypt outside TLS); maintainer recommends disabling static RSA. The general v1.5 decrypt still returns a distinguishable error, so the behavioral bit stays open; runtime-confirmed (5.9.1): garbage → -131 (RSA_BUFFER_E)CVE-2017-13099 · CVE-2023-6935
xmlsecCclosedrsa-1_5 calls EVP_PKEY_decrypt (RSA_PKCS1_PADDING) without disabling IR, so on OpenSSL ≥3.2 it inherits IR; downstream AES-CBC then masks any residual RSA bit. runtime-confirmed closed (xmlsec 1.2.41 / OpenSSL 3.5.4)#289 · kt_rsa.c

† Backend-dependent. These wrap OpenSSL and are closed only because the host links OpenSSL ≥3.2 (implicit rejection); on OpenSSL <3.2 or LibreSSL the same code is open.

What the table shows

Timeline: implicit rejection in .NET

How one library ended up open in one build and closed in another.

Outcome. Microsoft’s .NET throws on bad padding (open); Red Hat’s .NET returns a synthetic message (closed).


Bleichenbacher’s 1998 paper made it straightforward to recover plaintext from a 4096-bit RSA ciphertext or forge a signature, using only the Ok/Err bit: no key, no timing.