From d8bc2379c748b7e85b42fdf2ec259d450dcf8f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavol=20=C5=BD=C3=A1=C4=8Dik?= Date: Tue, 7 Jul 2026 21:28:21 +0200 Subject: [PATCH] asn1: Do not raise NOT_ENOUGH_DATA at object boundary Resolves: RHEL-192992 --- 0058-asn1_d2i_read_bio_EOF.patch | 304 +++++++++++++++++++++++++++++++ openssl.spec | 7 +- 2 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 0058-asn1_d2i_read_bio_EOF.patch diff --git a/0058-asn1_d2i_read_bio_EOF.patch b/0058-asn1_d2i_read_bio_EOF.patch new file mode 100644 index 0000000..b305d6e --- /dev/null +++ b/0058-asn1_d2i_read_bio_EOF.patch @@ -0,0 +1,304 @@ +From 836406633204bccb6d7a8718e15de8c3c2213a8e Mon Sep 17 00:00:00 2001 +From: icanhasmath +Date: Tue, 30 Jun 2026 16:55:40 -0500 +Subject: [PATCH 1/2] Don't raise NOT_ENOUGH_DATA on a clean EOF at an object + boundary + +asn1_d2i_read_bio() reads one ASN.1 object at a time from a BIO. Callers +commonly loop, decoding concatenated DER values until the call fails, and +rely on a failure with no queued error to recognise a clean end of input. +CPython's ssl module does this in _add_ca_certs() when loading the Windows +certificate store via SSLContext.load_verify_locations(cadata=...); it +re-raises any leftover ASN.1 error other than ASN1_R_HEADER_TOO_LONG as +fatal. + +Commit 9eb6922c59 ("asn1: raise NOT_ENOUGH_DATA on header EOF") changed the +BIO_read() check from "i < 0" to "i <= 0", so a clean EOF (BIO_read() +returning 0, as an exhausted BIO_new_mem_buf does) on an object boundary now +raises ASN1_R_NOT_ENOUGH_DATA instead of failing with an empty error queue. +The rewrite in commit 35852da1d9 carried this behaviour forward. As a +result Python 3 on Windows fails to initialise an SSLContext with: + + ssl.SSLError: [ASN1: NOT_ENOUGH_DATA] not enough data + +Raise ASN1_R_NOT_ENOUGH_DATA only on an actual read error, or when EOF is +reached in the middle of an object (diff != 0), which is genuine truncation. +A clean EOF on an object boundary again fails without queuing an error, +restoring the long-standing behaviour that looping callers depend on. + +Add regression tests for both the clean-EOF and truncated cases, and a +CHANGES.md entry. + +Fixes #31807 + +Assisted-by: Claude:claude-opus-4-8 +--- + crypto/asn1/a_d2i_fp.c | 15 ++++- + test/asn1_decode_test.c | 130 ++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 144 insertions(+), 1 deletion(-) + +diff --git a/crypto/asn1/a_d2i_fp.c b/crypto/asn1/a_d2i_fp.c +index a23dea8ebd..19595683ef 100644 +--- a/crypto/asn1/a_d2i_fp.c ++++ b/crypto/asn1/a_d2i_fp.c +@@ -139,7 +139,20 @@ int asn1_d2i_read_bio(BIO *in, BUF_MEM **pb) + } + i = BIO_read(in, &(b->data[len]), want); + if (i <= 0) { +- ERR_raise(ERR_LIB_ASN1, ASN1_R_NOT_ENOUGH_DATA); ++ /* ++ * A read error (i < 0), an EOF in the middle of an object ++ * (diff != 0, some bytes already buffered), or an EOF while ++ * still inside an indefinite-length constructed value awaiting ++ * its end-of-contents octets (eos != 0) all mean the input is ++ * truncated. Only a clean EOF at a top-level object boundary ++ * (i == 0, diff == 0, eos == 0) is the normal end of input: ++ * fail without queuing an error so that callers looping over ++ * concatenated DER values (e.g. the libcrypto d2i_*_bio() ++ * consumers in CPython's ssl module) terminate cleanly instead ++ * of seeing a spurious ASN1_R_NOT_ENOUGH_DATA. ++ */ ++ if (i < 0 || diff != 0 || eos != 0) ++ ERR_raise(ERR_LIB_ASN1, ASN1_R_NOT_ENOUGH_DATA); + goto err; + } + if (i > 0) { +diff --git a/test/asn1_decode_test.c b/test/asn1_decode_test.c +index 6e00d40896..ec20d6ebbc 100644 +--- a/test/asn1_decode_test.c ++++ b/test/asn1_decode_test.c +@@ -14,7 +14,11 @@ + #include + #include + #include ++#include ++#include ++#include + #include "internal/numbers.h" ++#include "internal/asn1.h" + #include "testutil.h" + + #ifdef __GNUC__ +@@ -266,6 +270,129 @@ err: + return ret; + } + ++/* ++ * A minimal, complete DER object: SEQUENCE { INTEGER 0 }. ++ * asn1_d2i_read_bio() should consume exactly these bytes. ++ */ ++static const unsigned char one_obj[] = { ++ 0x30, 0x03, /* SEQUENCE, length 3 */ ++ 0x02, 0x01, 0x00 /* INTEGER 0 */ ++}; ++ ++/* ++ * Reading concatenated DER objects from a BIO must stop cleanly at EOF: ++ * once the input is exhausted on an object boundary, asn1_d2i_read_bio() ++ * returns < 0 and must NOT leave an error on the queue. Callers that loop ++ * over concatenated values (e.g. CPython's ssl module loading the Windows ++ * certificate store via d2i_X509_bio()) rely on this to detect end-of-input; ++ * a spurious ASN1_R_NOT_ENOUGH_DATA there is reported as a fatal error. ++ */ ++static int test_d2i_read_bio_clean_eof(void) ++{ ++ unsigned char two_objs[sizeof(one_obj) * 2]; ++ BIO *bio = NULL; ++ BUF_MEM *buf = NULL; ++ int ret = 0; ++ ++ memcpy(two_objs, one_obj, sizeof(one_obj)); ++ memcpy(two_objs + sizeof(one_obj), one_obj, sizeof(one_obj)); ++ ++ if (!TEST_ptr(bio = BIO_new_mem_buf(two_objs, sizeof(two_objs)))) ++ goto err; ++ ERR_clear_error(); ++ ++ /* Both complete objects are read, one per call. */ ++ if (!TEST_int_eq(asn1_d2i_read_bio(bio, &buf), (int)sizeof(one_obj))) ++ goto err; ++ BUF_MEM_free(buf); ++ buf = NULL; ++ if (!TEST_int_eq(asn1_d2i_read_bio(bio, &buf), (int)sizeof(one_obj))) ++ goto err; ++ BUF_MEM_free(buf); ++ buf = NULL; ++ ++ /* Clean EOF: failure return, but no error must be queued. */ ++ if (!TEST_int_lt(asn1_d2i_read_bio(bio, &buf), 0)) ++ goto err; ++ if (!TEST_ulong_eq(ERR_peek_error(), 0)) ++ goto err; ++ ++ ret = 1; ++err: ++ BUF_MEM_free(buf); ++ BIO_free(bio); ++ return ret; ++} ++ ++/* ++ * In contrast, hitting EOF in the middle of an object is genuine truncation ++ * and must still be reported as ASN1_R_NOT_ENOUGH_DATA. ++ */ ++static int test_d2i_read_bio_truncated(void) ++{ ++ static const unsigned char truncated[] = { ++ 0x30, 0x05, /* SEQUENCE claims 5 content bytes ... */ ++ 0x02, 0x01 /* ... but only 2 are present */ ++ }; ++ BIO *bio = NULL; ++ BUF_MEM *buf = NULL; ++ unsigned long e; ++ int ret = 0; ++ ++ if (!TEST_ptr(bio = BIO_new_mem_buf(truncated, sizeof(truncated)))) ++ goto err; ++ ERR_clear_error(); ++ ++ if (!TEST_int_lt(asn1_d2i_read_bio(bio, &buf), 0)) ++ goto err; ++ e = ERR_peek_last_error(); ++ if (!TEST_int_eq(ERR_GET_LIB(e), ERR_LIB_ASN1) ++ || !TEST_int_eq(ERR_GET_REASON(e), ASN1_R_NOT_ENOUGH_DATA)) ++ goto err; ++ ++ ret = 1; ++err: ++ BUF_MEM_free(buf); ++ BIO_free(bio); ++ return ret; ++} ++ ++/* ++ * An EOF reached while still inside an indefinite-length constructed value, ++ * before its end-of-contents octets, is truncation too (not a clean boundary), ++ * so it must also report ASN1_R_NOT_ENOUGH_DATA rather than an empty queue. ++ */ ++static int test_d2i_read_bio_indefinite_truncated(void) ++{ ++ /* SEQUENCE (indefinite) { INTEGER 0 } with the 00 00 EOC missing */ ++ static const unsigned char truncated_indefinite[] = { ++ 0x30, 0x80, /* SEQUENCE, indefinite length */ ++ 0x02, 0x01, 0x00 /* INTEGER 0; no end-of-contents octets follow */ ++ }; ++ BIO *bio = NULL; ++ BUF_MEM *buf = NULL; ++ unsigned long e; ++ int ret = 0; ++ ++ if (!TEST_ptr(bio = BIO_new_mem_buf(truncated_indefinite, ++ sizeof(truncated_indefinite)))) ++ goto err; ++ ERR_clear_error(); ++ ++ if (!TEST_int_lt(asn1_d2i_read_bio(bio, &buf), 0)) ++ goto err; ++ e = ERR_peek_last_error(); ++ if (!TEST_int_eq(ERR_GET_LIB(e), ERR_LIB_ASN1) ++ || !TEST_int_eq(ERR_GET_REASON(e), ASN1_R_NOT_ENOUGH_DATA)) ++ goto err; ++ ++ ret = 1; ++err: ++ BUF_MEM_free(buf); ++ BIO_free(bio); ++ return ret; ++} ++ + int setup_tests(void) + { + #ifndef OPENSSL_NO_DEPRECATED_3_0 +@@ -279,5 +406,8 @@ int setup_tests(void) + ADD_TEST(test_utctime); + ADD_TEST(test_invalid_template); + ADD_TEST(test_reuse_asn1_object); ++ ADD_TEST(test_d2i_read_bio_clean_eof); ++ ADD_TEST(test_d2i_read_bio_truncated); ++ ADD_TEST(test_d2i_read_bio_indefinite_truncated); + return 1; + } +-- +2.54.0 + + +From a7272fcb339ec8e11578a23fcd667167b6dbab49 Mon Sep 17 00:00:00 2001 +From: icanhasmath +Date: Fri, 3 Jul 2026 09:32:10 -0500 +Subject: [PATCH 2/2] test: cover the partial-header EOF case in + asn1_d2i_read_bio + +The existing asn1_d2i_read_bio tests cover a clean EOF at an object +boundary, an EOF in the middle of an object body, and an EOF inside an +unterminated indefinite-length value. None exercised an EOF reached +part-way through an object header, with header bytes already buffered, +which is the "diff != 0" arm of the header-read truncation check. + +Add test_d2i_read_bio_partial_header feeding a SEQUENCE whose 2-byte +long-form length is truncated after the first byte, and assert it reports +ASN1_R_NOT_ENOUGH_DATA. + +Assisted-by: Claude:claude-opus-4-8 +--- + test/asn1_decode_test.c | 39 +++++++++++++++++++++++++++++++++++++-- + 1 file changed, 37 insertions(+), 2 deletions(-) + +diff --git a/test/asn1_decode_test.c b/test/asn1_decode_test.c +index ec20d6ebbc..8a9629c21d 100644 +--- a/test/asn1_decode_test.c ++++ b/test/asn1_decode_test.c +@@ -374,8 +374,42 @@ static int test_d2i_read_bio_indefinite_truncated(void) + unsigned long e; + int ret = 0; + +- if (!TEST_ptr(bio = BIO_new_mem_buf(truncated_indefinite, +- sizeof(truncated_indefinite)))) ++ bio = BIO_new_mem_buf(truncated_indefinite, sizeof(truncated_indefinite)); ++ if (!TEST_ptr(bio)) ++ goto err; ++ ERR_clear_error(); ++ ++ if (!TEST_int_lt(asn1_d2i_read_bio(bio, &buf), 0)) ++ goto err; ++ e = ERR_peek_last_error(); ++ if (!TEST_int_eq(ERR_GET_LIB(e), ERR_LIB_ASN1) ++ || !TEST_int_eq(ERR_GET_REASON(e), ASN1_R_NOT_ENOUGH_DATA)) ++ goto err; ++ ++ ret = 1; ++err: ++ BUF_MEM_free(buf); ++ BIO_free(bio); ++ return ret; ++} ++ ++/* ++ * An EOF reached part-way through an object's header, with some header bytes ++ * already buffered, is truncation as well. This exercises the "diff != 0" arm ++ * of the header-read check (distinct from the body read handled elsewhere). ++ */ ++static int test_d2i_read_bio_partial_header(void) ++{ ++ /* SEQUENCE with a 2-byte long-form length, but only one length byte given */ ++ static const unsigned char partial_header[] = { ++ 0x30, 0x82, 0x01 /* SEQUENCE, length declared as 2 bytes, 1 present */ ++ }; ++ BIO *bio = NULL; ++ BUF_MEM *buf = NULL; ++ unsigned long e; ++ int ret = 0; ++ ++ if (!TEST_ptr(bio = BIO_new_mem_buf(partial_header, sizeof(partial_header)))) + goto err; + ERR_clear_error(); + +@@ -409,5 +443,6 @@ int setup_tests(void) + ADD_TEST(test_d2i_read_bio_clean_eof); + ADD_TEST(test_d2i_read_bio_truncated); + ADD_TEST(test_d2i_read_bio_indefinite_truncated); ++ ADD_TEST(test_d2i_read_bio_partial_header); + return 1; + } +-- +2.54.0 + diff --git a/openssl.spec b/openssl.spec index 8b76a18..300e9a8 100644 --- a/openssl.spec +++ b/openssl.spec @@ -29,7 +29,7 @@ print(string.sub(hash, 0, 16)) Summary: Utilities from the general purpose cryptography library with TLS implementation Name: openssl Version: 3.5.7 -Release: 1%{?dist} +Release: 2%{?dist} Epoch: 1 Source0: openssl-%{version}.tar.gz Source1: fips-hmacify.sh @@ -99,6 +99,7 @@ Patch0054: 0054-Temporarily-disable-SLH-DSA-FIPS-self-tests.patch Patch0055: 0055-Add-a-define-to-disable-symver-attributes.patch Patch0056: 0056-Add-targets-to-skip-build-of-non-installable-program.patch Patch0057: 0057-Disable-RSA-PKCS1.5-FIPS-POST-not-relevant-for-RHEL.patch +Patch0058: 0058-asn1_d2i_read_bio_EOF.patch License: Apache-2.0 URL: http://www.openssl.org/ @@ -459,6 +460,10 @@ touch $RPM_BUILD_ROOT/%{_prefix}/include/openssl/engine.h %ldconfig_scriptlets libs %changelog +* Tue Jul 07 2026 Pavol Žáčik - 1:3.5.7-2 +- Backport https://github.com/openssl/openssl/pull/31818 + Resolves: RHEL-192992 + * Fri Jun 12 2026 Pavol Žáčik - 1:3.5.7-1 - Rebase to OpenSSL 3.5.7 Resolves: RHEL-179698