From d0e88a19ca27f26333f18b16e99b528c2ea2f3b2 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 14 Sep 2026 21:38:01 +0300 Subject: [PATCH] gh-157513: Add structured version info for ssl ssl.OPENSSL_VERSION_INFO is now a named tuple. Add ssl.OPENSSL_API_VERSION_INFO which provides information about the version of the OpenSSL library that was used for building the module. The private name _OPENSSL_API_VERSION is kept as an alias. Co-authored-by: Claude Opus 5 (1M context) --- Doc/library/ssl.rst | 18 +++- Doc/whatsnew/3.16.rst | 10 +++ Lib/ssl.py | 1 + Lib/test/pythoninfo.py | 1 + Lib/test/test_ssl.py | 10 ++- ...-09-14-18-36-31.gh-issue-157513.EW4QOC.rst | 3 + Modules/_ssl.c | 83 ++++++++++++++++--- 7 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-14-18-36-31.gh-issue-157513.EW4QOC.rst diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 66fe6c7aee48626..c38763037d67c59 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -982,14 +982,26 @@ Constants .. data:: OPENSSL_VERSION_INFO - A tuple of five integers representing version information about the - OpenSSL library:: + A named tuple of five integers representing version information about the + OpenSSL library loaded by the interpreter: + *major*, *minor*, *fix*, *patch* and *status*:: >>> ssl.OPENSSL_VERSION_INFO - (1, 0, 2, 11, 15) + ssl.OPENSSL_VERSION_INFO(major=3, minor=0, fix=0, patch=13, status=0) .. versionadded:: 3.2 + .. versionchanged:: next + It is now a named tuple. + +.. data:: OPENSSL_API_VERSION_INFO + + A named tuple containing the version of the OpenSSL library that was used + for building the module, with the same fields as :const:`OPENSSL_VERSION_INFO`. + This may be different from the OpenSSL library actually used at runtime. + + .. versionadded:: next + .. data:: OPENSSL_VERSION_NUMBER The raw version number of the OpenSSL library, as a single integer:: diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 53983637f520c8a..c0bc231bef3f023 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -559,6 +559,16 @@ sqlite3 (Contributed by Jiseok CHOI in :gh:`150449`.) +ssl +--- + +* :const:`ssl.OPENSSL_VERSION_INFO` is now a named tuple. + Added :const:`~ssl.OPENSSL_API_VERSION_INFO` which provides information + about the version of the OpenSSL library that was used for building + the module. + (Contributed by Serhiy Storchaka in :gh:`157513`.) + + symtable -------- diff --git a/Lib/ssl.py b/Lib/ssl.py index dc957121728f283..44dc0b046f4518a 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -103,6 +103,7 @@ import _ssl # if we can't import it, let the error propagate from _ssl import OPENSSL_VERSION_NUMBER, OPENSSL_VERSION_INFO, OPENSSL_VERSION +from _ssl import OPENSSL_API_VERSION_INFO from _ssl import _SSLContext, MemoryBIO, SSLSession from _ssl import ( SSLError, SSLZeroReturnError, SSLWantReadError, SSLWantWriteError, diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index b59e2acb9376f1e..04a011b722ac368 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -646,6 +646,7 @@ def format_attr(attr, value): attributes = ( 'OPENSSL_VERSION', 'OPENSSL_VERSION_INFO', + 'OPENSSL_API_VERSION_INFO', 'HAS_SNI', 'OP_ALL', 'OP_NO_TLSv1_1', diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 37323b7ebc6b1ae..2fc6196bd5e1976 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -543,7 +543,7 @@ def test_parse_cert_CVE_2013_4238(self): (('emailAddress', 'python-dev@python.org'),)) self.assertEqual(p['subject'], subject) self.assertEqual(p['issuer'], subject) - if ssl._OPENSSL_API_VERSION >= (0, 9, 8): + if ssl.OPENSSL_API_VERSION_INFO >= (0, 9, 8): san = (('DNS', 'altnull.python.org\x00example.com'), ('email', 'null@python.org\x00user@example.org'), ('URI', 'http://null.python.org\x00http://example.org'), @@ -599,6 +599,14 @@ def test_openssl_version(self): self.assertIsInstance(n, int) self.assertIsInstance(t, tuple) self.assertIsInstance(s, str) + self.assertEqual(len(t), 5) + self.assertEqual(t, (t.major, t.minor, t.fix, t.patch, t.status)) + a = ssl.OPENSSL_API_VERSION_INFO + self.assertIsInstance(a, tuple) + self.assertEqual(len(a), 5) + self.assertEqual(a, (a.major, a.minor, a.fix, a.patch, a.status)) + self.assertIs(ssl._OPENSSL_API_VERSION, a) + self.assertEqual(a.major, t.major) # Some sanity checks follow # >= 1.1.1 self.assertGreaterEqual(n, 0x10101000) diff --git a/Misc/NEWS.d/next/Library/2026-09-14-18-36-31.gh-issue-157513.EW4QOC.rst b/Misc/NEWS.d/next/Library/2026-09-14-18-36-31.gh-issue-157513.EW4QOC.rst new file mode 100644 index 000000000000000..c9d19314a7604d2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-14-18-36-31.gh-issue-157513.EW4QOC.rst @@ -0,0 +1,3 @@ +:const:`ssl.OPENSSL_VERSION_INFO` is now a named tuple. +Add :const:`ssl.OPENSSL_API_VERSION_INFO` which provides information about +the version of the OpenSSL library that was used for building the module. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 73b32c1d86c72ec..a3d7fe9cfa19ed7 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -7242,12 +7242,62 @@ parse_openssl_version(unsigned long libver, *major = libver & 0xFF; } +PyDoc_STRVAR(openssl_version_info__doc__, +"ssl.OPENSSL_VERSION_INFO\n\ +\n\ +OpenSSL version information as a named tuple."); + +static PyStructSequence_Field openssl_version_info_fields[] = { + {"major", "Major release number"}, + {"minor", "Minor release number"}, + {"fix", "Fix release number"}, + {"patch", "Patch release number"}, + {"status", "Release status"}, + {0} +}; + +static PyStructSequence_Desc openssl_version_info_desc = { + "ssl.OPENSSL_VERSION_INFO", /* name */ + openssl_version_info__doc__, /* doc */ + openssl_version_info_fields, /* fields */ + 5 +}; + +static PyObject * +make_openssl_version_info(PyTypeObject *type, unsigned long libver) +{ + PyObject *version; + int pos = 0; + unsigned int major, minor, fix, patch, status; + + parse_openssl_version(libver, &major, &minor, &fix, &patch, &status); + version = PyStructSequence_New(type); + if (version == NULL) { + return NULL; + } + +#define SetItem(VALUE) \ + PyStructSequence_SET_ITEM(version, pos++, VALUE); \ + if (PyErr_Occurred()) { \ + Py_DECREF(version); \ + return NULL; \ + } + + SetItem(PyLong_FromUnsignedLong(major)) + SetItem(PyLong_FromUnsignedLong(minor)) + SetItem(PyLong_FromUnsignedLong(fix)) + SetItem(PyLong_FromUnsignedLong(patch)) + SetItem(PyLong_FromUnsignedLong(status)) +#undef SetItem + + return version; +} + static int sslmodule_init_versioninfo(PyObject *m) { PyObject *r; unsigned long libver; - unsigned int major, minor, fix, patch, status; /* OpenSSL version */ /* SSLeay() gives us the version of the library linked against, @@ -7258,20 +7308,33 @@ sslmodule_init_versioninfo(PyObject *m) if (PyModule_Add(m, "OPENSSL_VERSION_NUMBER", r) < 0) return -1; - parse_openssl_version(libver, &major, &minor, &fix, &patch, &status); - r = Py_BuildValue("IIIII", major, minor, fix, patch, status); - if (PyModule_Add(m, "OPENSSL_VERSION_INFO", r) < 0) - return -1; - r = PyUnicode_FromString(OpenSSL_version(OPENSSL_VERSION)); if (PyModule_Add(m, "OPENSSL_VERSION", r) < 0) return -1; - libver = OPENSSL_VERSION_NUMBER; - parse_openssl_version(libver, &major, &minor, &fix, &patch, &status); - r = Py_BuildValue("IIIII", major, minor, fix, patch, status); - if (PyModule_Add(m, "_OPENSSL_API_VERSION", r) < 0) + PyTypeObject *version_type; + version_type = PyStructSequence_NewType(&openssl_version_info_desc); + if (version_type == NULL) { + return -1; + } + if (PyModule_Add(m, "OPENSSL_VERSION_INFO", + make_openssl_version_info(version_type, libver)) < 0) + { + Py_DECREF(version_type); + return -1; + } + r = make_openssl_version_info(version_type, OPENSSL_VERSION_NUMBER); + Py_DECREF(version_type); + if (r == NULL) { return -1; + } + if (PyModule_AddObjectRef(m, "OPENSSL_API_VERSION_INFO", r) < 0 || + PyModule_AddObjectRef(m, "_OPENSSL_API_VERSION", r) < 0) + { + Py_DECREF(r); + return -1; + } + Py_DECREF(r); return 0; }