From e35dcf9103f177ff8d8e775083230c8f531e273d Mon Sep 17 00:00:00 2001 From: Toddr Bot Date: Sat, 23 May 2026 01:31:39 +0000 Subject: [PATCH] Use byte length for plaintext size validation check_max_message_length() used sv_len() which returns character count for UTF-8 SVs, but rsa_crypt() passes the raw bytes to OpenSSL via SvPV(). For multi-byte UTF-8 strings the byte count exceeds the character count, so the pre-validation could pass while OpenSSL rejects the oversized input with a generic error instead of our descriptive "plaintext too long" message. Switch check_max_message_length() to accept the SV directly and extract byte length via SvPV(), matching what rsa_crypt() actually sends to OpenSSL. Co-Authored-By: Claude Opus 4.6 --- RSA.xs | 11 ++++++++--- t/crypto.t | 19 ++++++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/RSA.xs b/RSA.xs index 238bebb..947c3fb 100644 --- a/RSA.xs +++ b/RSA.xs @@ -426,10 +426,15 @@ EVP_PKEY* _load_rsa_key(SV* p_keyStringSv, return rsa; } -static void check_max_message_length(rsaData* p_rsa, STRLEN from_length) { +static void check_max_message_length(rsaData* p_rsa, SV* from_sv) { int size; int max_len = -1; const char *pad_name = NULL; + STRLEN from_length; + + /* sv_len() returns character count for UTF-8 SVs, but encryption + operates on bytes. SvPV always returns the byte length. */ + (void)SvPV(from_sv, from_length); size = EVP_PKEY_get_size(p_rsa->rsa); @@ -1204,7 +1209,7 @@ encrypt(p_rsa, p_plaintext) rsaData* p_rsa; SV* p_plaintext; CODE: - check_max_message_length(p_rsa, sv_len(p_plaintext)); + check_max_message_length(p_rsa, p_plaintext); #if OPENSSL_VERSION_NUMBER >= 0x30000000L RETVAL = rsa_crypt(p_rsa, p_plaintext, EVP_PKEY_encrypt, EVP_PKEY_encrypt_init, 1 /* is_encrypt */); #else @@ -1247,7 +1252,7 @@ private_encrypt(p_rsa, p_plaintext) croak("PSS padding with private_encrypt/public_decrypt is not supported. " "Use sign()/verify() for PSS signatures."); } - check_max_message_length(p_rsa, sv_len(p_plaintext)); + check_max_message_length(p_rsa, p_plaintext); #if OPENSSL_VERSION_NUMBER >= 0x30000000L RETVAL = rsa_crypt(p_rsa, p_plaintext, EVP_PKEY_sign, EVP_PKEY_sign_init, 0 /* is_encrypt */); #else diff --git a/t/crypto.t b/t/crypto.t index 583031d..70c7eaa 100644 --- a/t/crypto.t +++ b/t/crypto.t @@ -7,7 +7,7 @@ use Crypt::OpenSSL::RSA; # Tests for encrypt/decrypt error paths, boundary conditions, and edge cases. # These cover gaps not addressed by rsa.t or padding.t. -plan tests => 20; +plan tests => 22; Crypt::OpenSSL::Random::random_seed("OpenSSL needs at least 32 bytes."); Crypt::OpenSSL::RSA->import_random_seed(); @@ -156,6 +156,23 @@ $rsa->use_pkcs1_oaep_padding(); "no-padding oversized plaintext gives clear error message"); } +# --- UTF-8 byte-length validation --- +# sv_len() returns character count for UTF-8 SVs, which is shorter than the +# byte count. The length check must use byte count since OpenSSL operates +# on bytes. Construct a UTF-8 string where chars <= max but bytes > max. + +{ + $rsa->use_pkcs1_oaep_padding(); + my $utf8_str = "\xe9" x 120; + utf8::upgrade($utf8_str); # 120 chars, 240 UTF-8 bytes + require bytes; + ok(bytes::length($utf8_str) > $oaep_max, + "UTF-8 test string byte length exceeds OAEP max"); + eval { $rsa->encrypt($utf8_str) }; + like($@, qr/plaintext too long/, + "length check uses byte count for UTF-8 strings"); +} + # Decrypt still works (no false positive from validation) { $rsa->use_pkcs1_oaep_padding();