diff --git a/0076-asn1_d2i_read_bio_EOF.patch b/0076-asn1_d2i_read_bio_EOF.patch new file mode 100644 index 0000000..b475c0c --- /dev/null +++ b/0076-asn1_d2i_read_bio_EOF.patch @@ -0,0 +1,313 @@ +From 3687ff719503ffc6d29c97d77e00199251890774 Mon Sep 17 00:00:00 2001 +From: Marc Gutman +Date: Thu, 9 Jul 2026 15:23:28 -0500 +Subject: [PATCH] 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, on an EOF in the +middle of an object (some bytes already buffered), or on an EOF while still +inside an indefinite-length value awaiting its end-of-contents octets - all +of which are genuine truncation. A clean EOF at a top-level object boundary +again fails without queuing an error, restoring the long-standing behaviour +that looping callers depend on. + +Add regression tests covering the clean-EOF, truncated, indefinite-length +truncation and partial-header cases, and document the read behaviour in +ASN1_item_d2i_bio(3). + +Fixes #31807 + +Assisted-by: Claude:claude-opus-4-8 + +Reviewed-by: Dmitry Belyavskiy +Reviewed-by: Igor Ustinov +Reviewed-by: Tomas Mraz +MergeDate: Mon Jul 13 08:05:03 2026 +(Merged from https://github.com/openssl/openssl/pull/31818) + +(cherry picked from commit d7e77b66cabb770932091ed5986221e0d1e57144) +--- + crypto/asn1/a_d2i_fp.c | 15 ++- + doc/man3/ASN1_item_d2i_bio.pod | 18 +++- + test/asn1_decode_test.c | 165 +++++++++++++++++++++++++++++++++ + 3 files changed, 196 insertions(+), 2 deletions(-) + +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/doc/man3/ASN1_item_d2i_bio.pod b/doc/man3/ASN1_item_d2i_bio.pod +index f8e4678367..9b3f389a23 100644 +--- a/doc/man3/ASN1_item_d2i_bio.pod ++++ b/doc/man3/ASN1_item_d2i_bio.pod +@@ -59,6 +59,16 @@ B provided in the I parameter and the property query + string in I. See L for more information + about algorithm fetching. + ++When reading from I, decoding consumes one complete DER-encoded structure ++and leaves any following bytes in the BIO, so concatenated structures can be ++read with successive calls. Reaching the end of the input cleanly, at a ++structure boundary, is not treated as an error: the function returns NULL ++without adding to the error queue. If the end of the input is reached in the ++middle of a structure, or an indefinite-length value is missing its ++end-of-contents octets (that is, the input is truncated), an error is queued ++with reason code B. The same applies to ++ASN1_item_d2i_fp_ex(). ++ + ASN1_item_d2i_bio() is the same as ASN1_item_d2i_bio_ex() except that the + default B is used (i.e. NULL) and with a NULL property query + string. +@@ -92,6 +102,12 @@ that the I and I can be used when doing algorithm fetching. + ASN1_item_d2i_bio(), ASN1_item_unpack_ex() and ASN1_item_unpack() return a pointer to + an B or NULL on error. + ++The ASN1_item_d2i_bio() and ASN1_item_d2i_fp() functions, including their ++B<_ex> variants, also return NULL at a clean end of input. In that case the ++error queue is left unchanged, so a caller reading concatenated structures in ++a loop can distinguish a clean end of input from a decoding error by ++inspecting the error queue, for example with L. ++ + ASN1_item_i2d_mem_bio() returns a pointer to a memory BIO or NULL on error. + + ASN1_item_pack() returns a pointer to an B or NULL on error. +@@ -105,7 +121,7 @@ The function ASN1_item_unpack_ex() was added in OpenSSL 3.2. + + =head1 COPYRIGHT + +-Copyright 2021-2023 The OpenSSL Project Authors. All Rights Reserved. ++Copyright 2021-2026 The OpenSSL Project Authors. All Rights Reserved. + + Licensed under the Apache License 2.0 (the "License"). You may not use + this file except in compliance with the License. You can obtain a copy +diff --git a/test/asn1_decode_test.c b/test/asn1_decode_test.c +index 6e00d40896..8a9629c21d 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,163 @@ 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; ++ ++ 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(); ++ ++ 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 +440,9 @@ 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); ++ ADD_TEST(test_d2i_read_bio_partial_header); + return 1; + } +-- +2.54.0 + diff --git a/openssl.spec b/openssl.spec index 9f616b2..2ddad1c 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.5 -Release: 5%{?dist}.alma.1 +Release: 6%{?dist}.alma.1 Epoch: 1 Source0: openssl-%{version}.tar.gz Source1: fips-hmacify.sh @@ -117,6 +117,7 @@ Patch0072: 0072-CVE-2026-45446.patch Patch0073: 0073-CVE-2026-45447.patch Patch0074: 0074-CVE-2026-34182.patch Patch0075: 0075-asn1_d2i_read_bio-blocking.patch +Patch0076: 0076-asn1_d2i_read_bio_EOF.patch License: Apache-2.0 URL: http://www.openssl.org/ @@ -480,9 +481,13 @@ touch $RPM_BUILD_ROOT/%{_prefix}/include/openssl/engine.h %ldconfig_scriptlets libs %changelog -* Tue Jul 14 2026 Eduard Abdullin - 1:3.5.5-5.alma.1 +* Mon Jul 27 2026 Eduard Abdullin - 1:3.5.5-6.alma.1 - Redefine sslarch for x86_64_v2 arch +* Wed Jul 15 2026 Pavol Žáčik - 1:3.5.5-6 +- Patch asn1_d2i_read_bio to not raise NOT_ENOUGH_DATA at object boundary + Resolves: RHEL-210864 + * Thu Jun 25 2026 Pavol Žáčik - 1:3.5.5-5 - Patch asn1_d2i_read_bio to read headers without blocking Resolves: RHEL-169991