{"abstract":"PKCS#1 v1.5 RSA decryption exposes the 'was the padding valid?' bit: one Bleichenbacher oracle (1998), read through several channels: in the response (behavioral, ROBOT 2018), by timing (Marvin 2023), by cache (9 Lives 2019). This surveys the behavioral channel across 27 implementations, all runtime-confirmed. The behavioral Ok/Err oracle is open by default in self-contained v1.5 implementations; it is closed only where the backend (OpenSSL ≥3.2, NSS) or a TLS-only construction supplies implicit rejection. The OpenSSL wrappers that look closed flip open on older OpenSSL or LibreSSL.","author":"Mark Esler","content":"Companion to An Ok/Err Result Is a Bleichenbacher Oracle. It showed that one library\u0026rsquo;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.\nThat bit leaks more than one way, and has since Bleichenbacher\u0026rsquo;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 \u0026ldquo;closed\u0026rdquo; verdict means that success/error bit is gone; a \u0026ldquo;removed\u0026rdquo; verdict means a library dropped v1.5 decryption.\nVerdicts 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).\nTally: 10 closed (5 by design, 5 OpenSSL-backed wrappers †), 12 open, 1 masked, 3 removed, 1 token-dependent.\nScope. No timing claims: timing-safety (Marvin) is not measured here. \u0026ldquo;Open at the API\u0026rdquo; ≠ 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.\nImplementation Lang Oracle Mechanism / maintainer stance Discussion .NET runtime C# open Microsoft\u0026rsquo;s build disables OpenSSL IR to keep Decrypt throwing (as on Windows/CNG); Red Hat\u0026rsquo;s downstream build re-enables it (closed) #95157 · #97866 BearSSL C closed only 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 BoringSSL C open returns -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 \u0026ldquo;is vulnerable to a chosen-ciphertext attack\u0026rdquo; that \u0026ldquo;may give the attacker control over your private key\u0026rdquo;, citing Bleichenbacher (Crypto \u0026lsquo;98) rsa.h API doc BouncyCastle Java open default 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 cjose C masked JWE 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 API src/jwe.c Crypt-OpenSSL-RSA Perl removed v1.5 decryption removed (0.35 croaks \u0026ldquo;Marvin attack\u0026rdquo;, CVE-2024-2467); ≤0.34 was open. Runtime-confirmed (0.41) POD security note Go crypto/rsa Go open (deprecated) DecryptPKCS1v15\u0026rsquo;s godoc warns that an attacker who can \u0026ldquo;learn whether each instance returned an error … can decrypt and forge signatures as if they had the private key.\u0026rdquo; 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 jsrsasign JS removed v1.5 decrypt removed in v11 (\u0026ldquo;for Marvin attack\u0026rdquo;); ≤v10 was open 11.0.0 libgcrypt C open no 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_PROBLEM gcrypt-devel (Mar 2024) LibreSSL C open raw 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) M2Crypto Python closed † OpenSSL wrapper; runtime-confirmed (0.48.0 / OpenSSL 3.5.4): bad pad → synthetic message, no None/exception 84c53958 Mbed TLS C open default 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-rsa JS open throws; 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++/JS removed the 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 default node@54cd268 · Feb-2024 release NSS C closed RSA_DecryptBlock is implicit-rejection-only (synthetic message, always succeeds); added in NSS 3.61, the (timing-titled) bug 1651411 fix / CVE-2023-4421 bug 1651411 opencryptoki C closed implicit rejection (CVE-2024-0914); runtime-confirmed (v3.26 soft token): bad pad → CKR_OK + synthetic message #737 OpenSSL C closed implicit rejection, default for RSA_PKCS1_PADDING since 3.2: returns a pseudo-random message, not an error #13421 · #13817 · EVP_PKEY_decrypt(3) pkcs11-provider C token-dep no v1.5 validation of its own: inherits the backend token\u0026rsquo;s CK_RV; closed with NSS softoken, open with a strict HSM asymmetric_cipher.c pyca/cryptography Python closed † implicit rejection via the bundled OpenSSL ≥3.2 (v42 wheels, Jan 2024); earlier versions open 42.0.0 changelog PyCryptodome Python open never 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-rsa Python open (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\u0026rsquo;t be made timing-safe, and to fork it. runtime-confirmed (4.9.1) #165 · README Ruby openssl Ruby closed † OpenSSL::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\u0026rsquo;s IR, not ruby/openssl\u0026rsquo;s #732 Rust openssl Rust closed † Rsa::private_decrypt → RSA_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 rsa Rust open returns Err on bad pad; an implicit-rejection API (PR #680) is unmerged #626 · #680 s2n-tls C closed unpad-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 wolfSSL C open fixed 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 xmlsec C closed † rsa-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 \u0026lt;3.2 or LibreSSL the same code is open. What the table shows Self-contained implementations are open by default. These do their own v1.5 unpadding and return the bit: BoringSSL, BouncyCastle, Go, libgcrypt, LibreSSL, Mbed TLS, node-rsa, PyCryptodome, python-rsa, RustCrypto, wolfSSL. Implicit rejection is concentrated in OpenSSL ≥3.2. Five of the ten \u0026ldquo;closed\u0026rdquo; rows are OpenSSL-backed wrappers (†). The by-design closures use one of two mechanisms: implicit rejection, a derived synthetic message (OpenSSL ≥3.2 itself, NSS, and opencryptoki), or a constant-time random swap (BearSSL, s2n). For a † row the verdict is the linked libcrypto\u0026rsquo;s, not the library\u0026rsquo;s. A random swap closes it only if the value never escapes. IR\u0026rsquo;s synthetic message is derived from key and ciphertext, so replaying a ciphertext learns nothing. A fresh-random swap would leak under replay: the same response means the padding was valid, a different one means it was not. BearSSL and s2n swap safely only because the premaster never escapes: it feeds key derivation and the handshake fails identically whether real or random, which Klíma, Pokorný, Rosa proved defeats the attack. A general decrypt API returning a fresh value would need deterministic IR. PyCryptodome is that case: its sentinel escapes to the caller, so it stays open unless the caller makes it indistinguishable. Constant-time ≠ closed. wolfSSL, BoringSSL, node-rsa harden the timing of the unpad; the Ok/Err bit still leaks. Remove and deprecate are not the same outcome. Removing the v1.5 decrypt ends the oracle: jsrsasign (v11) and Crypt-OpenSSL-RSA (0.35) did. Deprecating it does not: Go 1.26 deprecated its whole v1.5 encryption API (#75302), yet DecryptPKCS1v15 is code-unchanged and still returns the bit; removing it would break the Go 1 compatibility promise. Either way, the CFRG draft and NIST SP 800-131A with SP 800-56B Rev. 2 disallow v1.5 key transport. Timeline: implicit rejection in .NET How one library ended up open in one build and closed in another.\nBackground. OpenSSL adds implicit rejection for PKCS#1 v1.5 (#13817), shipped in 3.2. 2023-11-22. A .NET crypto test fails: RSA.Decrypt no longer errors on bad padding (#95115). Cause: OpenSSL\u0026rsquo;s implicit rejection. 2023-11-24. .NET merges a one-line opt-out to disable it and keep Decrypt throwing, the documented, Windows-consistent behavior (#95157: EVP_PKEY_CTX_ctrl_str(ctx, \u0026quot;rsa_pkcs1_implicit_rejection\u0026quot;, \u0026quot;0\u0026quot;)). 2023-12-06. The author of the OpenSSL change objects on the PR: \u0026ldquo;This will make all users of the RSA PKCS#1v1.5 decryption API vulnerable!\u0026rdquo; (comment). A Red Hat engineer concurs: \u0026ldquo;this re-enables a Bleichenbacher timing oracle attack … I don\u0026rsquo;t think this should be merged\u0026rdquo; (comment). 2024-01-13. The .NET security team proceeds, after consulting Red Hat (statement): \u0026quot;…what we believe an appropriate default security stance is for the product. […] we speak only for our own framework. We don\u0026rsquo;t speak for other languages and frameworks, who are of course free to pursue a different strategy.\u0026quot; 2024-02-02. Red Hat takes that different strategy: it reverts the opt-out in its own .NET packages (\u0026ldquo;Revert \u0026lsquo;Disable implicit rejection for RSA PKCS#1\u0026rsquo;\u0026rdquo;), citing the Marvin attack. On a Fedora build the opt-out is gone: the rsa_pkcs1_implicit_rejection string is absent from the shipped libSystem.Security.Cryptography.Native.OpenSsl.so. Outcome. Microsoft\u0026rsquo;s .NET throws on bad padding (open); Red Hat\u0026rsquo;s .NET returns a synthetic message (closed).\nBleichenbacher\u0026rsquo;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.\n","date":"2026-06-06","description":"A runtime-verified survey of 27 RSA implementations for the behavioral (ROBOT / Ok-Err) PKCS#1 v1.5 decryption oracle. The oracle is open by default in self-contained implementations; it is closed only where the backend (OpenSSL ≥3.2, NSS) or a TLS-only design supplies implicit rejection. No timing claims.","lastmod":"2026-06-07","readingTime":9,"section":"datagrams","title":"Many RSA Libraries Leave the Bleichenbacher Oracle Open","url":"https://hexproof.dev/datagrams/bleichenbacher-oracle-survey/","wordCount":1717}