asn1: Do not raise NOT_ENOUGH_DATA at object boundary

Resolves: RHEL-192992
This commit is contained in:
Pavol Žáčik 2026-07-07 21:28:21 +02:00
parent 62e30f3668
commit d8bc2379c7
No known key found for this signature in database
GPG Key ID: 4EE16C6E333F70A8
2 changed files with 310 additions and 1 deletions

View File

@ -0,0 +1,304 @@
From 836406633204bccb6d7a8718e15de8c3c2213a8e Mon Sep 17 00:00:00 2001
From: icanhasmath <marcg@activestate.com>
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 <openssl/asn1.h>
#include <openssl/asn1t.h>
#include <openssl/obj_mac.h>
+#include <openssl/bio.h>
+#include <openssl/buffer.h>
+#include <openssl/err.h>
#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 <marcg@activestate.com>
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

View File

@ -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 <pzacik@redhat.com> - 1:3.5.7-2
- Backport https://github.com/openssl/openssl/pull/31818
Resolves: RHEL-192992
* Fri Jun 12 2026 Pavol Žáčik <pzacik@redhat.com> - 1:3.5.7-1
- Rebase to OpenSSL 3.5.7
Resolves: RHEL-179698