Fix RSA-OAEP to allow zero-length plaintext per RFC 8017#10012
Fix RSA-OAEP to allow zero-length plaintext per RFC 8017#10012MarkAtwood wants to merge 1 commit intowolfSSL:masterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates RSA-OAEP handling to permit zero-length plaintexts on both encryption and decryption, aligning behavior with RFC 8017 and other TLS/crypto libraries.
Changes:
- Relaxed
RsaPublicEncryptEx()argument validation to allowinLen == 0for OAEP padding. - Adjusted
RsaPrivateDecryptEx()constant-time error handling to permitret == 0results for OAEP padding. - Added RFC 8017 references in inline comments to document the behavioral change.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| * a zero-length input is invalid. */ | ||
| if (in == NULL || (inLen == 0 && pad_type != WC_RSA_OAEP_PAD)) { |
| * permitted: the spec requires mLen <= k - 2*hLen - 2, and mLen = 0 | ||
| * satisfies this for all supported key sizes. For other padding types, | ||
| * a zero-length input is invalid. */ |
| int zeroOk = ctMaskEq(pad_type, WC_RSA_OAEP_PAD); | ||
| ret = ctMaskSelInt(ctMaskNotEq(ret, 0) | zeroOk, ret, |
eefa39a to
a0752c3
Compare
RsaPublicEncryptEx() rejected inLen==0 unconditionally with BAD_FUNC_ARG. RFC 8017 Section 7.1.1 (RSAES-OAEP-ENCRYPT) permits zero-length messages: the only length constraint is mLen <= k - 2*hLen - 2, which mLen=0 always satisfies. RsaPrivateDecryptEx() converted a zero-length decryption result to RSA_BUFFER_E (unless WOLFSSL_RSA_DECRYPT_TO_0_LEN was defined). RFC 8017 Section 7.1.2 (RSAES-OAEP-DECRYPT) produces the original message M which may be empty. The fix uses constant-time masking to allow ret==0 when pad_type is WC_RSA_OAEP_PAD, preserving the existing timing-safe behavior for other padding types. Both OpenSSL and BoringSSL accept empty OAEP plaintexts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Found via Wycheproof test vectors.
a0752c3 to
e8e7cd1
Compare
There was a problem hiding this comment.
Pull request overview
Updates wolfCrypt RSA-OAEP handling to align with RFC 8017 by permitting zero-length plaintexts on encrypt and decrypt paths.
Changes:
- Adjusts
RsaPublicEncryptEx()argument validation to allowinLen == 0for OAEP padding. - Adjusts
RsaPrivateDecryptEx()handling ofret == 0to allow zero-length OAEP results (under#ifndef WOLFSSL_RSA_DECRYPT_TO_0_LEN) using constant-time masking. - Adds inline RFC 8017 references in both code paths.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /* RFC 8017 Section 7.1.2: OAEP decryption may produce a valid | ||
| * zero-length message. Only reject ret==0 for non-OAEP types. */ | ||
| { | ||
| int zeroOk = ctMaskEq(pad_type, WC_RSA_OAEP_PAD); | ||
| ret = ctMaskSelInt(ctMaskNotEq(ret, 0) | zeroOk, ret, | ||
| WC_NO_ERR_TRACE(RSA_BUFFER_E)); | ||
| } |
There was a problem hiding this comment.
Allowing ret==0 for OAEP here makes OAEP padding errors indistinguishable from a valid empty plaintext: RsaUnPad_OAEP() returns 0 on error by setting idx=pkcsBlockLen (see wolfcrypt/src/rsa.c around the "Return 0 data length on error" logic), so invalid OAEP blocks would now be treated as successful decryptions of an empty message. To support empty messages safely, OAEP unpadding needs a separate validity indicator / negative error return (selected constant-time), and this layer should continue to reject the error case even when the message length is 0.
| /* RFC 8017 Section 7.1.2: OAEP decryption may produce a valid | |
| * zero-length message. Only reject ret==0 for non-OAEP types. */ | |
| { | |
| int zeroOk = ctMaskEq(pad_type, WC_RSA_OAEP_PAD); | |
| ret = ctMaskSelInt(ctMaskNotEq(ret, 0) | zeroOk, ret, | |
| WC_NO_ERR_TRACE(RSA_BUFFER_E)); | |
| } | |
| /* Reject zero-length results unless the dedicated build option | |
| * explicitly enables them. In particular, OAEP unpadding may | |
| * return 0 on error, so allowing ret == 0 here would make an | |
| * invalid OAEP block indistinguishable from a valid empty | |
| * plaintext. */ | |
| ret = ctMaskSelInt(ctMaskNotEq(ret, 0), ret, | |
| WC_NO_ERR_TRACE(RSA_BUFFER_E)); |
| * a zero-length input is invalid. */ | ||
| if (in == NULL || (inLen == 0 && pad_type != WC_RSA_OAEP_PAD)) { |
There was a problem hiding this comment.
This new argument validation still rejects in==NULL even when inLen==0 with OAEP. If the intent is to allow empty OAEP plaintexts, consider permitting a NULL input pointer when inLen==0 (common C API convention and safe here because OAEP padding copies 0 bytes).
| * a zero-length input is invalid. */ | |
| if (in == NULL || (inLen == 0 && pad_type != WC_RSA_OAEP_PAD)) { | |
| * a zero-length input is invalid. Permit a NULL input pointer only for | |
| * the empty OAEP case, which is safe because no input bytes are read. */ | |
| if ((in == NULL && (inLen != 0 || pad_type != WC_RSA_OAEP_PAD)) || | |
| (inLen == 0 && pad_type != WC_RSA_OAEP_PAD)) { |
| * permitted: the spec requires mLen <= k - 2*hLen - 2, and mLen = 0 | ||
| * satisfies this for all supported key sizes. For other padding types, | ||
| * a zero-length input is invalid. */ |
There was a problem hiding this comment.
The comment claims mLen=0 "satisfies this for all supported key sizes", but OAEP also requires k > 2*hLen+2 for the chosen hash; small keys (or large hashes) can still be invalid even with mLen=0. Suggest rewording to only state that mLen=0 satisfies the message-length constraint from RFC 8017 step 1b, without implying all key sizes are valid.
| * permitted: the spec requires mLen <= k - 2*hLen - 2, and mLen = 0 | |
| * satisfies this for all supported key sizes. For other padding types, | |
| * a zero-length input is invalid. */ | |
| * permitted with respect to the step 1b message-length check: | |
| * mLen <= k - 2*hLen - 2. For other padding types, a zero-length input | |
| * is invalid. */ |
| int zeroOk = ctMaskEq(pad_type, WC_RSA_OAEP_PAD); | ||
| ret = ctMaskSelInt(ctMaskNotEq(ret, 0) | zeroOk, ret, |
There was a problem hiding this comment.
ctMaskEq()/ctMaskNotEq() return a byte mask (0x00/0xFF) and ctMaskSelInt() takes a byte mask. Declaring zeroOk as byte (and keeping the combined mask as byte) would better match the CT helpers and avoid potential compiler warnings about implicit int->byte truncation.
| int zeroOk = ctMaskEq(pad_type, WC_RSA_OAEP_PAD); | |
| ret = ctMaskSelInt(ctMaskNotEq(ret, 0) | zeroOk, ret, | |
| byte zeroOk = ctMaskEq(pad_type, WC_RSA_OAEP_PAD); | |
| byte validRetMask = (byte)(ctMaskNotEq(ret, 0) | zeroOk); | |
| ret = ctMaskSelInt(validRetMask, ret, |
| /* RFC 8017 Section 7.1.2: OAEP decryption may produce a valid | ||
| * zero-length message. Only reject ret==0 for non-OAEP types. */ | ||
| { | ||
| int zeroOk = ctMaskEq(pad_type, WC_RSA_OAEP_PAD); | ||
| ret = ctMaskSelInt(ctMaskNotEq(ret, 0) | zeroOk, ret, | ||
| WC_NO_ERR_TRACE(RSA_BUFFER_E)); |
There was a problem hiding this comment.
There are existing OAEP padding tests (e.g., rsa_oaep_padding_test() in wolfcrypt/test/test.c) but they don’t cover a successful OAEP encrypt/decrypt round-trip of an empty message nor do they assert that invalid OAEP padding yields a negative error (as opposed to a 0-length "success"). Adding those cases would prevent regressions around the ret==0 handling being changed here.
Summary
RsaPublicEncryptEx()rejectsinLen == 0withBAD_FUNC_ARG, but RFC 8017 Section 7.1.1 (RSAES-OAEP-ENCRYPT) permits zero-length messages. The only length constraint ismLen <= k - 2*hLen - 2, whichmLen = 0always satisfies.RsaPrivateDecryptEx()converts a zero-length decryption result toRSA_BUFFER_E(unlessWOLFSSL_RSA_DECRYPT_TO_0_LENis defined). RFC 8017 Section 7.1.2 (RSAES-OAEP-DECRYPT) produces the original messageMwhich may be empty.Both OpenSSL and BoringSSL accept empty OAEP plaintexts.
Changes
Encrypt side (
RsaPublicEncryptEx, line ~3321):inLen == 0rejection to only apply for non-OAEP padding typesDecrypt side (
RsaPrivateDecryptEx, line ~3716):WOLFSSL_RSA_DECRYPT_TO_0_LENis not defined, theret == 0rejection now uses constant-time masking (ctMaskEq) to allow zero-length results forWC_RSA_OAEP_PADwhile preserving the existing behavior for other padding typesSpec references
mLen <= k - 2*hLen - 2;mLen = 0is validMwhich may be the empty stringTesting
Verified with wolfcrypt-ring RSA-OAEP round-trip tests using SHA-1, SHA-256, SHA-384, and SHA-512 with zero-length plaintext.
🤖 Generated with Claude Code